@miller-tech/uap 1.178.1 → 1.179.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,10 +52,12 @@ def _load_signature():
52
52
  start = src.index("_ERROR_LINE_RE = re.compile(")
53
53
  end = src.index("\n# ---", src.index("def _error_signature"))
54
54
  exec(src[start:end], ns) # noqa: S102 - reading our own source, not input
55
- return ns["_error_signature"]
55
+ return ns
56
56
 
57
57
 
58
- _error_signature = _load_signature()
58
+ _NS = _load_signature()
59
+ _error_signature = _NS["_error_signature"]
60
+ _error_signature_for_result = _NS["_error_signature_for_result"]
59
61
 
60
62
 
61
63
  class TestHarnessCorrectivesAreNotFailures(unittest.TestCase):
@@ -164,3 +166,90 @@ class TestDeniedFailuresInPlainProse(unittest.TestCase):
164
166
  "2 tests failed",
165
167
  ):
166
168
  assert _error_signature(line) != "", line
169
+
170
+
171
+ # The source file the model was reading when ERROR-LOOP fired on 2026-07-31.
172
+ # Nothing failed; this is a file's CONTENTS, delivered as a successful Read.
173
+ SOURCE_FILE_READ = '''def _load_state() -> dict:
174
+ try:
175
+ data = json.loads(_state_path().read_text())
176
+ return data if isinstance(data, dict) else {}
177
+ except Exception: # noqa: BLE001 - unreadable state must not break the tool call
178
+ return {}
179
+ '''
180
+
181
+
182
+ class TestIsErrorFlagBeatsKeywords(unittest.TestCase):
183
+ """A file that CONTAINS "Exception" is not a failure.
184
+
185
+ Keyword sniffing cannot tell a program reporting a failure from a file that
186
+ merely mentions one. Reading a file is the most common thing an agent does,
187
+ so any source containing `Exception`, `error` or `not found` could
188
+ manufacture a streak — and did: three reads of one file produced three
189
+ identical signatures, and ERROR-LOOP told the model to re-read the file it
190
+ had just read.
191
+ """
192
+
193
+ def test_a_successful_read_never_produces_a_signature(self):
194
+ self.assertEqual(_error_signature_for_result(SOURCE_FILE_READ, False), "")
195
+
196
+ def test_three_successful_reads_cannot_arm_the_guard(self):
197
+ # Three was the threshold, and re-reading a file is not a failure streak.
198
+ sigs = {_error_signature_for_result(SOURCE_FILE_READ, False) for _ in range(3)}
199
+ self.assertEqual(sigs, {""})
200
+
201
+ def test_is_error_false_wins_over_a_real_looking_traceback(self):
202
+ # `cat` of a log file full of tracebacks is still a successful read.
203
+ text = "Traceback (most recent call last):\nTypeError: x is not a function"
204
+ self.assertEqual(_error_signature_for_result(text, False), "")
205
+
206
+ def test_is_error_true_still_produces_a_signature(self):
207
+ text = "ERROR: TypeError: x is not a function at /a/b.js:12"
208
+ self.assertNotEqual(_error_signature_for_result(text, True), "")
209
+
210
+ def test_is_error_true_with_an_unfamiliar_shape_still_forms_a_streak(self):
211
+ # The client declared a failure; losing it because no keyword matched
212
+ # would be the opposite mistake — a real repeated failure going untracked.
213
+ text = "the frobnicator declined\nmore detail here"
214
+ sig = _error_signature_for_result(text, True)
215
+ self.assertNotEqual(sig, "")
216
+ self.assertEqual(sig, _error_signature_for_result(text, True)) # stable
217
+
218
+ def test_without_the_flag_the_old_heuristics_still_apply(self):
219
+ # Clients that never send is_error must keep working exactly as before.
220
+ self.assertNotEqual(_error_signature_for_result("ERROR: SyntaxError: bad", None), "")
221
+ self.assertEqual(_error_signature_for_result("all tests passed", None), "")
222
+ self.assertEqual(
223
+ _error_signature_for_result(SOURCE_FILE_READ, None), _error_signature(SOURCE_FILE_READ)
224
+ )
225
+
226
+ def test_an_empty_declared_error_does_not_crash(self):
227
+ self.assertEqual(_error_signature_for_result("", True), "")
228
+ self.assertEqual(_error_signature_for_result(" \n\n ", True), "")
229
+
230
+
231
+ class TestTheFlagIsActuallyWiredThrough(unittest.TestCase):
232
+ """The logic above is worthless if the caller never passes the flag.
233
+
234
+ That was the whole bug: the request handler computed `_latest_err` from the
235
+ tool_result blocks and then called `note_tool_result_error(_latest_tr)`,
236
+ dropping it. Every unit test passed. Reverting the call site to the one-arg
237
+ form — i.e. restoring the live bug exactly — still passes the entire suite,
238
+ which is why this asserts on the source.
239
+ """
240
+
241
+ def test_the_request_handler_passes_the_is_error_flag(self):
242
+ src = PROXY.read_text()
243
+ assert "_latest_err" in src, "the handler no longer computes the is_error flag"
244
+ assert re.search(r"note_tool_result_error\(\s*_latest_tr\s*,\s*_latest_err\s*\)", src), (
245
+ "note_tool_result_error is called without the is_error flag — reading a file that "
246
+ "contains the word 'Exception' will manufacture a failure streak again"
247
+ )
248
+
249
+ def test_the_signature_helper_is_the_one_being_used(self):
250
+ # A refactor that quietly points the monitor back at _error_signature
251
+ # would reinstate the keyword-only behaviour.
252
+ src = PROXY.read_text()
253
+ assert re.search(
254
+ r"sig = _error_signature_for_result\(latest_result_text or \"\", result_error\)", src
255
+ ), "note_tool_result_error no longer routes through _error_signature_for_result"
@@ -0,0 +1,106 @@
1
+ """validate-plan-on-change: only track what `uap plan validate` can validate.
2
+
3
+ The enforcer recorded ANY plan-named file it saw written, including paths
4
+ outside the project. `uap plan validate` refuses those ("explicit plan file must
5
+ live under the project directory"), so the entry could never be cleared: every
6
+ build in the repo blocked, and the remedy the refusal named declined the file.
7
+ Observed live with a memory note at
8
+ ~/.claude/projects/<slug>/memory/plan_gate_before_build.md — not a plan at all,
9
+ matched only because its filename contains "plan".
10
+
11
+ The rename case is the one to guard hardest. `mv PLAN.md PLAN2.md` is not an
12
+ edit op, so no new pending entry is recorded; auto-forgiving the old key on the
13
+ build path would make a rename a silent, unattended gate bypass with the plan
14
+ content fully intact. An earlier draft of this change did exactly that.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import unittest
24
+ from pathlib import Path
25
+
26
+ ENFORCER = Path(__file__).resolve().parents[3] / "src" / "policies" / "enforcers" / "validate_plan_on_change.py"
27
+
28
+
29
+ def run(cwd: str, op: str, args: dict) -> str:
30
+ proc = subprocess.run(
31
+ [sys.executable, str(ENFORCER), "--operation", op, "--args", json.dumps(args)],
32
+ capture_output=True,
33
+ text=True,
34
+ cwd=cwd,
35
+ )
36
+ return proc.stdout + proc.stderr
37
+
38
+
39
+ def state(cwd: str) -> dict:
40
+ path = Path(cwd) / ".uap" / "plan_state.json"
41
+ return json.loads(path.read_text()) if path.exists() else {}
42
+
43
+
44
+ class TestPlanGateTracksOnlyProjectFiles(unittest.TestCase):
45
+ def setUp(self) -> None:
46
+ self._tmp = tempfile.TemporaryDirectory()
47
+ self.cwd = self._tmp.name
48
+ os.makedirs(os.path.join(self.cwd, "docs", "plans"), exist_ok=True)
49
+
50
+ def tearDown(self) -> None:
51
+ self._tmp.cleanup()
52
+
53
+ def test_outside_project_plan_is_not_tracked(self) -> None:
54
+ out = run(self.cwd, "Write", {"file_path": "/tmp/elsewhere/stray-plan.md"})
55
+ self.assertIn("outside the project", out)
56
+ self.assertEqual(state(self.cwd).get("pending", {}), {})
57
+
58
+ def test_in_project_plan_is_still_tracked(self) -> None:
59
+ Path(self.cwd, "docs", "plans", "real-plan.md").write_text("# real")
60
+ run(self.cwd, "Write", {"file_path": "docs/plans/real-plan.md"})
61
+ self.assertIn("docs/plans/real-plan.md", state(self.cwd).get("pending", {}))
62
+
63
+ def test_build_is_blocked_and_names_the_recovery_command(self) -> None:
64
+ Path(self.cwd, "docs", "plans", "real-plan.md").write_text("# real")
65
+ run(self.cwd, "Write", {"file_path": "docs/plans/real-plan.md"})
66
+ out = run(self.cwd, "Bash", {"command": "npm run build"})
67
+ self.assertIn("never validated", out)
68
+ # A wedged agent must not be sent to the one command that declines the file.
69
+ self.assertIn("uap plan clear", out)
70
+
71
+ def test_renaming_a_pending_plan_does_not_forgive_it(self) -> None:
72
+ plan = Path(self.cwd, "docs", "plans", "real-plan.md")
73
+ plan.write_text("# real")
74
+ run(self.cwd, "Write", {"file_path": "docs/plans/real-plan.md"})
75
+ plan.rename(Path(self.cwd, "docs", "plans", "real-plan-v2.md"))
76
+
77
+ out = run(self.cwd, "Bash", {"command": "npm run build"})
78
+ self.assertIn("never validated", out)
79
+ self.assertIn("docs/plans/real-plan.md", state(self.cwd).get("pending", {}))
80
+
81
+ def test_legacy_outside_entry_is_pruned_and_audited(self) -> None:
82
+ Path(self.cwd, "docs", "plans", "real-plan.md").write_text("# real")
83
+ run(self.cwd, "Write", {"file_path": "docs/plans/real-plan.md"})
84
+ st = state(self.cwd)
85
+ st.setdefault("pending", {})["/home/somewhere/legacy-plan.md"] = 1
86
+ Path(self.cwd, ".uap", "plan_state.json").write_text(json.dumps(st))
87
+
88
+ run(self.cwd, "Bash", {"command": "npm run build"})
89
+ st = state(self.cwd)
90
+ self.assertNotIn("/home/somewhere/legacy-plan.md", st.get("pending", {}))
91
+ # The in-project plan still gates the build.
92
+ self.assertIn("docs/plans/real-plan.md", st.get("pending", {}))
93
+ # A shrinking blocking set must leave a trail.
94
+ cleared = st.get("cleared", [])
95
+ self.assertEqual(len(cleared), 1)
96
+ self.assertEqual(cleared[0]["key"], "/home/somewhere/legacy-plan.md")
97
+ self.assertIn("outside the project", cleared[0]["reason"])
98
+
99
+ def test_non_plan_writes_are_untouched(self) -> None:
100
+ out = run(self.cwd, "Write", {"file_path": "src/index.ts"})
101
+ self.assertIn("not a plan artifact", out)
102
+ self.assertEqual(state(self.cwd).get("pending", {}), {})
103
+
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main()