Repository navigation
fix(pixel): judge an exit-0 unittest run by the runner's own summary; a coding stop takes precedence over test-command coaching - #6752
Open
Lightheartdevs wants to merge 4 commits into
Conversation
… a coding stop takes precedence over test-command coaching Fleet, open-prompt 07 photo-renamer on main d4a61f3. tower3 (Qwen3.5-27B, PARTIAL): `python3 -m unittest test_photo_renamer -v` exited 0 with "Ran 11 tests in 0.010s / OK", but a passing negative-path test had printed "Error: Folder '/nonexistent/folder/path' does not exist." The exit-0 heuristics in execResultHasNonCleanUnittestOutcome matched that program output, so the verification stayed failed, the failed-attempt count grew instead of resetting, and the model's accurate 973-character answer was replaced by VERIFICATION_FAILED_DELIVERY_PREFIX. When a result contains unittest's own "Ran N tests in X.XXXs" summary, it is now judged by the runner's markers only: a zero-test summary or NO TESTS RAN, a summary not followed by OK, a FAILED ( line, nonzero expected failures or unexpected successes (also in -v form), or an exact "FAIL|ERROR: test (module.Class...)" header. Program output such as "Error:", "ERROR:root:" logging, -v docstrings or AssertionError text no longer fails a clean run. ANSI colors (Python 3.14 on a pty) are ignored for this check. Without a summary (custom runners) the existing heuristics are unchanged, and a nonzero exit still decides first. strixy (Qwen3.6-35B-A3B): after the coding stop, the model's `...; echo "EXIT:$?"` reached the non-auditable refusal before the coding terminal and was told to "Run it directly", contradicting "Do not call another tool". After a coding stop that refusal now defers to the stop's terminal handling (terminal block, then abort). Its text, free-correction accounting and stale-pass invalidation are unchanged. Tests replay the recorded tower3, strixy (tail) and tower2 sessions call by call through createToolLoopGuard, reproducing every recorded refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; echoed summaries, compaction and fixture names Review of #6752 (462c784): - A clean "Ran N tests ... OK" summary made every other line program output, so a check that runs beside unittest in the same exec lost the df3f4bf protection: a module-level print checker imported by discovery, ad-hoc checks after unittest.main(exit=False), a test that catches its own assertion and prints the traceback. With a summary present, a line-start FAIL or AssertionError (optionally indented) now fails the result, as does unittest's failure-count line in any case ("failed (failures=1)"). - The failure-header check matched program output such as "ERROR: IMG_0002.jpg (corrupt)". A header now counts only right after unittest's "=" * 70 separator and only with a test name and a dotted identifier. - andChainVerificationParams accepts literal `echo` setup before the test command, and the echoed words share the result, so they could print a summary the test never produced. A chain with an echo setup segment is judged by the text heuristics only, on the exec and process-poll paths. - Tool Search compaction replaced the result with the summary alone, so a result that passed despite program output hid that output from the model. Compaction again requires the text heuristics to find nothing, as on d4a61f3. - Fixtures are renamed unittest-verdict-*-d4a61f33.json; #6753 adds a different photo-renamer-tower2-d4a61f33.json. The tower2 replay stops at the custom-runner call 8 and no longer asserts the rm -rf delivery text that #6753 changes. Replays pin the refusal text only for the calls this change concerns and otherwise check that the recorded path is followed, so #6751's Playground wording does not break them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… checker fail lines and AssertionError fail it Re-review of #6752 (probes R01-R06, R28, R30 and two real Python repros): - In summary mode, AssertionError anywhere in the result (any case) and a line that starts with FAIL in any case before ":", "(", a space, a tab or the line end now fail the run, as in the reviewer's prototype. A checker's "fail: ...", "Fail: ...", "Test failed: AssertionError('...')" or "x add(-1, -2): AssertionError: ..." beside a clean summary is failed again. A passing test that prints AssertionError text is now failed too: a false failure, never a false pass. - The summary is trusted only when the command the model sent runs `python -m unittest` directly (alone or after `cd /workspace/...`). Any and-chain setup segment (echo, cp <file> /dev/stdout, mv, mkdir, touch, true) and any direct test script (`python3 test_x.py`, whose own code runs around unittest.main()) keep the text heuristics. This closes the cp and echo summary forging, a script that prints a summary of its own, and "Error:"/"ERROR:" checks after unittest.main(exit=False). - withoutTerminalColors is factored out for reuse. Replays: tower3, strixy and tower2 are unchanged. Over the 63 recorded fleet sessions (3,350 calls) this commit differs from 462c784 nowhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 26, 2026
…ocess beside the runner verificationCommandIsAuditable refuses pipes, ";", redirections and and/or chains, but not a lone "&". `python3 -m unittest test_calc & python3 check_calc.py` is therefore a plain, auditable unittest command, and c0a83a7 trusted its runner summary. The checker's "Error: add(-1, -2) returned 1, expected -3" next to that clean summary passed; d4a61f3 fails the same result by its text heuristics. A command with "&" (other than the trailing stderr merge) is now judged by the text heuristics, on the exec and the process-poll paths. The pre-execution gate is unchanged. Red check: the new test fails on c0a83a7's guard (88/89) and the whole file fails 21 of 89 on 11c8f71's guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightheartdevs
added a commit
that referenced
this pull request
Sep 26, 2026
…ude/pixel-deletion-refusal-keeps-verified-work #6753 now builds on #6752's unittest verdict instead of its own. The conflict in execResultHasNonCleanUnittestOutcome is resolved as in the re-review's union: #6752's runner-summary verdict (runner-only trust, checker FAIL lines and AssertionError) plus #6753's execResultTexts and skipped-only check. #6753's own verdict change from eb55782 (a case-sensitive failure match whenever a clean "Ran N tests ... OK" appears anywhere) is dropped; without a trusted summary the text heuristics are main's, byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightheartdevs
added a commit
that referenced
this pull request
Sep 26, 2026
…request that asks for nothing in the reply and a test nothing overlapped Re-review of #6753 (probes N01, N04-N06, N20-N25): - Answer-request gate (medium). The receipt stands for the result only when the owner asked for nothing to be said in the reply. The detector is now deliberately broad: a question, or any ask for an explanation, description, summary, overview, report or results, usage or instructions, how it works or how to run it, documentation, a walkthrough, a comparison or citations, "tell me", "show me", "give me", "let me know", "include ...", or a reply, answer or response. A match keeps the run failed with the same honest receipt, as for any other incomplete run. All eight phrasings from the re-review now stay failed. - Overlapping background command (low-medium). A background exec that was running when the test started, started while a background test ran, or was observed to end while it ran, marks that pass as overlapped; so does any background exec still pending when a foreground test passes. An overlapped pass is not lifted, and the receipt says why. A background command that ended before the test started does not block the lift. - No passing test (low). The check reuses #6752's withoutTerminalColors and its unittest summary/outcome parsing: Python 3.14 colored "OK (skipped=N)" with N >= Ran, skipped plus expected failures covering every test, and any pytest summary line without a nonzero "passed" (skipped, xfailed, xpassed or deselected only) keep the refusal failed; Jest/Vitest "Tests" lines and node:test "todo" are covered the same way. The receipt line now says no test passed instead of claiming every test was skipped. - Coverage (extra, conservative). A test run from the workspace root covers only top-level files, and a test command that names a parent, home or absolute path outside its directory covers nothing. Unchanged and re-verified: the loop bound after the refusal, the deletion decisions (no new deletion allowed), the receipt text for a current pass, and the capped long-path receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightheartdevs
added a commit
that referenced
this pull request
Sep 26, 2026
… into claude/pixel-deletion-refusal-keeps-verified-work Brings in #6752's latest verdict fix: a unittest summary is not trusted when & starts a second process beside the runner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Fleet, open-prompt 07 photo-renamer ("write a python script that renames all photos in a folder by date taken, and test it") on main
d4a61f33:python3 -m unittest test_photo_renamer -vexited 0 withRan 11 tests in 0.010s/OK. A passing negative-path test had printedError: Folder '/nonexistent/folder/path' does not exist.to stdout.execResultHasNonCleanUnittestOutcomematched that line with `/(?:^...; echo "EXIT:$?"got "Run it directly", which contradicts "Do not call another tool". The model obeyed the newer message (call 41).Control on the same tower2 hardware and model: Hermes DELIVERED in 23 s and OpenCode DELIVERED in 19 s (
claude-app-compare/runs/val2/07-photo-renamer).The same heuristic also false-fails:
ERROR:root:...);ERROR: cannot read folder ...;-vdocstrings that start with "Error when".(
AssertionErrortext printed by a passing test still fails; see Accepted tradeoffs.)Change
plugin/tool-loop-guard.mjs:Runner summary verdict, for unittest's own runner only. The summary is trusted only when the command the model sent runs
python -m unittest ...directly, alone or aftercd /workspace/...(execFingerprintRunsUnittestRunner). If a result text of such a command contains unittest's summary line (Ran N tests in X.XXXs, CRLF-tolerant, ANSI colors stripped), the result is judged by these rules instead of the old text heuristics. It is non-clean if any of these holds:NO TESTS RAN;OK/OK (...)after at most one blank line;FAILED (line, or unittest's failure counts in any case (failed (failures=1));expected failures=orunexpected successes=greater than 0, or their-vforms;FAIL|ERROR: <test> (<dotted.id>)header directly after unittest's"=" * 70separator;FAILword, or withFAILin any case before:,(, a space, a tab or the line end (fail: negative numbers,Fail: ...);AssertionErroranywhere, in any case (Test failed: AssertionError('...'),✗ add(-1, -2): AssertionError: ...).The last two rules are the df3f4bf protection for a check that runs beside unittest in the same exec: a module-level print checker imported by discovery, or a test that catches its own assertion and prints it. Program output such as
Error: ...,ERROR: IMG_0002.jpg (corrupt),ERROR:root:logging or a-vdocstring is no longer failure evidence for such a command. Without a summary in the result, the old text heuristics apply.execFailedstill decides first, and pytest is still judged by exit status only.Anything else keeps the old text heuristics, byte for byte. A summary can be printed by something other than the runner, so these commands never use the summary rules:
andChainVerificationParamsaccepts before the test (echo Ran 5 tests ... && ...,cp summary.txt /dev/stdout && ...,mkdir,mv,touch,true);python3 test_calc.py), whose own code runs aroundunittest.main(): checks afterunittest.main(exit=False), or a custom runner that prints a summary of its own;&(python3 -m unittest test_calc & python3 check_calc.py), whichverificationCommandIsAuditabledoes not refuse. The trailing stderr merge2>&1alone is not a second process.This holds on the exec path and on the process-poll path.
Compaction keeps program output visible. Tool Search compaction (
cleanUnittestSummary) replaces a clean result with its summary alone. It still requires the old text heuristics to find nothing, as ond4a61f33. So a result that passes despite program output keeps that output in the transcript.Coding stop first. After
codingExhausted, the non-auditable refusal defers to the coding terminal handler: one terminal block, thenabortRun. Before a coding stop, the refusal text and its free-correction accounting are unchanged.Deliberate deviations from the spec (please review)
&& !state?.codingExhaustedon the branch condition would also skip the branch'slatestVerificationStatus = "failed"line. That line invalidates a pass that predates a later write. A piped test command has no verification fingerprint, soafterToolCalldoes not re-mark it, and the stalepassedwould survive. I checked this: with the literal condition, the new test "after a coding stop ... still invalidates a stale pass" fails withactual: 'passed'. So only the coaching return is gated oncodingExhausted.OK,FAILEDand the failure headers when output goes to a terminal (a pty exec). I checked this locally with Python 3.14.4 andFORCE_COLOR=1:\x1b[32mOK\x1b[0m (\x1b[33mskipped=1\x1b[0m). Without stripping, a clean colored run would newly fail. Main also let a colored exit-0FAILEDrun pass:\bAssertionError\bhas no word boundary afterm. Both cases are covered by tests.Accepted tradeoffs
python -m unittest, a check that reports a failure only asError:orERROR:lines (for example a module-level checker imported by discovery) passes next to a clean summary. This is the tower3 shape, which cannot be told apart from a passing negative-path test's output. A check that printsFAILat the start of a line, orAssertionErroranywhere, still fails.AssertionErrortext, or a program line that starts withFAIL(for exampleFAIL: IMG_0001.jpg (no EXIF date)), fails the run. Both also failed ond4a61f33: a false failure, never a false pass.d4a61f33. A checker line that starts withFAILED:orFailed:beside a cleanpython -m unittestsummary passes; it also passes ond4a61f33.d4a61f33. These shapes passed on main and now fail: a summary not followed byOKwithin one blank line, including output cut after theRanline, two blank lines, or a program line beforeOK. None of these occurs in the replayed fleet sessions.Review fixes (
11c8f711)FAILorAssertionErrorfails the result even with a summary. The real Python repro, reviewer probes A, B and D, the Python 3.14unittest.main(exit=False)capture, and pass-edit-rerun all return to failed.Error:line now stays in the persisted result.echocould forge a summary ahead of a custom runner. A mimic runner'sfailed (failures=1)was also missed.photo-renamer-tower2-d4a61f33.json. The tower2 test also asserted the rm -rf delivery text that #6753 changes.unittest-verdict-*-d4a61f33.json. The tower2 replay stops at call 8 and asserts only the call-6 and call-8 verdicts.UNITTEST_FAILURE_HEADERmatched program output such asERROR: IMG_0001.jpg (no EXIF date)."=" * 70separator and only as<test> (<dotted.id>).ERROR: IMG_0002.jpg (corrupt)andERROR: /tmp/x.jpg ([Errno 2] ...)now pass.Found while checking the fixes. #6751 changes the Playground refusal text that tower2 call 0 recorded, so the old exact-refusal check failed with #6751 merged. The replays now pin the refusal text only for the calls this change concerns (strixy 38-41). For every other call they check that the recorded path is followed: refused calls are refused and calls that ran still run.
Re-review fixes (
c0a83a78e,1363e768c)The re-review found that a failed or non-unittest output could still be read as a passing unittest trailer on
11c8f711.fail: ...,Fail: ...,fail(...)or a barefailline,Test failed: AssertionError('...'),✗ add(-1, -2): AssertionError: ...or a lower-caseassertionerrorbeside a clean summary passed.c0a83a78e:FAILin any case before:,(, a space, a tab or the line end, andAssertionErroranywhere in any case, fail the result in summary mode.cp expected_summary.txt /dev/stdout && python3 -m unittest -v, alsomv,mkdir,touch,true) was trusted.c0a83a78e: onlypython -m unittestitself, alone or aftercd /workspace/..., is trusted; every and-chain setup keeps the text heuristics (exec and process-poll).Error:/ERROR:checks afterunittest.main(exit=False)passed.c0a83a78e:python3 test_x.pykeeps the text heuristics, with or without a summary.python3 -m unittest test_calc & python3 check_calc.pyis a plain, auditable command, so its summary was trusted: the checker'sError: add(-1, -2) returned 1, expected -3next to the runner's clean summary passed on11c8f711andc0a83a78e, and fails ond4a61f33and main0a632581.1363e768c: a command with&(other than the trailing2>&1) keeps the text heuristics, on the exec and process-poll paths. The pre-execution gate is unchanged.Replays (recorded sessions, call by call through
createToolLoopGuard)The fixtures were trimmed from the session JSONLs, with provenance in each file:
047d3145, sha256e861b9f0…, all 8 calls;519cb0fa, sha256e5134e08…, the two writes, the six exit-1 runs each after its preceding edit, edit 37 and refused calls 38-41;fe9ee859, sha2568d21ebc1…, calls 0-8 (the fixture keeps all 11).The replay runs in a real temp workspace, so Playground routing behaves as it does on the host.
verificationForRunVERIFICATION_FAILED_DELIVERY_PREFIX(= recorded answer.txt){status:'passed'}replyPayloadSending(973-char answer)VERIFICATION_FAILED_DELIVERY_PREFIXundefined: the answer is delivered unchanged (beforeAgentFinalizealsoundefined)CODING_RETRY_EXHAUSTED_REASON×2 (= recorded)...; echo "EXIT:$?"VERIFICATION_COMMAND_NOT_AUDITABLE_REASON(= recorded), no abortCODING_LOOP_ABORT_REASON,abortRuncalledpython3 test_rename.py(custom runner, no summary)More replay probes:
CODING_REPEAT_NO_PROGRESS_REASON.Fleet-wide replay.
11c8f711(earlier): the reviewer harnessreplay2.mjsover 63 recorded sessions (the six d4a61f3 open-prompt photo-renamer sessions and 57 qualification sessions from rounds 100-107; 3,350 calls, 318 runs) gave 0 differences against462c7849, and againstd4a61f33the same 4 sessions as before: tower3 flips to delivered; strixy and two laptop coding journeys reach the coding stop one call earlier with no change in delivered status.1363e768c(this head): reviewer 2'sreplay-multiharness (each run ends with the realagent_endhandler) over 117 recorded sessions: the 60 d4a61f3 open-prompt sessions (6 hosts) and 57 qualification sessions from rounds 100-107; 372 runs, 3,728 calls. Against11c8f711: 0 differences in delivered status and text, refusals, signals and aborts. Of the 275 recorded unittest commands, 131 are a plain auditablepython -m unittestcommand and none of them contains&, so1363e768cchanges no recorded session.Tests
New file
tests/unittest_trailer_verdict.test.mjs, 89 tests.The replays are listed above. The unit cases below all exit 0 and run both native and Tool Search wrapped.
Stay passed (
python -m unittest):ERROR:root:logging;ERROR: cannot read folder;-vdocstring that starts with "Error when";OK (skipped=2);Error:/ERROR:lines after the summary;ERROR: IMG_0001.jpg (no EXIF date),ERROR: IMG_0002.jpg (corrupt)andERROR: /tmp/… ([Errno 2] …);Failed (no EXIF date): …;Stay failed:
FAIL:header withOK;ERROR:header after the separator;unittest.main(exit=False)withFAILED (failures=1);FAILED (errors=1);Ran 0 tests ... OK(df3f4bf);NO TESTS RAN;OK (expected failures=1);... unexpected success;FAILED;FAIL: negative numbers\nAssertionError(df3f4bf).New for the first review:
FAILafter the summary;unittest.main(exit=False)(Python 3.14 shape);FAIL;failed (failures=1)afterOK;python3 test_all.py);d4a61f33;New for the re-review:
AssertionErrortext printed by a passing test, mid-line or in lower case, and in a repr or after a mark;fail:,Fail:,fail(...)and a barefailchecker line;AssertionError(real Python 3.12 output);Error:/ERROR:checks afterunittest.main(exit=False)and a script that prints its own summary; the same output frompython3 -m unittestpasses;cp/mvto/dev/stdoutor/dev/stderr, and bymkdir/touch/truesetup chains (exec and process-poll);&, alone or aftercd /workspace/...(exec and process-poll); the trailing2>&1alone still passes.Other cases:
Unchanged:
verification_redirect.test.mjs;FAILED (failures=1)tests intool_loop_guard.test.mjs;DEFAULT_WEB_TOOL_LIMITS(3 / 6);RUN_PROGRESS_LIMITS;VERIFICATION_COMMAND_NOT_AUDITABLE_REASON;verificationCommand,verificationCommandIsAuditableandandChainVerificationParams;canonicalRequestedUnittestParams;compactFailedUnittestText.Red-checks:
11c8f711guard: 21 of 89 fail, all of them re-review cases.&test on thec0a83a78eguard: fails (88 of 89 pass).462c7849guard, 19 fail; on thed4a61f33guard, 32 fail.Full Linux suite (
remote-js-test.sh, tower2):d4a61f33(main)462c7849(first commit)11c8f711c0a83a78e1363e768c(this head)1363e768c+ main0a632581(local merge)1363e768c+ main8c589ef5(local merge)c091bd635(contains this head) + main8c589ef5Composition with open PRs (at
11c8f711). I merged each of 19 open Pixel PRs into11c8f711locally and ran the new test file each time: #6698, #6702, #6715, #6716, #6722, #6723, #6724, #6726, #6730, #6732, #6741, #6743 and #6746-#6753.d4a61f33itself, independently of this PR.Credential scan of fixtures. No keys, tokens, passwords, emails or private IPs. The host cwd is stripped. The only long hex strings are the session sha256 values.
Not done here
session-trace.jsonok:false,Wsl/Service/0x8007274c), not from Pixel. An earlier version of this description said "code in chat only", which was wrong;python3 -m unittest mod.T.test_one) resets the failed-attempt count, so failing full-suite runs can continue past the limit. This is the same ond4a61f33and deserves its own change.🤖 Generated with Claude Code