@miller-tech/uap 1.175.6 → 1.175.10
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.
- package/package.json +2 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +54 -2
- package/tools/agents/tests/test_enforcer_escape_hatches.py +67 -5
- package/tools/agents/tests/test_enforcer_suite_coverage.py +91 -0
- package/tools/agents/tests/test_error_loop_ignores_correctives.py +84 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.175.
|
|
3
|
+
"version": "1.175.10",
|
|
4
4
|
"description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"start": "node dist/bin/cli.js",
|
|
22
22
|
"test": "vitest",
|
|
23
23
|
"test:ci": "vitest run",
|
|
24
|
-
"test:enforcers": "python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break",
|
|
24
|
+
"test:enforcers": "python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage",
|
|
25
25
|
"test:coverage": "vitest --coverage",
|
|
26
26
|
"bench": "vitest --config vitest.bench.config.ts",
|
|
27
27
|
"lint": "eslint src --ext .ts",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -325,22 +325,74 @@ _HARNESS_CORRECTIVE_RE = re.compile(
|
|
|
325
325
|
)
|
|
326
326
|
|
|
327
327
|
|
|
328
|
+
# A tool result that declares its own SUCCESS is not a failure, whatever words
|
|
329
|
+
# appear in its prose.
|
|
330
|
+
#
|
|
331
|
+
# _ERROR_LINE_RE matches on tokens ("failed", "error", "not found"), so it reads
|
|
332
|
+
# a NEGATED failure as a failure. Observed live (2026-07-31): the deliver
|
|
333
|
+
# follow-mode poll returns
|
|
334
|
+
#
|
|
335
|
+
# {"ok":true,...,"note":"The deliver run (pid 774213) is STILL RUNNING after
|
|
336
|
+
# 45s. It has not failed -- this wait gave up, the mission did not. ..."}
|
|
337
|
+
#
|
|
338
|
+
# and "failed", inside the sentence saying it had NOT failed, produced an error
|
|
339
|
+
# signature. Three identical healthy polls then tripped the ERROR-LOOP guard,
|
|
340
|
+
# which told the model to re-read its failing output. The model concluded the
|
|
341
|
+
# tool was stuck for 10+ minutes, abandoned follow mode, hand-rolled
|
|
342
|
+
# `sleep 60 && ls` polling, and finally collided with "a deliver run is already
|
|
343
|
+
# in progress" -- a mission that was healthy the whole time.
|
|
344
|
+
#
|
|
345
|
+
# The sentence was written to REASSURE the model it had not failed. Reading the
|
|
346
|
+
# structured verdict instead of the prose is what makes that safe to say.
|
|
347
|
+
_SUCCESS_ENVELOPE_RE = re.compile(r'"ok"\s*:\s*true', re.IGNORECASE)
|
|
348
|
+
_ERROR_FIELD_RE = re.compile(r'"error"\s*:\s*(?!null|""|\s*[,}])', re.IGNORECASE)
|
|
349
|
+
|
|
350
|
+
# "it has not failed", "did not fail", "no errors" -- a denial of failure, in
|
|
351
|
+
# plain prose that carries no ok:true envelope to consult.
|
|
352
|
+
_NEGATED_FAILURE_RE = re.compile(
|
|
353
|
+
r"\b(?:has\s+not|have\s+not|had\s+not|did\s+not|does\s+not|is\s+not|was\s+not|"
|
|
354
|
+
r"were\s+not|not|no|never|without)\s+"
|
|
355
|
+
r"(?:\w+\s+){0,2}?(?:fail(?:ed|ure|s)?|error(?:ed|s)?|crash(?:ed|es)?)\b",
|
|
356
|
+
re.IGNORECASE,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _is_successful_result(text: str) -> bool:
|
|
361
|
+
"""True when the payload structurally declares success.
|
|
362
|
+
|
|
363
|
+
Only an `"ok": true` envelope with no populated `"error"` field counts. A
|
|
364
|
+
bare `"ok": true` alongside a real error field is treated as a failure,
|
|
365
|
+
because a partial success that still reports an error is one the model needs
|
|
366
|
+
to see."""
|
|
367
|
+
if not _SUCCESS_ENVELOPE_RE.search(text):
|
|
368
|
+
return False
|
|
369
|
+
return not _ERROR_FIELD_RE.search(text)
|
|
370
|
+
|
|
371
|
+
|
|
328
372
|
def _error_signature(text: str) -> str:
|
|
329
373
|
"""Edit-invariant signature of the first error line in a tool result.
|
|
330
374
|
|
|
331
375
|
Normalizes away paths, line:col numbers, hex, and bare digits so that the
|
|
332
376
|
SAME underlying failure produces the SAME signature across turns even as the
|
|
333
377
|
model edits different code around it. Returns "" when no error line is found
|
|
334
|
-
(a passing result resets the streak),
|
|
335
|
-
|
|
378
|
+
(a passing result resets the streak), "" for the harness's own correctives
|
|
379
|
+
(_HARNESS_CORRECTIVE_RE), and "" for results that declare success
|
|
380
|
+
(_is_successful_result) or explicitly deny failure (_NEGATED_FAILURE_RE)."""
|
|
336
381
|
if not text:
|
|
337
382
|
return ""
|
|
383
|
+
# Checked against the WHOLE payload, before line matching: the ok:true lives
|
|
384
|
+
# at the head of the envelope while the failure-shaped token can be anywhere
|
|
385
|
+
# in a long note.
|
|
386
|
+
if _is_successful_result(text):
|
|
387
|
+
return ""
|
|
338
388
|
m = _ERROR_LINE_RE.search(text)
|
|
339
389
|
if not m:
|
|
340
390
|
return ""
|
|
341
391
|
line = m.group(0)
|
|
342
392
|
if _HARNESS_CORRECTIVE_RE.search(line):
|
|
343
393
|
return ""
|
|
394
|
+
if _NEGATED_FAILURE_RE.search(line):
|
|
395
|
+
return ""
|
|
344
396
|
line = re.sub(r"(/[^\s:]+)+", "<path>", line) # unix paths
|
|
345
397
|
line = re.sub(r"\b[0-9a-fA-F]{6,}\b", "<hex>", line) # hashes/addresses
|
|
346
398
|
line = re.sub(r"\d+", "#", line) # line numbers, counts
|
|
@@ -14,16 +14,30 @@ override read from os.environ is set by whoever launched the session.
|
|
|
14
14
|
self-protect additionally refuses inline attempts at either flag, so trying reads
|
|
15
15
|
as an explicit refusal rather than appearing to work.
|
|
16
16
|
|
|
17
|
-
NOTE ON THE HARNESS: every case runs with cwd inside a real git repo
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
NOTE ON THE HARNESS: every case runs with cwd inside a real git repo, ON A
|
|
18
|
+
BRANCH, with no review artifact. All three conditions are load-bearing, and each
|
|
19
|
+
one silently inverts a result when it is missing:
|
|
20
|
+
|
|
21
|
+
- outside a git repo, or on a DETACHED head, expert-review cannot resolve a
|
|
22
|
+
branch and fail-opens, so every result reads "allowed". The first version of
|
|
23
|
+
this verification ran from a temp dir and reported the self-grant case as
|
|
24
|
+
passing when it was not. The second ran with cwd=REPO, which is detached in
|
|
25
|
+
CI (checkout of a merge ref) — green locally, red in CI, for the same reason.
|
|
26
|
+
- inside THIS repo on a feature branch, a review artifact usually exists, and
|
|
27
|
+
expert-review then allows the ship on its own merits. The test would pass or
|
|
28
|
+
fail depending on whether the working session happened to record one.
|
|
29
|
+
|
|
30
|
+
So the fixture is a throwaway repo built here, not the checkout the suite runs
|
|
31
|
+
in. A test of a security control must not depend on the mood of its surroundings.
|
|
21
32
|
"""
|
|
22
33
|
|
|
34
|
+
import atexit
|
|
23
35
|
import json
|
|
24
36
|
import os
|
|
37
|
+
import shutil
|
|
25
38
|
import subprocess
|
|
26
39
|
import sys
|
|
40
|
+
import tempfile
|
|
27
41
|
import unittest
|
|
28
42
|
from pathlib import Path
|
|
29
43
|
|
|
@@ -39,6 +53,28 @@ SHIP = "git " + "commit -m x"
|
|
|
39
53
|
RESTART = "systemctl --user " + "rest" + "art " + "uap-" + "llama-server.service"
|
|
40
54
|
|
|
41
55
|
|
|
56
|
+
def _make_fixture_repo() -> Path:
|
|
57
|
+
"""A throwaway git repo on a branch, with no review artifact.
|
|
58
|
+
|
|
59
|
+
Deterministic in every environment: local checkout, CI's detached merge ref,
|
|
60
|
+
or a bare container. See the module docstring for what each missing
|
|
61
|
+
condition would silently do to the results."""
|
|
62
|
+
proj = Path(tempfile.mkdtemp(prefix="uap-hatch-"))
|
|
63
|
+
atexit.register(shutil.rmtree, proj, True)
|
|
64
|
+
run = lambda *a: subprocess.run(a, cwd=proj, check=True, capture_output=True) # noqa: E731
|
|
65
|
+
run("git", "init", "-q", "-b", "feature/escape-hatch-fixture")
|
|
66
|
+
(proj / "f.txt").write_text("x\n")
|
|
67
|
+
run("git", "add", "-A")
|
|
68
|
+
# --no-verify: the repo's own hooks are not under test here, and a hook that
|
|
69
|
+
# blocks would leave the fixture without the commit the enforcers look for.
|
|
70
|
+
run("git", "-c", "user.email=t@t", "-c", "user.name=t",
|
|
71
|
+
"commit", "-qm", "init", "--no-verify")
|
|
72
|
+
return proj
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
PROJECT = _make_fixture_repo()
|
|
76
|
+
|
|
77
|
+
|
|
42
78
|
def verdict(enforcer: str, cmd: str, env: dict | None = None):
|
|
43
79
|
e = dict(os.environ)
|
|
44
80
|
for k in ("UAP_NO_REVIEW", "UAP_INFRA_PROTECT_OFF", "UAP_SELF_PROTECT_OFF"):
|
|
@@ -47,7 +83,7 @@ def verdict(enforcer: str, cmd: str, env: dict | None = None):
|
|
|
47
83
|
p = subprocess.run(
|
|
48
84
|
[sys.executable, str(ENFORCERS / enforcer),
|
|
49
85
|
"--operation", "Bash", "--args", json.dumps({"command": cmd})],
|
|
50
|
-
capture_output=True, text=True, cwd=str(
|
|
86
|
+
capture_output=True, text=True, cwd=str(PROJECT), env=e,
|
|
51
87
|
)
|
|
52
88
|
try:
|
|
53
89
|
return json.loads(p.stdout or "{}").get("allowed")
|
|
@@ -55,6 +91,32 @@ def verdict(enforcer: str, cmd: str, env: dict | None = None):
|
|
|
55
91
|
return f"ERR {(p.stderr or '')[:80]}"
|
|
56
92
|
|
|
57
93
|
|
|
94
|
+
class FixtureIsSoundTest(unittest.TestCase):
|
|
95
|
+
"""If the fixture degrades, every hatch test below passes vacuously.
|
|
96
|
+
|
|
97
|
+
expert-review fail-opens when it cannot resolve a branch, so a broken
|
|
98
|
+
fixture turns "the self-grant was refused" into "everything is allowed" —
|
|
99
|
+
a suite that reports the security control working while it is bypassed.
|
|
100
|
+
That is precisely how this file was green locally and red in CI."""
|
|
101
|
+
|
|
102
|
+
def test_the_fixture_is_a_git_repo_on_a_named_branch(self):
|
|
103
|
+
got = subprocess.run(["git", "symbolic-ref", "--short", "HEAD"],
|
|
104
|
+
cwd=str(PROJECT), capture_output=True, text=True)
|
|
105
|
+
assert got.returncode == 0, f"fixture has no branch (detached?): {got.stderr.strip()}"
|
|
106
|
+
assert got.stdout.strip() == "feature/escape-hatch-fixture", got.stdout
|
|
107
|
+
|
|
108
|
+
def test_the_fixture_has_no_review_artifact(self):
|
|
109
|
+
# With one present, expert-review allows the ship on its own merits and
|
|
110
|
+
# the refusal tests below stop testing anything.
|
|
111
|
+
assert not (PROJECT / ".uap" / "reviews").exists()
|
|
112
|
+
|
|
113
|
+
def test_expert_review_actually_engages_in_the_fixture(self):
|
|
114
|
+
# The positive control: a plain ship must be REFUSED here. If this ever
|
|
115
|
+
# reads True, the enforcer is fail-opening and every assertion that
|
|
116
|
+
# something was "refused" is meaningless.
|
|
117
|
+
assert verdict("expert_review_required.py", SHIP) is False
|
|
118
|
+
|
|
119
|
+
|
|
58
120
|
class InfraProtectHatchTest(unittest.TestCase):
|
|
59
121
|
E = "enforcement_infra_protect.py"
|
|
60
122
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""The Python test gate must actually run the tests it claims to.
|
|
2
|
+
|
|
3
|
+
`npm run test:enforcers` is an EXPLICIT module list handed to
|
|
4
|
+
`python -m unittest`, not discovery. Two silent failure modes follow, and both
|
|
5
|
+
had occurred by 2026-07-31:
|
|
6
|
+
|
|
7
|
+
1. A module exists but is not listed. 37 of 51 were unlisted; 34 of those passed
|
|
8
|
+
perfectly well. The guardrail suites — error-loop, deferral-break,
|
|
9
|
+
stuck-break, session-admission, cycle-break — had no CI coverage at all,
|
|
10
|
+
which is how the same guards kept regressing on green PRs.
|
|
11
|
+
|
|
12
|
+
2. A module is listed but contains no unittest.TestCase subclasses.
|
|
13
|
+
`python -m unittest` collects nothing from a plain pytest-style class,
|
|
14
|
+
reports "Ran 0 tests / NO TESTS RAN", and exits 0. The suite looks covered
|
|
15
|
+
and asserts nothing.
|
|
16
|
+
|
|
17
|
+
Both are invisible in a green build, which is exactly why they need a test.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
26
|
+
TESTS = Path(__file__).resolve().parent
|
|
27
|
+
|
|
28
|
+
# Verified failing for their own pre-existing reasons, not by omission. Each
|
|
29
|
+
# entry is a debt with a stated cause — not a place to park a newly broken test.
|
|
30
|
+
KNOWN_EXCLUDED = {
|
|
31
|
+
"test_anthropic_proxy_streaming": "behaviour drifted from the assertions",
|
|
32
|
+
"test_delivery_enforcement_worktree": "behaviour drifted from the assertions",
|
|
33
|
+
"test_uap_compliance": "needs a populated DB; environment-bound",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _listed_modules() -> set:
|
|
38
|
+
script = json.loads((ROOT / "package.json").read_text())["scripts"]["test:enforcers"]
|
|
39
|
+
return set(re.findall(r"tools\.agents\.tests\.(\w+)\b", script))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _modules_on_disk() -> set:
|
|
43
|
+
return {p.stem for p in TESTS.glob("test_*.py")}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestEveryModuleIsListed(unittest.TestCase):
|
|
47
|
+
def test_no_test_module_is_silently_unlisted(self):
|
|
48
|
+
missing = _modules_on_disk() - _listed_modules() - set(KNOWN_EXCLUDED)
|
|
49
|
+
assert missing == set(), (
|
|
50
|
+
f"these test modules exist but CI never runs them: {sorted(missing)}. "
|
|
51
|
+
"Add them to the test:enforcers script, or record them in "
|
|
52
|
+
"KNOWN_EXCLUDED with the reason they cannot run."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def test_the_exclusion_list_does_not_name_files_that_are_gone(self):
|
|
56
|
+
# A stale exclusion silently re-opens the hole it was documenting.
|
|
57
|
+
stale = set(KNOWN_EXCLUDED) - _modules_on_disk()
|
|
58
|
+
assert stale == set(), f"KNOWN_EXCLUDED names modules that no longer exist: {sorted(stale)}"
|
|
59
|
+
|
|
60
|
+
def test_an_excluded_module_is_not_also_listed(self):
|
|
61
|
+
both = set(KNOWN_EXCLUDED) & _listed_modules()
|
|
62
|
+
assert both == set(), f"listed AND marked excluded — one of the two is wrong: {sorted(both)}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestEveryListedModuleActuallyCollects(unittest.TestCase):
|
|
66
|
+
def test_listed_modules_define_unittest_testcases(self):
|
|
67
|
+
# The "Ran 0 tests" trap: a listed module made of plain classes
|
|
68
|
+
# contributes nothing and still exits 0.
|
|
69
|
+
empty = []
|
|
70
|
+
for name in sorted(_listed_modules()):
|
|
71
|
+
path = TESTS / f"{name}.py"
|
|
72
|
+
if not path.exists():
|
|
73
|
+
continue # covered by the stale-name test below
|
|
74
|
+
src = path.read_text()
|
|
75
|
+
has_case = re.search(r"class\s+\w+\s*\(\s*[\w.]*TestCase\s*\)", src)
|
|
76
|
+
# A module may instead expose bare `def test_*` at module level,
|
|
77
|
+
# which unittest also does not collect — so that is not a rescue.
|
|
78
|
+
if not has_case:
|
|
79
|
+
empty.append(name)
|
|
80
|
+
assert empty == [], (
|
|
81
|
+
f"listed in test:enforcers but define no unittest.TestCase, so they run "
|
|
82
|
+
f"zero tests and pass: {empty}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def test_every_listed_module_exists(self):
|
|
86
|
+
ghosts = {n for n in _listed_modules() if not (TESTS / f"{n}.py").exists()}
|
|
87
|
+
assert ghosts == set(), f"test:enforcers names modules that do not exist: {sorted(ghosts)}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
unittest.main()
|
|
@@ -18,32 +18,47 @@ making progress.
|
|
|
18
18
|
The distinction the fix encodes: a CORRECTIVE is the harness talking to the model
|
|
19
19
|
about its behaviour; an ERROR is a tool reporting that something did not work.
|
|
20
20
|
Only the latter belongs in the streak.
|
|
21
|
+
|
|
22
|
+
The same guard fired again on 2026-07-31, on the opposite side: deliver's
|
|
23
|
+
follow-mode poll returns `{"ok":true, ... "It has not failed — this wait gave up,
|
|
24
|
+
the mission did not."}`, and the word "failed", inside the sentence saying it had
|
|
25
|
+
NOT failed, produced an error signature. See TestSuccessfulResultsAreNotFailures.
|
|
26
|
+
|
|
27
|
+
NOTE ON THE TEST RUNNER: these classes MUST subclass unittest.TestCase. CI runs
|
|
28
|
+
`npm run test:enforcers`, which is `python3 -m unittest <explicit module list>`,
|
|
29
|
+
and unittest collects nothing from a plain pytest-style class — this file
|
|
30
|
+
reported "Ran 0 tests / NO TESTS RAN" while appearing to be covered. Adding a
|
|
31
|
+
class here without TestCase, or adding a module without listing it in
|
|
32
|
+
test:enforcers, produces a test that never runs.
|
|
21
33
|
"""
|
|
22
34
|
|
|
23
35
|
import re
|
|
36
|
+
import unittest
|
|
24
37
|
from pathlib import Path
|
|
25
38
|
|
|
26
39
|
PROXY = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
def _load_signature():
|
|
30
|
-
"""Extract _error_signature and its patterns without importing the server.
|
|
43
|
+
"""Extract _error_signature and its patterns without importing the server.
|
|
44
|
+
|
|
45
|
+
Takes ONE contiguous slice, from the first pattern through the end of
|
|
46
|
+
_error_signature, rather than cherry-picking names. The previous version
|
|
47
|
+
exec'd a hardcoded list of regexes, so adding a helper to the proxy left the
|
|
48
|
+
test raising NameError against its own source -- the loader had to be edited
|
|
49
|
+
in lockstep with any new dependency."""
|
|
31
50
|
src = PROXY.read_text()
|
|
32
51
|
ns = {"re": re}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
exec(src[start:end], ns) # noqa: S102 - reading our own source, not input
|
|
37
|
-
start = src.index("def _error_signature")
|
|
38
|
-
end = src.index("\n# ---", start)
|
|
39
|
-
exec(src[start:end], ns) # noqa: S102
|
|
52
|
+
start = src.index("_ERROR_LINE_RE = re.compile(")
|
|
53
|
+
end = src.index("\n# ---", src.index("def _error_signature"))
|
|
54
|
+
exec(src[start:end], ns) # noqa: S102 - reading our own source, not input
|
|
40
55
|
return ns["_error_signature"]
|
|
41
56
|
|
|
42
57
|
|
|
43
58
|
_error_signature = _load_signature()
|
|
44
59
|
|
|
45
60
|
|
|
46
|
-
class TestHarnessCorrectivesAreNotFailures:
|
|
61
|
+
class TestHarnessCorrectivesAreNotFailures(unittest.TestCase):
|
|
47
62
|
def test_write_nudge_does_not_enter_the_failure_streak(self):
|
|
48
63
|
# The exact line from the incident.
|
|
49
64
|
assert _error_signature("[agent r6 error] write-nudge injected after 5 read-only rounds") == ""
|
|
@@ -64,7 +79,7 @@ class TestHarnessCorrectivesAreNotFailures:
|
|
|
64
79
|
assert {_error_signature(line) for _ in range(6)} == {""}
|
|
65
80
|
|
|
66
81
|
|
|
67
|
-
class TestRealFailuresStillTracked:
|
|
82
|
+
class TestRealFailuresStillTracked(unittest.TestCase):
|
|
68
83
|
def test_tool_errors_still_produce_a_signature(self):
|
|
69
84
|
for line in (
|
|
70
85
|
"ERROR: TypeError: x is not a function at /a/b.js:12",
|
|
@@ -90,3 +105,62 @@ class TestRealFailuresStillTracked:
|
|
|
90
105
|
# become a way to hide real errors behind a nudge.
|
|
91
106
|
text = "ERROR: ReferenceError: foo is not defined\n[agent r6 error] write-nudge injected"
|
|
92
107
|
assert _error_signature(text) != ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# The follow-mode incident, 2026-07-31.
|
|
111
|
+
FOLLOW_POLL = (
|
|
112
|
+
'{"ok":true,"dryRun":false,"exitCode":0,"note":"The deliver run (pid 774213) is '
|
|
113
|
+
"STILL RUNNING after 45s. It has not failed — this wait gave up, the mission did "
|
|
114
|
+
"not. This is the NORMAL answer for a mission that takes longer than one poll, and "
|
|
115
|
+
"the run is healthy. Call deliver again with follow:true to keep waiting. Do NOT "
|
|
116
|
+
'kill the deliver process."}'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestSuccessfulResultsAreNotFailures(unittest.TestCase):
|
|
121
|
+
def test_the_healthy_follow_poll_produces_no_signature(self):
|
|
122
|
+
# Reproduces the incident exactly: "failed", inside the sentence saying
|
|
123
|
+
# it had NOT failed, made a healthy poll look like a failure.
|
|
124
|
+
assert _error_signature(FOLLOW_POLL) == ""
|
|
125
|
+
|
|
126
|
+
def test_three_identical_healthy_polls_cannot_arm_the_guard(self):
|
|
127
|
+
# Three was the threshold. This is the whole bug: waiting patiently, as
|
|
128
|
+
# instructed, was what tripped the loop guard.
|
|
129
|
+
assert {_error_signature(FOLLOW_POLL) for _ in range(3)} == {""}
|
|
130
|
+
|
|
131
|
+
def test_ok_true_beats_failure_words_anywhere_in_the_note(self):
|
|
132
|
+
assert _error_signature('{"ok":true,"note":"0 tests failed, no errors"}') == ""
|
|
133
|
+
|
|
134
|
+
def test_a_populated_error_field_still_counts_despite_ok_true(self):
|
|
135
|
+
# A partial success that still reports an error is one the model needs
|
|
136
|
+
# to see; ok:true must not become a way to launder real failures.
|
|
137
|
+
text = '{"ok":true,"error":"ENOENT: cannot find module foo"}'
|
|
138
|
+
assert _error_signature(text) != ""
|
|
139
|
+
|
|
140
|
+
def test_an_empty_error_field_does_not_count(self):
|
|
141
|
+
assert _error_signature('{"ok":true,"error":null,"note":"nothing failed"}') == ""
|
|
142
|
+
assert _error_signature('{"ok":true,"error":"","note":"nothing failed"}') == ""
|
|
143
|
+
|
|
144
|
+
def test_ok_false_is_still_a_failure(self):
|
|
145
|
+
text = '{"ok":false,"error":"a deliver run is already in progress"}'
|
|
146
|
+
assert _error_signature(text) != ""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestDeniedFailuresInPlainProse(unittest.TestCase):
|
|
150
|
+
def test_a_denial_of_failure_is_not_a_failure(self):
|
|
151
|
+
# No ok:true envelope to consult — the prose itself has to be read.
|
|
152
|
+
for line in (
|
|
153
|
+
"The build has not failed; it is still compiling.",
|
|
154
|
+
"The run did not fail — it is waiting on the model.",
|
|
155
|
+
"Completed with no errors.",
|
|
156
|
+
"Finished without errors.",
|
|
157
|
+
):
|
|
158
|
+
assert _error_signature(line) == "", line
|
|
159
|
+
|
|
160
|
+
def test_a_plain_assertion_of_failure_still_counts(self):
|
|
161
|
+
for line in (
|
|
162
|
+
"The build failed.",
|
|
163
|
+
"ERROR: the run failed after 3 retries",
|
|
164
|
+
"2 tests failed",
|
|
165
|
+
):
|
|
166
|
+
assert _error_signature(line) != "", line
|