"""Doc-contract checks for the shell-spawn hard-stop rule (#258). When the shell executor fails to spawn (exit_code: -1 with empty stdout/stderr), agents must probe once, mark shell unavailable, hard-stop after two consecutive spawn failures, and emit a recovery report instead of retry-spiralling. These tests pin that guidance in the runbooks, the portable skill doc, and the operator guide so it cannot silently regress. """ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent RUNBOOK = REPO_ROOT / "docs" / "llm-workflow-runbooks.md" SKILL = REPO_ROOT / "skills" / "llm-project-workflow" / "SKILL.md" def _normalize(text: str) -> str: """Collapse whitespace so phrase checks survive markdown line wrapping.""" return " ".join(text.split()) def _runbook_text() -> str: return _normalize(RUNBOOK.read_text(encoding="utf-8")) def _skill_text() -> str: return _normalize(SKILL.read_text(encoding="utf-8")) def test_runbook_has_shell_spawn_hard_stop_section(): text = _runbook_text() assert "## Shell Spawn Hard-Stop Rule" in text for phrase in ( "exit_code: -1", "empty stdout/stderr", "trivial probe", "two consecutive spawn failures", "mark shell unavailable", "recovery report", ): assert phrase in text, f"runbook missing phrase: {phrase!r}" def test_runbook_recovery_report_names_required_steps(): text = _runbook_text() for phrase in ( "restart the session", "kill hung background terminals", "MCP-native", ): assert phrase in text, f"runbook recovery guidance missing: {phrase!r}" def test_runbook_forbids_retry_spirals(): text = _runbook_text() assert "never retry the same failing spawn" in text def test_skill_doc_declares_shell_spawn_hard_stop_rule(): text = _skill_text() assert "## Shell Spawn Hard-Stop Rule" in text for phrase in ( "exit_code: -1", "two consecutive spawn failures", "recovery report", ): assert phrase in text, f"SKILL.md missing phrase: {phrase!r}" def test_operator_guide_rules_include_shell_spawn_hard_stop(): import sys sys.path.insert(0, str(REPO_ROOT)) import gitea_mcp_server rule = gitea_mcp_server._GUIDE_RULES["shell_spawn_hard_stop"] for phrase in ( "exit_code: -1", "two consecutive", "recovery report", ): assert phrase in rule, f"operator guide rule missing: {phrase!r}"