From 879ef3053e3194c6e157893cd31b006dba8e9427 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 26 Aug 2026 12:34:29 -0400 Subject: [PATCH] fix(systems): the bootstrap ask reads the catalog, so its test must supply one (#3027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught the seam the promotion opened: the standard names now come from an async catalog read, and the unit test patches systems_svc wholesale — so the read raised, the fail-open swallowed it, and the ask shipped without the names it is supposed to carry. Stub standard_systems in that test, and assert the wiring rather than the vocabulary: THAT the seeded set is these eight is migration 0087's business and belongs in the inception integration test, against a real database. Adds the case the promotion actually created — an unreachable or empty catalog must still produce the ask. The names are an aid to the question, not the question; degrading to a weaker nudge is fine, going silent is not. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_mcp_tool_systems.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_mcp_tool_systems.py b/tests/test_mcp_tool_systems.py index 45e3a32..97f26e2 100644 --- a/tests/test_mcp_tool_systems.py +++ b/tests/test_mcp_tool_systems.py @@ -113,6 +113,15 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project(): patch("scribe.mcp.tools.systems.notes_svc") as notes: svc.list_systems = AsyncMock(return_value=[]) notes.list_notes = AsyncMock(return_value=(recent, 282)) + # The standard names come from the GLOBAL catalog now (milestone 307), + # not a module constant — so the ask reads them through the service. + # That the SEEDED vocabulary is these eight is migration 0087's + # business, asserted against a real database in the inception + # integration test; what belongs here is that whatever the catalog + # holds reaches the ask verbatim. + svc.standard_systems = AsyncMock(return_value=[ + ("CI & Release", "..."), ("Auth & Access", "..."), + ]) hint = await untagged_systems_hint(1, 5) assert "282 records" in hint assert "Fix scrape retry backoff" in hint # the project's own evidence @@ -126,6 +135,25 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project(): assert "no Systems yet" not in hint +@pytest.mark.asyncio +async def test_bootstrap_ask_still_asks_when_the_catalog_is_unreachable(): + """The standard names are an AID to the question, not the question. If the + catalog read fails (or an install has an empty one), the ask must still + carry the project's evidence and demand the same deliverable — degrading to + a weaker nudge is acceptable, going silent is not.""" + from scribe.mcp.tools.systems import untagged_systems_hint + recent = [fake_note(title="Fix scrape retry backoff")] + with patch("scribe.mcp.tools.systems.systems_svc") as svc, \ + patch("scribe.mcp.tools.systems.notes_svc") as notes: + svc.list_systems = AsyncMock(return_value=[]) + notes.list_notes = AsyncMock(return_value=(recent, 282)) + svc.standard_systems = AsyncMock(side_effect=RuntimeError("db down")) + hint = await untagged_systems_hint(1, 5) + assert hint is not None + assert "282 records" in hint and "create_system" in hint + assert "3-6" in hint and "without asking permission" in hint + + @pytest.mark.asyncio async def test_bootstrap_ask_stays_quiet_below_threshold_and_fails_open(): from scribe.mcp.tools import systems as tools