@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.175.6",
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",
@@ -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), and "" for the harness's own
335
- correctives -- see _HARNESS_CORRECTIVE_RE."""
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. Outside
18
- one, expert-review fail-opens on an unresolvable branch and EVERY result reads
19
- "allowed" the first version of this verification was run from a temp dir and
20
- reported the self-grant case as passing when it was not.
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(REPO), env=e,
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
- for name in ("_ERROR_LINE_RE", "_HARNESS_CORRECTIVE_RE"):
34
- start = src.index(f"{name} = re.compile(")
35
- end = src.index(")\n", src.index("re.", start + 20)) + 1
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