Documents fail-closed behavior when the shell executor fails to spawn (exit_code: -1, empty stdout/stderr): probe once, mark shell unavailable, hard-stop after two consecutive spawn failures, and emit a recovery report (restart session, kill hung background terminals, prefer MCP-native paths) instead of retry-spiralling. - skills/llm-project-workflow/SKILL.md: portable rule section - docs/llm-workflow-runbooks.md: Gitea runbook section - gitea_mcp_server.py: shell_spawn_hard_stop operator guide rule - tests/test_shell_spawn_hard_stop_docs.py: doc-contract tests (5) - tests/test_operator_guide.py: require the new guide key Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
83 lines
2.4 KiB
Python
83 lines
2.4 KiB
Python
"""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}"
|