diff --git a/apps/shared/src/gateway-contract.generated.ts b/apps/shared/src/gateway-contract.generated.ts index 3174d29d86..84e78e4952 100644 --- a/apps/shared/src/gateway-contract.generated.ts +++ b/apps/shared/src/gateway-contract.generated.ts @@ -1,6 +1,6 @@ // GENERATED by scripts/gen_gateway_contracts.py from tui_gateway/contracts — DO NOT EDIT. // Regenerate: .venv/bin/python scripts/gen_gateway_contracts.py -// tests/contracts/test_generated.py fails when this file is stale. +// tests/tui_gateway/contracts/test_generated.py fails when this file is stale. /* eslint-disable */ // ── Types ── /** Any method the desktop may route to a named profile (``requestGatewayForProfile`` adds ``profile``). */ diff --git a/apps/shared/src/gateway-events.ts b/apps/shared/src/gateway-events.ts index ee2c031e35..f4cab92169 100644 --- a/apps/shared/src/gateway-events.ts +++ b/apps/shared/src/gateway-events.ts @@ -6,7 +6,7 @@ * `./gateway-contract.generated.ts` carries `RpcMethods` (client→server method → params/result), * `ServerRequestMap` (server→client request → params/result), `GatewayEventMap` (notification * type → payload) and every value shape. `scripts/gen_gateway_contracts.py` regenerates it and - * `tests/contracts/test_generated.py` fails when the committed file is stale, so a field the + * `tests/tui_gateway/contracts/test_generated.py` fails when the committed file is stale, so a field the * backend stops sending fails `tsc` here instead of drifting. * * This module adds only what the wire does not carry: the client-local synthetic events the TUI diff --git a/scripts/ci/classify_changes.py b/scripts/ci/classify_changes.py index 2e3534bd02..81e158ef35 100644 --- a/scripts/ci/classify_changes.py +++ b/scripts/ci/classify_changes.py @@ -96,7 +96,7 @@ _PY_RELEVANT_SITE = ( # Editing only the JSON in an apps/-only PR would otherwise skip the one test # that can catch the drift, so these force the Python lane too. _PY_RELEVANT_CONTRACT_FILES = { - # tests/contracts/test_generated.py (rendered from tui_gateway/contracts) + # tests/tui_gateway/contracts/test_generated.py (rendered from tui_gateway/contracts) "apps/shared/src/gateway-contract.generated.ts", "apps/shared/src/gateway-contract.openrpc.json", # tests/hermes_cli/test_desktop_slash_registry.py diff --git a/scripts/gen_gateway_contracts.py b/scripts/gen_gateway_contracts.py index 2c71c72bd8..476660cb66 100644 --- a/scripts/gen_gateway_contracts.py +++ b/scripts/gen_gateway_contracts.py @@ -34,7 +34,7 @@ OPENRPC_OUT = ROOT / "apps" / "shared" / "src" / "gateway-contract.openrpc.json" HEADER = ( "// GENERATED by scripts/gen_gateway_contracts.py from tui_gateway/contracts — DO NOT EDIT.\n" "// Regenerate: .venv/bin/python scripts/gen_gateway_contracts.py\n" - "// tests/contracts/test_generated.py fails when this file is stale.\n" + "// tests/tui_gateway/contracts/test_generated.py fails when this file is stale.\n" ) diff --git a/tests/tools/test_subagent_steer.py b/tests/tools/test_subagent_steer.py index d5418e9337..f06c49f44c 100644 --- a/tests/tools/test_subagent_steer.py +++ b/tests/tools/test_subagent_steer.py @@ -715,8 +715,11 @@ class TestSubagentSteerRPC: transport=owner_transport, session_record=owner_record, ) - assert envelope["result"]["status"] == "queued" - assert agent.steered == ["ignore serialized capabilities"] + # The wire contract refuses unknown keys outright, so a forged runtime artifact never + # reaches the handler (before contracts: silently ignored, steer still queued). + assert envelope["error"]["code"] == 4000 + assert "owner_transport" in envelope["error"]["message"] + assert agent.steered == [] finally: _unregister_subagent("sid-rpc-param-spoof") diff --git a/tests/contracts/__init__.py b/tests/tui_gateway/contracts/__init__.py similarity index 100% rename from tests/contracts/__init__.py rename to tests/tui_gateway/contracts/__init__.py diff --git a/tests/contracts/test_generated.py b/tests/tui_gateway/contracts/test_generated.py similarity index 98% rename from tests/contracts/test_generated.py rename to tests/tui_gateway/contracts/test_generated.py index a67d22ac20..11a8638a3b 100644 --- a/tests/contracts/test_generated.py +++ b/tests/tui_gateway/contracts/test_generated.py @@ -14,7 +14,7 @@ from pathlib import Path import pytest -REPO = Path(__file__).resolve().parents[2] +REPO = Path(__file__).resolve().parents[3] GEN = REPO / "scripts" / "gen_gateway_contracts.py" diff --git a/tui_gateway/AGENTS.md b/tui_gateway/AGENTS.md index 1a0bf58cd0..783f59df27 100644 --- a/tui_gateway/AGENTS.md +++ b/tui_gateway/AGENTS.md @@ -38,7 +38,7 @@ has a `Params` + `Result` model, every server→client request a `Params` + `Res result or an emitted payload does not match its model (production only logs). `apps/shared/src/ gateway-contract.generated.ts` (`RpcMethods`, `ServerRequestMap`, `BackendGatewayEventMap` + every value shape) and `gateway-contract.openrpc.json` are rendered by `scripts/gen_gateway_contracts.py`; -`tests/contracts/test_generated.py` fails when they are stale, so the loop is: change the model → +`tests/tui_gateway/contracts/test_generated.py` fails when they are stale, so the loop is: change the model → regenerate → `tsc` shows every consumer the field moved. `apps/shared/src/gateway-events.ts` only adds the client-local synthetic events and the `GatewayEvent` envelope on top. New question for the user = `_ask("", sid, params, timeout)` in the emitter, a handler in diff --git a/tui_gateway/contracts/base.py b/tui_gateway/contracts/base.py index 4c4eedf76b..f88c6021e5 100644 --- a/tui_gateway/contracts/base.py +++ b/tui_gateway/contracts/base.py @@ -4,7 +4,7 @@ Python is the single source of truth for the JSON-RPC wire: every client→serve (params + result), every server→client request (params + result) and every notification payload is a Pydantic model declared in this package. ``scripts/gen_gateway_contracts.py`` renders them into ``apps/shared/src/gateway-contract.generated.ts`` and -``apps/shared/src/gateway-contract.openrpc.json``; ``tests/contracts/test_generated.py`` +``apps/shared/src/gateway-contract.openrpc.json``; ``tests/tui_gateway/contracts/test_generated.py`` regenerates in memory and diffs the committed files, so a model edited without regenerating fails CI on the Python side, and TS that reads a phantom field fails ``tsc``. diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 25908dd1ce..019463899f 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -769,7 +769,7 @@ def _err(rid, code: int, msg: str, data=None) -> dict: def register_method(name: str, fn) -> None: """The ONE registration seam (``@method`` here and ``HandlerRegistry.install`` for the split - modules). ``tests/contracts/test_generated.py::test_every_method_has_a_contract`` and the + modules). ``tests/tui_gateway/contracts/test_generated.py::test_every_method_has_a_contract`` and the generator's ``assert_complete`` fail when a registered name has no contract.""" _methods[name] = fn