feat(system): DPLAN-0088 auto STATUS.md sync on PR create/merge (#155)

- Trigger: add pr_created + pr_merged events (#13, #14) with fire-and-forget
  handler that runs `drone @prax status sync` (9 new tests, 191 total)
- Drone: wire trigger.fire() into pr_handler, pr_plugin, merge_plugin
  (lazy import, try/except — never blocks PR workflow, 447 tests)
- Flow: fix dashboard push failure on plan create
- Global prompt: remove compaction anxiety language, Patrick controls compaction

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIPass
2026-03-30 22:42:12 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 8ff56c2914
commit ec3c79d4dc
13 changed files with 430 additions and 42 deletions
+134
View File
@@ -681,3 +681,137 @@ class TestModuleRegistration:
assert hasattr(mod, "handle_command")
assert hasattr(mod, "get_help")
assert hasattr(mod, "get_introspective")
# ===========================================================================
# 7. trigger.fire() integration tests
# ===========================================================================
class TestTriggerFireIntegration:
"""Verify trigger.fire() is called after successful PR/merge operations."""
def test_pr_handler_fires_pr_created(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""pr_handler.create_pr fires pr_created event on success."""
registry = tmp_path / "AIPASS_REGISTRY.json"
registry.write_text("{}", encoding="utf-8")
monkeypatch.chdir(tmp_path)
call_count = 0
def mock_run(cmd, **kwargs):
nonlocal call_count
call_count += 1
r = MagicMock()
r.stderr = ""
if cmd[1:3] == ["rev-parse", "--abbrev-ref"]:
r.returncode = 0
r.stdout = "main\n"
elif cmd[0] == "git" and cmd[1] == "add":
r.returncode = 0
r.stdout = ""
elif cmd[1:3] == ["diff", "--cached"]:
r.returncode = 1 # 1 = something staged
r.stdout = ""
elif cmd[0] == "git" and cmd[1] == "commit":
r.returncode = 0
r.stdout = ""
elif cmd[0] == "git" and cmd[1] == "branch":
r.returncode = 0
r.stdout = ""
elif cmd[0] == "git" and cmd[1] == "push":
r.returncode = 0
r.stdout = ""
elif cmd[0] == "gh" and cmd[1] == "pr":
r.returncode = 0
r.stdout = "https://github.com/org/repo/pull/99\n"
else:
r.returncode = 0
r.stdout = ""
return r
mock_trigger = MagicMock()
with patch("aipass.drone.apps.handlers.git.pr_handler.subprocess.run", side_effect=mock_run):
with patch("aipass.drone.apps.handlers.git.pr_handler.acquire_lock", return_value={"success": True, "message": "ok"}):
with patch("aipass.drone.apps.handlers.git.pr_handler.release_lock"):
with patch("aipass.trigger.apps.modules.core.trigger", mock_trigger):
result = create_pr("api", "test trigger", tmp_path / "src" / "aipass" / "api")
assert result["success"] is True
mock_trigger.fire.assert_any_call("pr_created", branch="api", pr_url="https://github.com/org/repo/pull/99")
def test_pr_handler_continues_if_trigger_fails(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""pr_handler.create_pr succeeds even if trigger.fire raises."""
registry = tmp_path / "AIPASS_REGISTRY.json"
registry.write_text("{}", encoding="utf-8")
monkeypatch.chdir(tmp_path)
def mock_run(cmd, **kwargs):
r = MagicMock()
r.stderr = ""
if cmd[1:3] == ["rev-parse", "--abbrev-ref"]:
r.returncode = 0
r.stdout = "main\n"
elif cmd[1:3] == ["diff", "--cached"]:
r.returncode = 1
r.stdout = ""
elif cmd[0] == "gh" and cmd[1] == "pr":
r.returncode = 0
r.stdout = "https://github.com/org/repo/pull/100\n"
else:
r.returncode = 0
r.stdout = ""
return r
mock_trigger = MagicMock()
mock_trigger.fire.side_effect = RuntimeError("trigger broken")
with patch("aipass.drone.apps.handlers.git.pr_handler.subprocess.run", side_effect=mock_run):
with patch("aipass.drone.apps.handlers.git.pr_handler.acquire_lock", return_value={"success": True, "message": "ok"}):
with patch("aipass.drone.apps.handlers.git.pr_handler.release_lock"):
with patch("aipass.trigger.apps.modules.core.trigger", mock_trigger):
result = create_pr("api", "test resilience", tmp_path / "src" / "aipass" / "api")
assert result["success"] is True # PR still succeeds despite trigger failure
def test_merge_plugin_fires_pr_merged(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""merge_plugin.merge_pr fires pr_merged event on success."""
registry = tmp_path / "AIPASS_REGISTRY.json"
registry.write_text("{}", encoding="utf-8")
monkeypatch.chdir(tmp_path)
from aipass.drone.apps.plugins.devpulse_ops.merge_plugin import merge_pr
call_idx = 0
def mock_run(cmd, **kwargs):
nonlocal call_idx
call_idx += 1
r = MagicMock()
r.stderr = ""
if cmd[0] == "gh" and cmd[1] == "pr" and cmd[2] == "merge":
r.returncode = 0
r.stdout = ""
elif cmd[0] == "git" and cmd[1] == "pull":
r.returncode = 0
r.stdout = ""
elif cmd[1:3] == ["rev-parse", "HEAD"]:
r.returncode = 0
r.stdout = "abc123def456\n"
elif cmd[0] == "gh" and cmd[1] == "pr" and cmd[2] == "view":
r.returncode = 0
r.stdout = "Fix the thing\n"
else:
r.returncode = 0
r.stdout = ""
return r
mock_trigger = MagicMock()
with patch("aipass.drone.apps.plugins.devpulse_ops.merge_plugin.subprocess.run", side_effect=mock_run):
with patch("aipass.trigger.apps.modules.core.trigger", mock_trigger):
result = merge_pr("42", "devpulse")
assert result["success"] is True
mock_trigger.fire.assert_any_call("pr_merged", pr_number="42", title="Fix the thing")