Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions tests/test_governance_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,63 @@ def test_veto_escalate_on_sensitive_path_blocks_even_when_attended(tmp_path):
pkt = _fresh_packet(tmp_path, decision="escalate", sensitive=True)
rc = _run_veto(tmp_path, packet=pkt, env={"DORIAN_POLICY": "assist", "DORIAN_BASE": "HEAD"})
assert rc == 2


# --- Slice 0 release-hardening: pin the fail-closed veto contract at its boundaries -------


def test_veto_strict_identity_mismatch_blocks(tmp_path):
# strict + fresh CONTINUE, but the packet's base_ref != DORIAN_BASE -> identity mismatch blocks
pkt = _fresh_packet(tmp_path, decision="continue", base="HEAD")
rc = _run_veto(
tmp_path, packet=pkt, env={"DORIAN_POLICY": "unattended", "DORIAN_BASE": "other-ref"}
)
assert rc == 2


def test_veto_godmode_escalate_blocks(tmp_path):
# DORIAN_EFFORT=godmode forces strict even under the default (attended) policy
pkt = _fresh_packet(tmp_path, decision="escalate")
rc = _run_veto(tmp_path, packet=pkt, env={"DORIAN_EFFORT": "godmode", "DORIAN_BASE": "HEAD"})
assert rc == 2


def test_veto_strict_fresh_at_899s_allowed(tmp_path):
# freshness boundary: 899s < FRESH_SECONDS(900) is still fresh. Use float, not int() — int()
# floors away the sub-second fraction and would eat the ~1s margin for subprocess spawn.
pkt = _fresh_packet(tmp_path, decision="continue")
pkt["created_at_epoch"] = time.time() - 899
rc = _run_veto(tmp_path, packet=pkt, env={"DORIAN_POLICY": "unattended", "DORIAN_BASE": "HEAD"})
assert rc == 0


def test_veto_strict_stale_at_901s_blocks(tmp_path):
# freshness boundary: 901s > FRESH_SECONDS(900) is stale -> fails closed under strict
pkt = _fresh_packet(tmp_path, decision="continue")
pkt["created_at_epoch"] = time.time() - 901
rc = _run_veto(tmp_path, packet=pkt, env={"DORIAN_POLICY": "unattended", "DORIAN_BASE": "HEAD"})
assert rc == 2


def test_veto_attended_escalate_empty_path_blocks(tmp_path):
# a standing escalate on a tool with no file_path is unreviewable -> blocks even when attended
pkt = _fresh_packet(tmp_path, decision="escalate", sensitive=False)
rc = _run_veto(
tmp_path,
tool='{"tool_input": {}}',
packet=pkt,
env={"DORIAN_POLICY": "assist", "DORIAN_BASE": "HEAD"},
)
assert rc == 2


def test_veto_attended_malformed_stdin_allows(tmp_path):
# malformed tool JSON under an attended policy fails OPEN (a human is present)
rc = _run_veto(tmp_path, tool="{ not json", packet=None, env={"DORIAN_POLICY": "assist"})
assert rc == 0


def test_veto_strict_malformed_stdin_blocks(tmp_path):
# malformed tool JSON under a strict policy fails CLOSED (exit 2)
rc = _run_veto(tmp_path, tool="{ not json", packet=None, env={"DORIAN_POLICY": "unattended"})
assert rc == 2
29 changes: 29 additions & 0 deletions tests/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,32 @@ def test_installed_scaffolds_loop_guard_skill(installed_dorian: Path, tmp_path:
text = (repo / ".claude/skills/dorian-loop-guard/SKILL.md").read_text(encoding="utf-8")
for phrase in ("/dorian-loop-guard", "not a sandbox", "token-free"):
assert phrase in text


def test_installed_scaffolds_governance_adapter(installed_dorian: Path, tmp_path: Path) -> None:
"""The packaged binary scaffolds the Claude Code governance adapter from package data —
proving the two host hooks + settings + docs ship in the wheel, and that the installed
PreToolUse veto carries its exit-2 fail-closed path for a real `pip install dorian-vwp` user."""
repo = tmp_path / "govrepo"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True, capture_output=True)
r = subprocess.run(
[str(installed_dorian), "--repo", str(repo), "governance", "install"],
capture_output=True,
text=True,
)
assert r.returncode == 0, r.stderr
veto = repo / ".claude/hooks/dorian_preflight_veto.py"
stop = repo / ".claude/hooks/dorian_loop_preflight.py"
for f in (
veto,
stop,
repo / ".claude/settings.dorian-governance.example.json",
repo / ".claude/dorian-governance/README.md",
repo / ".claude/dorian-governance/reference/enforcement-modes.md",
):
assert f.is_file(), f
veto_text = veto.read_text(encoding="utf-8")
assert "return 2" in veto_text # the exit-2 tool-block veto ships in the installed hook
assert "FAIL CLOSED" in veto_text.upper()
assert "hookSpecificOutput" in stop.read_text(encoding="utf-8")