Files
python-sdk/tests/test_smoke.py
T
zeekayandHanzo Dev eb953b9fd5 test_smoke: billing takes the seat crm no longer holds
The document at the pinned ref names no `crm` tag and serves no /v1/crm,
so `hanzoai.cloud.api.crm_api` is a module the generator had nothing to
write — the import has raised ModuleNotFoundError since the emission that
dropped it.

This is the document's call, not a generation defect. The client emits one
module per tag and its 120 modules are exactly the 120 tags api.hanzo.ai
declares, with no difference in either direction; cli's spec/cloud.json,
projected from this same ref and digest, likewise carries neither the tag
nor the routes. So the expectation moves to the spec rather than the
client moving to the expectation.

Unlike tracker, analytics and chat before it, crm did not fold into
another product: contacts, companies, opportunities and applications have
no successor address anywhere in the document. billing replaces it because
it has been in every emission since the first generated client, which is
the property the test is really asserting — that whole products, not just
the AI routes, reach the SDK.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-04 00:09:01 -07:00

160 lines
6.9 KiB
Python

"""Smoke tests for the generated `hanzoai` client.
The client is `hanzoai.cloud`, generated from hanzoai/cloud's openapi.yaml at the
ref .spec-lock names. These assert the package imports and that the unified
api.hanzo.ai/v1 product surface is present — not network behaviour.
"""
import hanzoai
import hanzoai.cloud
def test_client_constructs():
cfg = hanzoai.cloud.Configuration(host="https://api.hanzo.ai")
cfg.access_token = "sk-test"
client = hanzoai.cloud.ApiClient(cfg)
assert client is not None
assert cfg.host == "https://api.hanzo.ai"
def test_the_bearer_comes_from_the_configuration():
"""`Configuration(access_token=…)` reaches the wire as `Authorization`.
The document declares `securitySchemes.bearer` and applies it document-wide,
so every operation carries `auth_settings=['bearer']` and
`_apply_auth_params` sets the header. This asserts the whole path, not just
the dict: serialize a real operation and read the header it produced.
It asserted the opposite until the document grew the scheme — that a caller
who set `access_token` sent nothing at all, and had to pass the header to
`ApiClient` by hand. Keeping the negative here as an inverted assertion is
what makes the fix a fact rather than a claim.
"""
cfg = hanzoai.cloud.Configuration(host="https://api.hanzo.ai", access_token="sk-test")
assert cfg.auth_settings()["bearer"] == {
"type": "bearer", "in": "header", "key": "Authorization",
"value": "Bearer sk-test",
}
api = hanzoai.cloud.IamApi(hanzoai.cloud.ApiClient(cfg))
_, _, headers, _, _ = api._get_iam_keys_serialize(
owner=None, _request_auth=None, _content_type=None, _headers=None, _host_index=0)
assert headers["Authorization"] == "Bearer sk-test"
def test_the_public_operations_send_no_credential():
"""An operation the document marks `security: []` must not carry the header.
A client that attaches a credential to an operation that takes none makes
the API look like it authenticates when it does not. Five operations carry
the marking; `GET /v1/openapi.json` is the one that has carried it across
every pin, so it is what the mechanism is held to. `/v1/models` was here
until it stopped declaring the marking, which is the document's call.
"""
cfg = hanzoai.cloud.Configuration(host="https://api.hanzo.ai", access_token="sk-test")
api = hanzoai.cloud.OpenapiApi(hanzoai.cloud.ApiClient(cfg))
_, _, headers, _, _ = api._get_openapi_json_serialize(
_request_auth=None, _content_type=None, _headers=None, _host_index=0)
assert "Authorization" not in headers
def test_product_apis_present():
"""Whole products, not just the AI routes, reach the SDK.
Name products the document still tags, read off the client rather than
guessed. Tags move: `tracker`, then `analytics` and `chat`, each stood here
until cloud folded those routes into the product that owns them. `crm` left
differently — cloud withdrew /v1/crm entirely, so contacts, companies,
opportunities and applications have no successor address to follow.
`billing` takes the seat because it has been in every emission since the
first, which is the property this test wants: a product broad enough that
losing it would mean the client stopped projecting whole products.
"""
from hanzoai.cloud.api.billing_api import BillingApi
from hanzoai.cloud.api.commerce_api import CommerceApi
from hanzoai.cloud.api.iam_api import IamApi
client = hanzoai.cloud.ApiClient(hanzoai.cloud.Configuration(host="https://api.hanzo.ai"))
for cls in (BillingApi, CommerceApi, IamApi):
assert cls(client) is not None
def test_full_surface_breadth():
import glob
import os
api_dir = os.path.dirname(hanzoai.cloud.api.__file__)
modules = glob.glob(os.path.join(api_dir, "*_api.py"))
# One API group per TAG in the document, not the ~40 of the LLM-gateway
# client this replaced. 117 at the pinned ref, where cloud carries 130
# products; the floor is a breadth check, not a count to keep in step.
assert len(modules) > 100, f"only {len(modules)} api modules — surface too small"
def test_one_client_only():
"""There is exactly one generated client in this distribution.
`hanzoai.api` / `hanzoai.models` was a second one, re-exported flat from the
package root: 1,695 routes, of which 1,452 no longer exist in the document.
A caller reaching for the obvious flat name got a client that 404s. This
refuses its return — under that name or any other name beside `cloud`.
"""
import pkgutil
generated = {
m.name
for m in pkgutil.iter_modules(hanzoai.__path__)
if m.ispkg and m.name in {"cloud", "api", "models"}
}
assert generated == {"cloud"}, f"more than one generated client: {sorted(generated)}"
def test_version_is_single_sourced():
"""`hanzoai.__version__` must be the installed distribution, never a literal.
The generator writes `__version__ = "1.0.0"` (its default) into the package
__init__; that literal disagreed with pyproject for every release.
"""
from importlib.metadata import version
assert hanzoai.__version__ == version("hanzoai")
def test_cloud_package_imports():
"""`hanzoai.cloud` is the generated client — it must import cleanly.
This guarded a tag-casing collision in the retired hanzo.yaml lineage: tags
differing only by case (`AI`/`ai`, `API Keys`/`api-keys`, `MCP`/`mcp`) made
openapi-generator emit imports for classes it never wrote, so
`import hanzoai.cloud` raised ImportError. The client is a projection of
hanzoai/cloud's own emission now, which carries ONE spelling per tag, so the
class names lost their all-caps variants: `AIApi`/`APIKeysApi`/`MCPApi` are
`AiApi`/`KeysApi`/`McpApi`. The collision cannot recur while one document
with one spelling per tag is the only input; what this still pins is that
the top-level names resolve at all.
"""
import hanzoai.cloud
for name in ("AiApi", "IamApi", "ToolsApi", "OpenapiApi"):
assert hasattr(hanzoai.cloud, name), f"hanzoai.cloud.{name} missing"
def test_plugin_operator_surface():
"""The plugin operator surface must be reachable from the SDK.
Four published routes under `tools`: GET /v1/tools/plugins, GET and DELETE
/v1/tools/plugins/authored[/{id}], POST /v1/tools/plugins/build. This asked
`AdminApi` for /v1/admin/plugins until cloud stopped publishing an `admin`
tag; that address still answers, and a client is a projection of the
document, which no longer names it.
"""
from hanzoai.cloud.api.tools_api import ToolsApi
for op in (
"get_tools_plugins",
"get_tools_plugins_authored",
"delete_tools_plugins_authored_by_id",
"post_tools_plugins_build",
):
assert callable(getattr(ToolsApi, op, None)), f"ToolsApi.{op} missing"