Repository navigation
loopguard: a heredoc's lines are not test runs - #174
Aaron Li (aaron-jx-li) merged 2 commits into
Conversation
status_of_tests split the raw command, so a heredoc line that starts with a test runner counted as a test run of its own. A turn whose last real run failed, followed by `gh pr create --body-file - <<'EOF'` with `pytest -q` in the body, was recorded as ended green, and the judge was told the last verification passed. It now drops heredoc bodies before splitting, as exit_status_covers already does. A runner that reads a heredoc (`pytest -q <<EOF`), or runs after one, is now judged by its own status, where before it was read as "mixed".
|
@microsoft-github-policy-service agree |
A heredoc that ends a bash -lc '<script>' closes on EOF' in the recorded command, so dropping heredoc bodies before the unwrap also dropped the rest of the script, and the test run before the heredoc was lost. _pieces now drops them after the unwrap, for both status_of_tests and exit_status_covers.
Muhammad Abdullah Butt (1AbdullahButt)
left a comment
There was a problem hiding this comment.
I read the change against exit_status_covers and _split_heredocs, and checked _HEREDOC_START on a few shell forms in a standalone snippet. Moving the heredoc strip into _pieces (after the bash -lc unwrap) makes sense to me, and it also stops a set -o pipefail inside a heredoc body from turning pipefail on for the outer command.
One thing that now reaches status_of_tests: _HEREDOC_START (tokeneconomizer/loopguard/shell_edits.py:16, <<-?\s*(['"]?)([A-Za-z_]\w*)\1) also matches the << inside a here-string or a shift. read -r v <<< word gives the tag word, echo $((1<<n)) gives n, and echo "a<<b" gives b. Because _split_heredocs treats an unclosed body as running to the end, every later line is dropped. So with this PR:
read -r v <<< word
pytest -q
now returns None from status_of_tests (on main it returns "own"). A failing run written this way would no longer count as the turn's last verification, which is the same ended-green mix-up this PR fixes, just in the other direction. It's rare, but cheap to close:
_HEREDOC_START = re.compile(r"(?<!<)<<(?!<)-?\s*(['\"]?)([A-Za-z_]\w*)\1")That handles the here-string, and cat<<EOF with no space still matches. A matching test next to the new ones in tests/test_live_core.py, for example status_of_tests("read -r v <<< word\npytest -q") == "own", would pin it. Quoted text (echo "a<<b") and arithmetic ($((1<<n))) could still trip it. Masking quotes before the heredoc scan would handle that, but it may be more than this PR needs.
Note: I did not run the test suite locally. I only checked the regex in isolation.
Reviewed with help from Claude Code.
Summary
status_of_testssplit the raw command, so a heredoc line that starts with a test runner counted as a test run. It now drops heredoc bodies first, asexit_status_coversalready does:def status_of_tests(cmd): ... - pieces, pipefail = _pieces(cmd) + pieces, pipefail = _pieces(_split_heredocs(cmd)[0])Before, a turn whose last real test run failed, followed by
gh pr create --body-file - <<'EOF'withpytest -qin the body, was recorded as ended green. The judge was then told "The last verification run passed."Evidence
Before (main
264e7a7):All 5 new tests in
tests/test_live_core.pyfail.After:
The new tests pass, and
python -m pytest testspasses: 1246 passed, 19 skipped. The only failures inexperiments/testsare the fivetest_model_spellingones, which call GNUtimeout. That's missing on macOS, and the same tests fail on main.Merge Danger
Door: two-way. One line, and no on-disk state or setting changes.
Blast Radius: narrow
Only commands that contain a heredoc are classified differently. The one case that moves the other way is a script piped into a shell (
bash <<EOF\npytest\nEOF). Its body no longer counts as a test run, which matches howexit_status_coversalready treats it.