Files
Agent-System/scripts/tests/test_agentctl_w4.py
T
WeiCHEN 59739a2b48 feat(agentctl): harden CURRENT projection and human-section guards
W2-U5: own_row_only/slot_protected policy wiring, normalized projection
rows, idempotent request ids, five-state status vocab, expanded protected
human sections, projections ledger readout, and doc contradiction fixes.
2026-09-23 17:00:44 +08:00

214 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""Black-box tests for agentctl_w4 projection apply and deploy/recover loop."""
from __future__ import annotations
import importlib.util
import json
import shutil
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1]
W4_SOURCE = SOURCE / "agentctl_w4.py"
def load_w4(path: Path):
spec = importlib.util.spec_from_file_location("agentctl_w4_under_test", path)
mod = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(mod)
return mod
class AgentctlW4Tests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix="agentctl-w4-")
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name) / "repo"
self.root.mkdir()
(self.root / "scripts").mkdir()
shutil.copy2(W4_SOURCE, self.root / "scripts" / "agentctl_w4.py")
(self.root / "configs").mkdir(parents=True, exist_ok=True)
shutil.copy2(SOURCE.parent / "configs" / "agent-governance.json",
self.root / "configs" / "agent-governance.json")
self.w4 = load_w4(self.root / "scripts" / "agentctl_w4.py")
self.db_path = Path(self.tmp.name) / "w4.sqlite"
self.db = sqlite3.connect(self.db_path)
self.db.row_factory = sqlite3.Row
self.addCleanup(self.db.close)
self.w4.ensure_schema(self.db)
def _write_current(self, body: str) -> Path:
path = self.root / ".doc" / "ops" / "CURRENT.md"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
return path
def test_row_projection_cas_and_insert(self):
doc = (
"# Coordination\n\n"
"| Task | Owner | Branch | State |\n"
"| --- | --- | --- | --- |\n"
"| alpha | Alice | feature/alpha | active |\n"
)
target = self._write_current(doc)
current_row = "| alpha | Alice | feature/alpha | active |"
replacement = "| alpha | Alice | feature/alpha | done |"
change = self.w4.build_row_projection_change(
file=".doc/ops/CURRENT.md",
record_id="alpha",
current_row=current_row,
replacement=replacement,
)
result = self.w4.apply_projection_locally(self.root, change)
self.assertEqual(result["status"], "applied")
self.assertIn(replacement, target.read_text(encoding="utf-8"))
self.assertNotIn(current_row, target.read_text(encoding="utf-8"))
insert_row = "| gamma | Carol | feature/gamma | active |"
insert = self.w4.build_row_projection_change(
file=".doc/ops/CURRENT.md",
record_id="gamma",
current_row=None,
replacement=insert_row,
)
self.assertIsNone(insert["expected_sha256"])
inserted = self.w4.apply_projection_locally(self.root, insert)
self.assertEqual(inserted["status"], "applied")
text = target.read_text(encoding="utf-8")
self.assertIn(insert_row, text)
self.assertIn(replacement, text)
def test_document_projection_cas_mismatch_refuses(self):
original = "# Features\n\nplanned\n"
path = self.root / ".doc" / "status" / "FEATURE-MATRIX.md"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(original, encoding="utf-8")
change = self.w4.build_document_projection_change(
file=".doc/status/FEATURE-MATRIX.md",
current_text=original,
replacement="# Features\n\ndone\n",
)
# Mutate on disk so live hash no longer matches the CAS tip.
path.write_text("# Features\n\nchanged-underneath\n", encoding="utf-8")
with self.assertRaises(RuntimeError) as ctx:
self.w4.apply_projection_locally(self.root, change)
self.assertIn("BASE_CHANGED", str(ctx.exception))
self.assertEqual(path.read_text(encoding="utf-8"), "# Features\n\nchanged-underneath\n")
def test_deploy_recover_loop_success_with_fake_command(self):
outcome = self.w4.execute_deploy_recover_loop(
self.db,
deploy_id="dep-ok",
task="modular-decouple-residual-2026-09-23",
env="dev",
expected_commit="abcdef1",
command=["/bin/echo", "ok"],
)
self.assertEqual(outcome["status"], "passed")
self.assertEqual(outcome["deploy"]["phase"], "passed")
self.assertTrue(outcome["receipt"].get("ok"))
self.assertTrue(outcome["receipt"].get("accepted"))
stored = self.w4.deployment_get(self.db, "dep-ok")
self.assertEqual(stored["phase"], "passed")
receipt = json.loads(stored["receipt"])
self.assertTrue(receipt.get("accepted"))
def test_deploy_recover_loop_failure_sets_recovery_required(self):
outcome = self.w4.execute_deploy_recover_loop(
self.db,
deploy_id="dep-fail",
task="modular-decouple-residual-2026-09-23",
env="dev",
expected_commit="abcdef2",
command=["/bin/false"],
)
self.assertEqual(outcome["status"], "recovery_required")
self.assertEqual(outcome["deploy"]["phase"], "recovery_required")
self.assertFalse(outcome["receipt"].get("ok"))
stored = self.w4.deployment_get(self.db, "dep-fail")
self.assertEqual(stored["phase"], "recovery_required")
pending = self.w4.queue_list(self.db, task="modular-decouple-residual-2026-09-23",
state="pending")
self.assertTrue(any(j["id"] == "dep-fail-recover-pending" for j in pending))
def test_recover_deploy_from_recovery_required(self):
self.w4.execute_deploy_recover_loop(
self.db,
deploy_id="dep-recover",
task="modular-decouple-residual-2026-09-23",
env="dev",
expected_commit="abcdef3",
command=[sys.executable, "-c", "raise SystemExit(1)"],
)
recovered = self.w4.recover_deploy(
self.db, "dep-recover", reason="operator cleared after fake failure")
self.assertEqual(recovered["phase"], "passed")
self.assertIsNone(recovered["last_error"])
receipt = json.loads(recovered["receipt"])
self.assertTrue(receipt.get("recovered"))
self.assertEqual(receipt.get("recover_reason"),
"operator cleared after fake failure")
def test_row_projection_ambiguous_record_refuses(self):
doc = (
"# Coordination\n\n"
"| owner | task | branch | state |\n"
"| --- | --- | --- | --- |\n"
"| Alice | alpha | feature/alpha | active |\n"
"| Bob | alpha | feature/alpha-2 | active |\n"
)
self._write_current(doc)
change = self.w4.build_row_projection_change(
file=".doc/ops/CURRENT.md",
record_id="alpha",
current_row="| Alice | alpha | feature/alpha | active |",
replacement="| Alice | alpha | feature/alpha | done |",
)
with self.assertRaises(RuntimeError) as ctx:
self.w4.apply_projection_locally(self.root, change)
self.assertIn("AMBIGUOUS_RECORD", str(ctx.exception))
def test_project_cleanup_markdown_header_is_report_only(self):
rendered = self.w4.project_cleanup_markdown([{
"path": ".worktrees/example",
"reason": "cleanup=pending",
"cleanup_owner": "maintainer",
"trigger": "tip ancestor",
}])
self.assertIn("| 路径/事项 | 分类与保留原因 | cleanup_owner | 复查触发 |", rendered)
self.assertNotIn("--kind cleanup", rendered)
self.assertIn(".worktrees/example", rendered)
self.assertIn("tip ancestor", rendered)
def test_build_projection_row_canonical(self):
row = self.w4.build_projection_row(
task_id="alpha", owner="Test Agent", branch="feature/alpha",
worktree=".worktrees/alpha", mutex="m1", status="in_progress")
self.assertEqual(
row,
"| Test Agent | alpha | feature/alpha | .worktrees/alpha | m1 | in_progress |")
class DarwinCwdVisibilityTests(unittest.TestCase):
def setUp(self):
self.w4 = load_w4(W4_SOURCE)
def test_cwd_visibility_structured_on_host(self):
with tempfile.TemporaryDirectory(prefix="w4-cwd-") as temp:
report = self.w4.cwd_visibility(Path(temp))
self.assertIn(report["status"], ("clear", "busy", "unreadable", "unsupported"))
self.assertIsInstance(report.get("pids"), list)
if sys.platform == "darwin":
self.assertNotEqual(report["status"], "unsupported")
self.assertNotIn("/proc", report.get("message", ""))
if __name__ == "__main__":
unittest.main()