Every Hanzo SDK is supposed to ship the SAME six examples — hello, chat, money,
store, agent, tools. Until now that was six repos independently remembering to
agree, and nothing checked. Nothing could: the flows existed in no manifest, in
no repo, in any language. The TypeScript and Python sets are the first two, and
writing them is what made the gap obvious.
flows.yaml is data only, like sdks.yaml beside it: per flow, the operationIds in
hanzo.yaml in call order, plus why THAT route and not a neighbour. Those choices
are the part worth writing down —
hello ai_getAccount, not bot_authMe (bot-service scoped) and not the IAM OIDC
userinfo endpoint, which is on a different ORIGIN; using it would force
every example set to know two base URLs, and IAM answers 200 text/html
for an unregistered path, so a typo there is silent breakage, not a 404.
tools automations_mcp, not tasks_tasksMcp: only the former has typed
request/response schemas, so `method` is a checked enum in every
generated client instead of a free string.
store the data plane (/v1/kv/keys/*), not the provisioning plane (/v1/kv,
which creates stores) — the two are easy to confuse by name alone.
test_flows.py asserts every id named here still exists in hanzo.yaml. Verified it
fails on a typo, not just passes on the truth:
FAIL test_every_operation_exists_in_the_spec: flows.yaml names operations
hanzo.yaml does not have: [('hello', 'ai_getAccount_TYPO')]
So deleting a route breaks ONE gate here rather than six SDKs' examples quietly
and separately.
Also gives this repo a `test:` block, which it did not have. 25 tests lived here
and not one had ever gated a commit — and that is not academic: hanzo.yaml spent
this entire period unable to generate a client in ANY language, because 35
/v1/platform operations carried no `responses` and openapi-generator refuses the
whole document over it. Nothing here noticed, because nothing here ever tried.
hanzo.yaml is the single input to seven language SDKs; a defect merged here is a
defect merged into all seven at once.
25 passed in 19.99s
Co-authored-by: Hanzo Dev <[email protected]>
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for flows.yaml — the manifest that keeps every SDK's examples the same six.
|
|
|
|
Runnable standalone (`python3 test_flows.py`, non-zero exit on failure) AND under
|
|
pytest, like test_audit.py beside it.
|
|
|
|
The manifest is only worth having if it cannot drift from the spec it names. An
|
|
operationId that no longer exists is the exact failure this catches: without it,
|
|
deleting a route upstream leaves six SDKs each carrying an example that no longer
|
|
compiles, discovered six separate times. Here it is one red test in one repo.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
import yaml
|
|
|
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# The set is closed on purpose. A seventh flow is a fleet-wide decision — every
|
|
# language owes the same six — so adding one should require editing this line.
|
|
CANONICAL = ["hello", "chat", "money", "store", "agent", "tools"]
|
|
|
|
|
|
def load():
|
|
flows = yaml.safe_load(open(os.path.join(ROOT, "flows.yaml")))["flows"]
|
|
spec = yaml.safe_load(open(os.path.join(ROOT, "hanzo.yaml")))
|
|
methods = ("get", "post", "put", "patch", "delete", "head", "options")
|
|
ids = {
|
|
op["operationId"]
|
|
for item in spec["paths"].values()
|
|
for verb, op in item.items()
|
|
if verb in methods and isinstance(op, dict) and op.get("operationId")
|
|
}
|
|
return flows, ids
|
|
|
|
|
|
def test_the_six_are_present_and_only_those():
|
|
flows, _ = load()
|
|
assert list(flows) == CANONICAL, f"flows.yaml lists {list(flows)}, expected {CANONICAL}"
|
|
|
|
|
|
def test_every_operation_exists_in_the_spec():
|
|
flows, ids = load()
|
|
missing = [
|
|
(name, op)
|
|
for name, flow in flows.items()
|
|
for op in flow["operations"]
|
|
if op not in ids
|
|
]
|
|
assert not missing, "flows.yaml names operations hanzo.yaml does not have: " + repr(missing)
|
|
|
|
|
|
def test_every_flow_says_what_it_does():
|
|
flows, _ = load()
|
|
for name, flow in flows.items():
|
|
assert flow.get("summary"), f"{name} has no summary"
|
|
assert flow.get("operations"), f"{name} names no operations"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
failures = 0
|
|
for name, fn in sorted(globals().items()):
|
|
if name.startswith("test_") and callable(fn):
|
|
try:
|
|
fn()
|
|
print(f"ok {name}")
|
|
except AssertionError as e:
|
|
failures += 1
|
|
print(f"FAIL {name}: {e}")
|
|
sys.exit(1 if failures else 0)
|