From 2571773879630b825875a219bb3b88f0b9dbc66a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 09:03:29 +0000 Subject: [PATCH] Fix import/console-tab bugs in a0 and add package run tests - subagents.py hard-imported the optional claude_agent_sdk at module level, so importing a0.adapters crashed whenever the SDK was absent even though ClaudeAgentAdapter guards the same import. Make the import degrade gracefully with a stand-in AgentDefinition. - transcripts is a standalone page, not a metadata-driven console tab; its page-nav UI_META (path, no tab_id/sections) was aggregated by collect_ui_meta(), producing an unrenderable placeholder tab. Drop it from the console tab collection. - Fix stale import in test_aggregate_round_trips_through_cache_breakdown (cache_breakdown is a module function, not an attribute of a removed energy_registry instance). - Remove dead test_energy_registry_persistence.py: it targets an EnergyRegistry class / set_active_provider_persistent API that no longer exists (provider selection moved to the conduct slot). - Add tests/test_a0_package.py: imports every a0/ submodule, asserts the subagent registry loads without the optional SDK, and runs the a0.a0 CLI end-to-end. https://claude.ai/code/session_018fWZmwRbdzw3WAofM8bARB --- a0/adapters/subagents.py | 21 ++++- python/routes/__init__.py | 9 +- tests/test_a0_package.py | 81 +++++++++++++++++ tests/test_energy_registry_persistence.py | 102 ---------------------- tests/test_inference_modes_usage.py | 2 +- 5 files changed, 106 insertions(+), 109 deletions(-) create mode 100644 tests/test_a0_package.py delete mode 100644 tests/test_energy_registry_persistence.py diff --git a/a0/adapters/subagents.py b/a0/adapters/subagents.py index 46130deb..9403ad55 100644 --- a/a0/adapters/subagents.py +++ b/a0/adapters/subagents.py @@ -1,4 +1,4 @@ -# 127:32 0:0 4:0 +# 136:37 0:0 4:0 """PTCA subagent definitions for the claude-agent-sdk. Each AgentDefinition maps to a PTCA architectural role. @@ -15,7 +15,22 @@ """ from __future__ import annotations -from claude_agent_sdk import AgentDefinition +try: + from claude_agent_sdk import AgentDefinition +except ImportError: + # claude_agent_sdk is an optional runtime dependency (see + # ClaudeAgentAdapter._SDK_AVAILABLE). When it is absent the adapter + # degrades gracefully, so these definitions must remain importable too. + # This stand-in carries the same fields the SDK's AgentDefinition does, + # keeping ALL_SUBAGENTS / MODE_SUBAGENTS populated for inspection. + from dataclasses import dataclass, field + + @dataclass + class AgentDefinition: # type: ignore[no-redef] + description: str = "" + prompt: str = "" + tools: list[str] = field(default_factory=list) + model: str = "" # --------------------------------------------------------------------------- # Private cognitive cores @@ -175,4 +190,4 @@ "route": ROUTE_SUBAGENTS, "act": ACT_SUBAGENTS, } -# 127:32 0:0 4:0 +# 136:37 0:0 4:0 diff --git a/python/routes/__init__.py b/python/routes/__init__.py index 631143cf..5963c670 100644 --- a/python/routes/__init__.py +++ b/python/routes/__init__.py @@ -1,4 +1,4 @@ -# 164:17 0:0 0:31 +# 163:21 0:0 0:31 from .chat import router as chat_router from .agents import router as agents_router from .memory import router as memory_router @@ -88,7 +88,10 @@ def collect_ui_meta() -> list[dict]: "python.routes.cli", "python.routes.liminals", "python.routes.artifacts", - "python.routes.transcripts", + # transcripts is a standalone page (routed at /transcripts via the top + # nav), not a metadata-driven console tab. Its UI_META is a page-nav + # descriptor (path, no tab_id/sections) and must not be aggregated here, + # or the console renders an unrenderable placeholder tab for it. "python.routes.fleet", ] tabs = [] @@ -195,4 +198,4 @@ def collect_doc_meta() -> list[dict]: # === END CONTRACTS === # 171:16 -# 164:17 0:0 0:31 +# 163:21 0:0 0:31 diff --git a/tests/test_a0_package.py b/tests/test_a0_package.py new file mode 100644 index 00000000..105e0c3e --- /dev/null +++ b/tests/test_a0_package.py @@ -0,0 +1,81 @@ +# 53:10 0:0 0:0 +# DOC module: tests.test_a0_package +# DOC label: a0 package import + CLI smoke +# DOC description: Imports every module under the a0/ package to catch +# import-time breaks (e.g. an unguarded optional dependency), verifies the +# subagent registry is populated without the optional claude_agent_sdk, and +# runs the a0.a0 CLI end-to-end through stdin/stdout. +import importlib +import json +import pkgutil +import subprocess +import sys + +import pytest + +import a0 + + +A0_MODULES = sorted( + m.name for m in pkgutil.walk_packages(a0.__path__, prefix="a0.") +) + + +def test_module_discovery_nonempty(): + # Guards against the walk silently finding nothing (which would make the + # parametrized import test below vacuously pass). + assert len(A0_MODULES) > 20, f"only discovered {len(A0_MODULES)} a0 modules" + + +@pytest.mark.parametrize("modname", A0_MODULES) +def test_a0_submodule_imports_clean(modname): + importlib.import_module(modname) + + +def test_subagent_registry_populated_without_sdk(): + # claude_agent_sdk is an optional dependency; the definitions must still + # load and carry their data when it is absent. + from a0.adapters import ALL_SUBAGENTS, MODE_SUBAGENTS + + assert set(ALL_SUBAGENTS) == {"phi", "psi", "omega", "jury", "bandit"} + for mode in ("analyze", "route", "act"): + assert mode in MODE_SUBAGENTS + assert len(MODE_SUBAGENTS[mode]) > 0 + bandit = ALL_SUBAGENTS["bandit"] + assert bandit.tools and bandit.model + + +def test_handle_round_trips_in_process(): + from a0.contract import A0Request, normalize_hmmm + from a0.router import handle + + req = A0Request( + task_id="unit-1", + input={"text": "hello", "files": [], "metadata": {}}, + tools_allowed=["none"], + mode="analyze", + hmmm=normalize_hmmm(["hmm"]), + ) + resp = handle(req) + assert resp.task_id == "unit-1" + assert resp.result is not None + + +def test_a0_cli_smoke(): + payload = { + "task_id": "smoke1", + "input": {"text": "hello a0", "files": [], "metadata": {}}, + "tools_allowed": ["none"], + "mode": "analyze", + "hmmm": ["hmm"], + } + proc = subprocess.run( + [sys.executable, "-m", "a0.a0"], + input=json.dumps(payload).encode("utf-8"), + stdout=subprocess.PIPE, + check=True, + ) + out = json.loads(proc.stdout.decode("utf-8")) + assert out["task_id"] == "smoke1" + assert "result" in out +# 53:10 0:0 0:0 diff --git a/tests/test_energy_registry_persistence.py b/tests/test_energy_registry_persistence.py deleted file mode 100644 index 9ae38415..00000000 --- a/tests/test_energy_registry_persistence.py +++ /dev/null @@ -1,102 +0,0 @@ -# 65:7 0:0 0:0 -# DOC module: tests.test_energy_registry_persistence -# DOC label: Provider persistence path -# DOC description: Exercises success/failure behavior of -# set_active_provider_persistent with controlled async session doubles. -import importlib -import sys -import types - -import pytest - - -def _load_energy_registry_with_sqlalchemy_stub(monkeypatch): - """Import python.services.energy_registry even when sqlalchemy is absent. - - We stub only what this module needs: sqlalchemy.text. - """ - sa = types.SimpleNamespace(text=lambda s: s) - monkeypatch.setitem(sys.modules, "sqlalchemy", sa) - mod = importlib.import_module("python.services.energy_registry") - return importlib.reload(mod) - - -class _SessionOK: - def __init__(self): - self.executed = [] - self.committed = False - - async def execute(self, stmt, params): - self.executed.append((stmt, params)) - - async def commit(self): - self.committed = True - - -class _AsyncCtx: - def __init__(self, session): - self.session = session - - async def __aenter__(self): - return self.session - - async def __aexit__(self, exc_type, exc, tb): - return False - - -def test_persistent_switch_writes_and_commits_async(monkeypatch): - er_mod = _load_energy_registry_with_sqlalchemy_stub(monkeypatch) - - session = _SessionOK() - fake_db = types.SimpleNamespace(get_session=lambda: _AsyncCtx(session)) - monkeypatch.setitem(sys.modules, "python.database", fake_db) - - registry = er_mod.EnergyRegistry() - provider_id = registry.list_providers()[0]["id"] - - import asyncio - ok = asyncio.run(registry.set_active_provider_persistent(provider_id)) - assert ok is True - assert registry.get_active_provider() == provider_id - assert session.committed is True - assert len(session.executed) == 1 - stmt, params = session.executed[0] - assert "INSERT INTO a0p_settings" in stmt - assert params == {"pid": provider_id} - - -def test_persistent_switch_db_failure_is_non_fatal(monkeypatch): - er_mod = _load_energy_registry_with_sqlalchemy_stub(monkeypatch) - - class _SessionFails(_SessionOK): - async def execute(self, stmt, params): - raise RuntimeError("db down") - - session = _SessionFails() - fake_db = types.SimpleNamespace(get_session=lambda: _AsyncCtx(session)) - monkeypatch.setitem(sys.modules, "python.database", fake_db) - - registry = er_mod.EnergyRegistry() - provider_id = registry.list_providers()[0]["id"] - - import asyncio - ok = asyncio.run(registry.set_active_provider_persistent(provider_id)) - assert ok is True, "in-memory switch should still succeed" - assert registry.get_active_provider() == provider_id - assert session.committed is False - - -def test_persistent_switch_rejects_unknown_provider(monkeypatch): - er_mod = _load_energy_registry_with_sqlalchemy_stub(monkeypatch) - - fake_db = types.SimpleNamespace(get_session=lambda: _AsyncCtx(_SessionOK())) - monkeypatch.setitem(sys.modules, "python.database", fake_db) - - registry = er_mod.EnergyRegistry() - current = registry.get_active_provider() - - import asyncio - ok = asyncio.run(registry.set_active_provider_persistent("definitely-not-a-provider")) - assert ok is False - assert registry.get_active_provider() == current -# 65:7 0:0 0:0 diff --git a/tests/test_inference_modes_usage.py b/tests/test_inference_modes_usage.py index f5750290..d425d70a 100644 --- a/tests/test_inference_modes_usage.py +++ b/tests/test_inference_modes_usage.py @@ -171,7 +171,7 @@ def test_aggregate_round_trips_through_cache_breakdown(): energy_registry.cache_breakdown — which the chat route calls on the final usage — reads back the same numbers without subtracting cache_read twice.""" - from python.services.energy_registry import energy_registry + from python.services import energy_registry serialized = [ {"model": "claude", "content": "x", "error": None, "usage": {"input_tokens": 200, "output_tokens": 50,