gabriel / muse public

test_cmd_commit_hooks_acceptance.py file-level

at sha256:4 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:7 feat: add chess domain plugin (Episode 22 finale) A chess game as a po… · gabriel · Sep 12, 2026
1 """HK_ACCEPT_01 — musehub#192 Phase 5 acceptance test.
2
3 Reproduces this session's real incident end-to-end against a real
4 disposable repo: .museagent.md drifts from its compiled adapter (edited but
5 never re-synced), and confirms the dogfood hook
6 (`muse agent-config status --fail-if-out-of-sync`) catches it at commit
7 time, blocking the commit — and that --no-verify still gets it through,
8 visibly.
9 """
10
11 from __future__ import annotations
12
13 import json
14 import os
15 import pathlib
16 import stat
17 import sys
18
19 import pytest
20
21 from tests.cli_test_helper import CliRunner, InvokeResult
22
23 runner = CliRunner()
24
25
26 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
27 saved = os.getcwd()
28 try:
29 os.chdir(repo)
30 return runner.invoke(None, args)
31 finally:
32 os.chdir(saved)
33
34
35 @pytest.fixture(autouse=True)
36 def _muse_shim_on_path(tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch) -> None:
37 """Make the ``muse`` name on PATH resolve to *this checkout's* source.
38
39 The pre-commit hook mechanism (``run_hook_point``) shells out to
40 whatever ``muse`` the environment's PATH resolves — by design, since
41 hooks call the real user-facing tool. In this repo specifically, plain
42 ``muse`` is the separately installer-managed stable venv (see
43 docs/agent-guide.md's "muse vs muse-dev"), which does not yet have the
44 ``--fail-if-out-of-sync`` flag this acceptance test exercises. Without
45 this shim, the test would silently exercise stale installed code
46 instead of the change under test.
47 """
48 shim_dir = tmp_path_factory.mktemp("muse-shim")
49 shim = shim_dir / "muse"
50 shim.write_text(
51 f"#!{sys.executable}\n"
52 "import sys\n"
53 "from muse.cli.app import main\n"
54 "main(sys.argv[1:])\n",
55 encoding="utf-8",
56 )
57 shim.chmod(shim.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
58 monkeypatch.setenv("PATH", f"{shim_dir}{os.pathsep}{os.environ['PATH']}")
59
60 repo_root = pathlib.Path(__file__).resolve().parents[1]
61 existing_pythonpath = os.environ.get("PYTHONPATH", "")
62 monkeypatch.setenv(
63 "PYTHONPATH",
64 f"{repo_root}{os.pathsep}{existing_pythonpath}" if existing_pythonpath else str(repo_root),
65 )
66
67
68 class TestAgentConfigDriftAcceptance:
69 def test_reproduces_agent_config_drift_incident(self, tmp_path: pathlib.Path) -> None:
70 assert _invoke(tmp_path, ["init", "--domain", "code"]).exit_code == 0
71
72 assert _invoke(tmp_path, ["agent-config", "init"]).exit_code == 0
73 assert _invoke(
74 tmp_path, ["agent-config", "set", "--adapters", "claude,codex"]
75 ).exit_code == 0
76 assert _invoke(tmp_path, ["agent-config", "sync"]).exit_code == 0
77
78 assert _invoke(tmp_path, ["code", "add", "."]).exit_code == 0
79 assert _invoke(tmp_path, ["commit", "-m", "initial agent config"]).exit_code == 0
80
81 # Wire up the exact dogfood hook from #192's plan.
82 (tmp_path / ".musehooks.toml").write_text(
83 '[pre-commit]\ncommands = ["muse agent-config status --fail-if-out-of-sync"]\n',
84 encoding="utf-8",
85 )
86 assert _invoke(tmp_path, ["code", "add", ".musehooks.toml"]).exit_code == 0
87 assert _invoke(tmp_path, ["commit", "-m", "add pre-commit hook"]).exit_code == 0
88 assert _invoke(tmp_path, ["hooks", "install"]).exit_code == 0
89
90 # Reproduce the actual incident: edit .museagent.md, do NOT re-sync
91 # the embed adapter (codex/AGENTS.md) — exactly what happened in
92 # ~/ecosystem/musehub this session.
93 agent_md = tmp_path / ".museagent.md"
94 agent_md.write_text(agent_md.read_text() + "\n## New rule\nSomething new.\n")
95 (tmp_path / "unrelated.py").write_text("x = 1\n")
96 assert _invoke(tmp_path, ["code", "add", "unrelated.py"]).exit_code == 0
97
98 r = _invoke(tmp_path, ["commit", "-m", "edit agent config without syncing", "--json"])
99 assert r.exit_code == 1, r.output
100 data = json.loads(r.output)
101 assert "agent-config" in data["failed_command"]
102 assert data["error"] == "pre_commit_hook_failed"
103
104 # --no-verify still gets it through, and never silently.
105 r2 = _invoke(
106 tmp_path,
107 ["commit", "-m", "edit agent config without syncing", "--no-verify", "--json"],
108 )
109 assert r2.exit_code == 0, r2.output
110 data2 = json.loads(r2.output)
111 assert any("no-verify" in w.lower() for w in data2["warnings"])
112
113 # Prove the fix actually was "re-sync", not just "commit anyway" —
114 # after syncing, the same edit commits clean without --no-verify.
115 (tmp_path / "unrelated2.py").write_text("y = 2\n")
116 assert _invoke(tmp_path, ["code", "add", "unrelated2.py"]).exit_code == 0
117 assert _invoke(tmp_path, ["agent-config", "sync"]).exit_code == 0
118 assert _invoke(tmp_path, ["code", "add", "AGENTS.md"]).exit_code == 0
119 r3 = _invoke(tmp_path, ["commit", "-m", "re-sync then commit", "--json"])
120 assert r3.exit_code == 0, r3.output