From e054c1fd377cf6e3c261773174ade402e576e0d5 Mon Sep 17 00:00:00 2001 From: Cruz Morales Date: Mon, 19 Oct 2026 21:37:45 -0500 Subject: [PATCH] test(dojo-verify): pin the warn default under a bare environment and in its message The warn default landed in 7479bd6; these tests hold it in place. The default-mode test now checks stderr is empty and that the message tells the reader to set mode to block. The bare-environment portability test runs warn as well as block. The block-mode test sets the mode itself instead of relying on the sandbox default. The hook's docstring names warn as the default and block as opt-in. Co-Authored-By: Claude Opus 5.5 (1M context) --- plugins/dojo-verify/hooks/stop_evidence.py | 6 +++--- plugins/dojo-verify/tests/_helpers.py | 3 ++- .../dojo-verify/tests/test_stop_evidence.py | 21 +++++++++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/plugins/dojo-verify/hooks/stop_evidence.py b/plugins/dojo-verify/hooks/stop_evidence.py index eec5768..9b82dbe 100644 --- a/plugins/dojo-verify/hooks/stop_evidence.py +++ b/plugins/dojo-verify/hooks/stop_evidence.py @@ -2,9 +2,9 @@ """dojo-verify: Stop hook. A reply that claims success needs a passing check behind it. When the assistant's final message claims success (tests pass, verified, fixed, deployed ...) and no recognized -check ran, and came back clean, after the last change made in this session, this hook blocks the stop once (mode -block), or just says so (mode warn). A session with no change in it is not judged. It reads the newest part of the -transcript, never runs anything, and fails open on every error. +check ran, and came back clean, after the last change made in this session, this hook says so (mode warn, the +default), or blocks the stop once (mode block, opt-in). A session with no change in it is not judged. It reads the +newest part of the transcript, never runs anything, and fails open on every error. Stdlib only, Python 3.9. The claim and check patterns live in patterns.json (the mod carries a copy in patterns.ts; a test keeps them equal). diff --git a/plugins/dojo-verify/tests/_helpers.py b/plugins/dojo-verify/tests/_helpers.py index 8928b22..4b09e6f 100644 --- a/plugins/dojo-verify/tests/_helpers.py +++ b/plugins/dojo-verify/tests/_helpers.py @@ -158,7 +158,8 @@ def payload(self, **kw): return p def env(self, **extra): - # Most tests exercise the block path, so they set it; the default (warn) is tested with the option absent. + # The detection tests read a firing as a block, so they run in mode block; the default (warn) has its own + # tests, which drop this with CLAUDE_PLUGIN_OPTION_MODE=None. env = {"PATH": "/usr/bin:/bin", "HOME": self.home, "CLAUDE_PLUGIN_DATA": self.state, "TMPDIR": self.tmpdir, "CLAUDE_PLUGIN_OPTION_MODE": "block"} for k, v in extra.items(): diff --git a/plugins/dojo-verify/tests/test_stop_evidence.py b/plugins/dojo-verify/tests/test_stop_evidence.py index 11a0b44..006af79 100644 --- a/plugins/dojo-verify/tests/test_stop_evidence.py +++ b/plugins/dojo-verify/tests/test_stop_evidence.py @@ -25,15 +25,17 @@ def claim_turn(self, final="Fixed. The tests pass.", middle=None): class TestKillSwitchesAndModes(Base): - def test_default_mode_warns_and_does_not_block(self): + def test_default_mode_warns_and_never_blocks(self): r = self.run_hook(self.claim_turn(), env=self.sb.env(CLAUDE_PLUGIN_OPTION_MODE=None)) self.assertEqual(r.code, 0) + self.assertEqual(r.err, "") data = r.json() self.assertNotIn("decision", data) self.assertTrue(data["systemMessage"].startswith("dojo-verify:")) + self.assertIn("set mode to block", data["systemMessage"]) def test_block_mode_blocks_with_the_documented_shape(self): - r = self.run_hook(self.claim_turn()) + r = self.run_hook(self.claim_turn(), env=self.sb.env(CLAUDE_PLUGIN_OPTION_MODE="block")) self.assertEqual(r.code, 0) self.assertEqual(r.err, "") data = r.json() @@ -86,7 +88,7 @@ def test_off_mode_is_silent(self): def test_unknown_mode_falls_back_to_warn(self): r = self.run_hook(self.claim_turn(), env=self.sb.env(CLAUDE_PLUGIN_OPTION_MODE="garbage")) - self.assertNotIn("decision", r.json()) + self.assertFalse(r.blocked) self.assertIn("systemMessage", r.json()) def test_stop_hook_active_never_blocks(self): @@ -1098,13 +1100,20 @@ def test_an_unwritable_state_dir_still_blocks(self): class TestPortability(Base): - def test_block_and_fail_open_under_a_bare_environment(self): + def test_warn_block_and_fail_open_under_a_bare_environment(self): # The same invocation the engine could use on a machine with nothing but the system tools. import subprocess self.sb.write(self.claim_turn()) env_cmd = ["/usr/bin/env", "-i", "PATH=/usr/bin:/bin", "HOME=" + self.sb.home, - "TMPDIR=" + self.sb.tmpdir, "CLAUDE_PLUGIN_OPTION_MODE=block", h.SYSTEM_PYTHON, h.HOOK] - blocked = subprocess.run(env_cmd, input=json.dumps(self.sb.payload()).encode(), stdout=subprocess.PIPE, + "TMPDIR=" + self.sb.tmpdir, h.SYSTEM_PYTHON, h.HOOK] + warned = subprocess.run(env_cmd, input=json.dumps(self.sb.payload()).encode(), stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=20, cwd=self.sb.project) + self.assertEqual(warned.returncode, 0) + self.assertNotIn("decision", json.loads(warned.stdout)) + self.assertIn("systemMessage", json.loads(warned.stdout)) + self.assertEqual(warned.stderr, b"") + block_cmd = env_cmd[:4] + ["CLAUDE_PLUGIN_OPTION_MODE=block"] + env_cmd[4:] + blocked = subprocess.run(block_cmd, input=json.dumps(self.sb.payload()).encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20, cwd=self.sb.project) self.assertEqual(blocked.returncode, 0) self.assertEqual(json.loads(blocked.stdout)["decision"], "block")