"""Tests for the extracted prompts package. The render() functions are pure (they take docs as a parameter, no module globals), so they're trivially unit-testable. We verify each one renders without raising both with and without a docs index. """ import json from pathlib import Path import pytest from mcaxl import prompts from mcaxl.docs_loader import DocsIndex @pytest.fixture def fake_docs(tmp_path: Path) -> DocsIndex: chunks = [ { "id": "cucm::v15::admin::Route-Plan::0", "text": "Route plan defines call routing.", "heading_path": ["Route Plan"], "source_path": str(tmp_path / "fake.md"), "product": "cucm", "version": "v15", "doc": "system-config-guide", }, { "id": "cucm::v15::admin::SIP-Trunk::0", "text": "SIP trunks connect CUCM to other call control entities.", "heading_path": ["SIP Trunk Configuration"], "source_path": str(tmp_path / "fake.md"), "product": "cucm", "version": "v15", "doc": "system-config-guide", }, ] (tmp_path / "chunks.jsonl").write_text( "\n".join(json.dumps(c) for c in chunks) ) (tmp_path / "index_meta.json").write_text( json.dumps({"model_name": "test", "embedding_dim": 384, "products": ["cucm"]}) ) idx = DocsIndex.load(tmp_path) assert idx is not None return idx # ---- route_plan_overview ---------------------------------------------------- def test_route_plan_overview_renders_with_docs(fake_docs): text = prompts.route_plan_overview.render(fake_docs) assert "Route Plan Overview" in text assert "axl_version" in text assert "route_partitions" in text def test_route_plan_overview_renders_without_docs(): # Graceful degradation — no docs index should still produce a usable prompt text = prompts.route_plan_overview.render(None) assert "Route Plan Overview" in text assert "cisco-docs index is not loaded" in text # ---- investigate_pattern --------------------------------------------------- def test_investigate_pattern_includes_pattern_in_output(fake_docs): text = prompts.investigate_pattern.render(fake_docs, "10.911", "CER911-PT") assert "10.911" in text assert "CER911-PT" in text def test_investigate_pattern_no_partition(fake_docs): text = prompts.investigate_pattern.render(fake_docs, "9.@") assert "9.@" in text # The partition clause should not appear when no partition is given assert "in partition" not in text # ---- audit_routing ---------------------------------------------------------- def test_audit_routing_default_focus(fake_docs): text = prompts.audit_routing.render(fake_docs) assert "focus: `full`" in text def test_audit_routing_custom_focus(fake_docs): text = prompts.audit_routing.render(fake_docs, "translations") assert "focus: `translations`" in text # ---- cucm_sql_help ---------------------------------------------------------- def test_cucm_sql_help_includes_question(fake_docs): q = "How do I find phones with no associated user?" text = prompts.cucm_sql_help.render(fake_docs, q) assert q in text assert "axl_describe_table" in text def test_cucm_sql_help_handles_empty_question(fake_docs): # No substantive keywords — must still render something useful text = prompts.cucm_sql_help.render(fake_docs, "x y z") assert "axl_list_tables" in text # ---- sip_trunk_report (NEW) ------------------------------------------------ def test_sip_trunk_report_includes_query_1(fake_docs): text = prompts.sip_trunk_report.render(fake_docs) # Query 1 essentials: device JOIN sipdevice, the trunk_name column, # and the WHERE filter on Trunk class assert "FROM device d" in text assert "JOIN sipdevice sd" in text assert "tc.name = 'Trunk'" in text def test_sip_trunk_report_includes_query_2_and_tools(fake_docs): text = prompts.sip_trunk_report.render(fake_docs) assert "siptrunkdestination" in text # And references the existing MCP tool for route group traversal assert "route_lists_and_groups()" in text def test_sip_trunk_report_with_name_filter_injects_safe_like(fake_docs): text = prompts.sip_trunk_report.render(fake_docs, "PSTN") # The filter should appear as a LIKE clause and the scope note should # reflect the narrowing assert "%PSTN%" in text assert "narrowed to trunks" in text def test_sip_trunk_report_name_filter_escaped(fake_docs): # SQL injection defense: a single-quote in the name_filter must be # doubled before going into the LIKE pattern text = prompts.sip_trunk_report.render(fake_docs, "O'Reilly") assert "O''Reilly" in text def test_sip_trunk_report_renders_without_docs(): text = prompts.sip_trunk_report.render(None) assert "SIP Trunk Report" in text assert "cisco-docs index is not loaded" in text # ---- registration smoke test ----------------------------------------------- def test_all_prompts_registered_in_server(): """Confirm each prompt module's render() is wired through a FastMCP shim in server.py. Catches the case where a new module is added but the shim wasn't (the prompt would be invisible to the LLM).""" import asyncio from mcaxl import server async def _list(): registered = await server.mcp.list_prompts() return {p.name for p in registered} names = asyncio.run(_list()) assert names == { "route_plan_overview", "investigate_pattern", "audit_routing", "cucm_sql_help", "sip_trunk_report", "phone_inventory_report", "user_audit", "inbound_did_audit", "hunt_pilot_audit", "whoami", "did_block_overlap", "partition_summary", "dead_dn_finder", }, f"unexpected prompt set: {names}" # ---- new prompts: render-with-and-without-docs smoke tests ----------------- def test_phone_inventory_report_renders(fake_docs): text = prompts.phone_inventory_report.render(fake_docs) assert "Phone Inventory" in text # Embedded SQL should query the `device` table with tkclass='Phone' assert "tc.name = 'Phone'" in text assert "all phones" in text def test_phone_inventory_report_with_filter(fake_docs): text = prompts.phone_inventory_report.render(fake_docs, "Lobby") assert "%Lobby%" in text assert "narrowed" in text def test_phone_inventory_report_filter_escaped(fake_docs): text = prompts.phone_inventory_report.render(fake_docs, "O'Hara") assert "O''Hara" in text # SQL injection defense def test_user_audit_default_focus(fake_docs): text = prompts.user_audit.render(fake_docs) assert "focus: `full`" in text assert "applicationuser" in text assert "enduser" in text def test_user_audit_admin_focus(fake_docs): text = prompts.user_audit.render(fake_docs, "admin") assert "focus: `admin`" in text def test_user_audit_unknown_focus_falls_back_to_full(fake_docs): text = prompts.user_audit.render(fake_docs, "bogus_value") assert "focus: `full`" in text def test_inbound_did_audit_renders(fake_docs): text = prompts.inbound_did_audit.render(fake_docs) assert "Inbound DID Audit" in text assert "XFORM-Inbound-DNIS" in text assert "PSTN-Screen-PT" in text def test_hunt_pilot_audit_uses_correct_column(fake_docs): """Hamilton bonus finding: huntpilotqueue joins via fknumplan_pilot, NOT fknumplan. The prompt's embedded SQL must use the correct column or every audit run will silently fail one category.""" text = prompts.hunt_pilot_audit.render(fake_docs) assert "fknumplan_pilot" in text, ( "hunt_pilot_audit must use the verified column name `fknumplan_pilot`; " "`fknumplan` was the silent-failure version we fixed in Hamilton review" ) assert "tkpatternusage = 7" in text # Hunt Pilot type code def test_all_new_prompts_render_without_docs(): """Graceful degradation: each prompt produces usable output even when the docs index isn't loaded.""" for name, fn, args in [ ("phone_inventory_report", prompts.phone_inventory_report.render, ()), ("user_audit", prompts.user_audit.render, ()), ("inbound_did_audit", prompts.inbound_did_audit.render, ()), ("hunt_pilot_audit", prompts.hunt_pilot_audit.render, ()), ("whoami", prompts.whoami.render, ()), ]: text = fn(None, *args) assert "cisco-docs index is not loaded" in text, ( f"{name} failed graceful degradation" ) # ---- whoami specifics ------------------------------------------------------ def test_whoami_uses_correct_table_names_for_cucm_15(fake_docs): """The query principle came in with `enduserauthgroupmap` and `dirgrouprolemap`, but those tables don't exist on CUCM 15. The prompt MUST embed the verified names: `enduserdirgroupmap` and `functionroledirgroupmap`. If a future contributor reverts to the older names, this test fires red.""" text = prompts.whoami.render(fake_docs, "TestUser") assert "enduserdirgroupmap" in text assert "functionroledirgroupmap" in text # And the older deprecated names should NOT appear in the SQL we ship # (they're fine in the prose section that explains why the names changed) sql_section = text.split("## Schema knowledge")[0] + text.split("## Step")[1] for old_name in ("enduserauthgroupmap", "dirgrouprolemap"): assert old_name not in sql_section, ( f"deprecated table name {old_name} appeared in the SQL section" ) def test_whoami_explicit_userid_in_query(fake_docs): text = prompts.whoami.render(fake_docs, "SomeAccount") assert "SomeAccount" in text assert "explicit userid" in text def test_whoami_default_uses_axl_user_env(fake_docs, monkeypatch): monkeypatch.setenv("AXL_USER", "EnvAccount") text = prompts.whoami.render(fake_docs) assert "EnvAccount" in text assert "AXL service account" in text def test_whoami_no_userid_no_env(fake_docs, monkeypatch): monkeypatch.delenv("AXL_USER", raising=False) text = prompts.whoami.render(fake_docs) assert "no userid supplied" in text # Tells the LLM what to do — set the param or the env var assert "set userid parameter" in text or "AXL_USER env var" in text def test_whoami_userid_escaped_for_sql_safety(fake_docs): text = prompts.whoami.render(fake_docs, "O'Brien") assert "O''Brien" in text # single quote doubled per Informix convention