"""Tests for the built-in /plan command (formerly the bundled `plan` skill). Covers the shared prompt builder (agent.plan_prompt.build_plan_prompt) and the registry wiring that makes /plan a first-class command on every surface — CLI, gateway messengers, and TUI. The skill-to-builtin move exists precisely so /plan survives the Telegram/Discord command-menu caps that trimmed it as an alphabetical skill entry. """ from agent.plan_prompt import build_plan_prompt class TestBuildPlanPrompt: def test_task_is_included_verbatim(self): task = "migrate the auth provider to OIDC with zero downtime" prompt = build_plan_prompt(task) assert task in prompt def test_empty_task_infers_from_conversation(self): prompt = build_plan_prompt("") assert "infer the task from the current conversation context" in prompt # Whitespace-only behaves the same. assert "infer the task" in build_plan_prompt(" ") def test_plan_mode_ground_rules_always_present(self): for arg in ("", "build a REST API"): prompt = build_plan_prompt(arg) assert "PLAN MODE" in prompt assert "Do not implement code" in prompt assert "Do not run mutating terminal commands" in prompt assert "read-only" in prompt def test_save_location_contract(self): prompt = build_plan_prompt("anything") assert ".hermes/plans/" in prompt assert "YYYY-MM-DD_HHMMSS-.md" in prompt def test_authoring_craft_travels_with_every_prompt(self): prompt = build_plan_prompt("x") assert "bite-sized" in prompt assert "exact file paths" in prompt.lower() assert "TDD" in prompt assert "YAGNI" in prompt def test_no_execution_handoff_in_same_turn(self): prompt = build_plan_prompt("x") assert "do not start executing in this turn" in prompt class TestPlanRegistryWiring: def test_plan_is_registered_and_resolves(self): from hermes_cli.commands import resolve_command cmd = resolve_command("plan") assert cmd is not None assert cmd.name == "plan" def test_plan_is_not_cli_only(self): # /plan must reach messaging gateways — the whole point of the # builtin conversion is menu visibility on Telegram/Discord. from hermes_cli.commands import resolve_command assert not resolve_command("plan").cli_only def test_plan_reaches_gateway_dispatch(self): from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS assert "plan" in GATEWAY_KNOWN_COMMANDS def test_plan_in_telegram_bot_commands(self): from hermes_cli.commands import telegram_bot_commands names = {n for n, _ in telegram_bot_commands()} assert "plan" in names def test_no_bundled_plan_skill_remains(self): # The bundled skill was removed with the builtin conversion; a # leftover copy would collide with the core command at scan time # (scan_skill_commands skips core-colliding skill slugs with a # warning, so the skill would be silently unreachable). from pathlib import Path repo_root = Path(__file__).resolve().parents[2] assert not (repo_root / "skills" / "software-development" / "plan").exists()