Eureka/tools/test-tools.sh
Eureka 11f4e08a21 ygg-verify: local mode for the Yggdrasil seat; executable modes
One code path with a transport switch: on a host named yggdrasil[-*] (or
YGG_VERIFY_LOCAL=1) the remote half runs through local bash with cp and a local
git push instead of ssh/scp; the ssh path is unchanged. Scripts meant to be run
directly are committed 100755, and the scripts that call ygg-verify.sh do so
through bash (noexec skill mount). test-tools.sh covers the local branch and,
under a mock ssh, the remote branch and the detection rule.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 16:22:11 +00:00

911 lines
80 KiB
Shell
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Local, cheap checks of the verdict-bearing text parsers: tools/testparse.sh and
# the summary and wait modes of tools/stopgap/ygg-verify.sh, over
# tools/fixtures/logs. cargo-*.log (the tiny-crate fixture) and mutation-table-nonce.log (the soul2
# reproducer) are real Yggdrasil runs, 2026-10-01; soul-s5/*.log are real
# dotnet, pytest, vitest and cargo runs of Soul's probe repo (several runs per
# log, split by "=== MARK <name>" lines); the rest are hand-written.
# The mutation runners tools/mutation-table.sh and tools/cargo-mutants.sh (launcher, host driver
# and container halves) run under a mock docker and, for cargo-mutants, a fake cargo and flock
# (fixtures/mutation-mock/fakebin), with real slot locks. ygg-verify.sh's local branch and (through a mock ssh)
# its remote branch run end to end over the same mock docker.
# Run: bash tools/test-tools.sh (no network, no builds)
set -uo pipefail
here=$(cd "$(dirname "$0")" && pwd)
L=$here/fixtures/logs; V=$here/stopgap/ygg-verify.sh
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
fail=0
NONCE=''
# seg <log> <mark> -> a file holding just that run of a multi-run log
seg() { local o="$T/$(basename "$1").$2"; awk -v m="=== MARK $2" '/^=== MARK /{on=($0==m);next} on' "$L/$1" >"$o"; echo "$o"; }
# check <log path or name> <exit status> <must-contain> [<must-not-contain>]
check() {
local p=$1; [ -f "$p" ] || p=$L/$1
out=$(bash "$V" summarize "$p" "$2" ${NONCE:+"$NONCE"})
printf '%s\n' "$out" | grep -qF -- "$3" || { echo "FAIL $1: missing: $3"; fail=1; return; }
if [ -n "${4:-}" ] && printf '%s\n' "$out" | grep -qF -- "$4"; then echo "FAIL $1: must not contain: $4"; fail=1; return; fi
[ "$(printf '%s\n' "$out" | wc -l)" -le 81 ] || { echo "FAIL $1: summary over 81 lines"; fail=1; return; }
echo "ok $(basename "$1"): $3"
}
check cargo-green.log 0 "exit 0 GREEN" "RED"
check cargo-green.log 0 "FRAMEWORK cargo passed=3 failed=0"
check cargo-red.log 101 "exit 101 RED"
check cargo-red.log 101 "FAILED tests::add_adds"
check cargo-red.log 101 "left: 5"
check cargo-zero-tests.log 0 "ZERO TESTS RAN"
NONCE=474d380d662e71a5 # a real run: the soul2 reproducer through the snapshot runner, 2026-10-01
check mutation-table-nonce.log 1 "untested-equiv | SURVIVED"
check mutation-table-nonce.log 1 "stamp2-bad | CAUGHT | tests::d_cache"
check mutation-table-nonce.log 1 "add-minus | CAUGHT | tests::c_add"
check mutation-table-nonce.log 1 "caught=2 survived=1 unviable=0 timeout=0 noapply=0 error=0" "no test counts recognised"
NONCE=
check dotnet-red.log 1 "FRAMEWORK dotnet passed=11 failed=1 skipped=2"
check dotnet-red.log 1 "FAILED MyNs.AddTests.Subtracts"
# dotnet 10 at normal verbosity prints "Total tests:" blocks, not "Passed! - Failed:". dotnet10-green/red/zero.log are
# real runs (dotnet 10.0.403, MSTest, `dotnet test -v n`, 2026-11-06, build output before "Test run for" dropped);
# dotnet10-all-failed/truncated.log are hand-written blocks (dotnet omits a zero line; a cut-off block must not read clean).
check dotnet10-green.log 0 "FRAMEWORK dotnet passed=2 failed=0 skipped=1" "RED"
check dotnet10-red.log 1 "FRAMEWORK dotnet passed=1 failed=1 skipped=1"
check dotnet10-red.log 1 "FAILED Bad"
check dotnet10-red.log 0 "MISMATCH: exit 0 but the log shows"
check dotnet10-zero.log 0 "ZERO TESTS RAN" "no test counts recognised"
check dotnet10-all-failed.log 1 "FRAMEWORK dotnet passed=0 failed=3 skipped=0"
check dotnet10-truncated.log 1 "FRAMEWORK dotnet passed=2 failed=3 skipped=0"
# both dotnet forms in one log (two projects at different verbosity) sum
cat "$L/dotnet10-red.log" "$L/dotnet-red.log" >"$T/dotnet-both.log"
check "$T/dotnet-both.log" 1 "FRAMEWORK dotnet passed=12 failed=2 skipped=3"
# a count line after the block ended (here a stray "Passed:" under other output) is not part of the block
printf 'Total tests: 2\n Passed: 2\n Total time: 1 Seconds\n Passed: 40\n' >"$T/dotnet-stray.log"
check "$T/dotnet-stray.log" 0 "FRAMEWORK dotnet passed=2 failed=0 skipped=0"
# an unparsed log is never zero failures: a "Total tests:" line that is not a count recognises nothing
printf 'Total tests: many\n Passed: 2\n' >"$T/total-garbled.log"
check "$T/total-garbled.log" 0 "no test counts recognised"
check vitest-red.log 1 "FRAMEWORK vitest passed=10 failed=1 skipped=1"
check vitest-red.log 1 "FAILED src/add.test.ts > add > carries"
check pytest-red.log 1 "FRAMEWORK pytest passed=5 failed=1 skipped=1"
check pytest-red.log 1 "FAILED tests/test_add.py::test_carries"
check exit0-but-failed.log 0 "MISMATCH: exit 0 but the log shows"
check no-verdict.log 97 "NO VERDICT"
check no-verdict.log 97 "RED" "GREEN"
# ---- tools/mutation-table.sh, host driver plus container half, against the mock in
# fixtures/mutation-mock: shell scripts that print cargo/vitest/jest-shaped output, run
# from the subdirectory pkg/ (a workdir below the repo root), under a mock docker
# (fixtures/mutation-mock/fakebin) where a container is a directory and `commit` copies
# it, so every mutant really runs on a copy of the baselined snapshot.
# One case per finding of the 2026-10-01 Soul passes; each fails if it regresses.
mt() { # mt <mutants file> <TEST> [BUILD]: sets mt_out, mt_rc, mt_repo, mt_fd
local root; root=$(mktemp -d); mt_repo=$root/repo; mt_fd=$root/fd; mkdir -p "$mt_repo/pkg" "$root/mt" "$mt_fd"
cp -r "$here/fixtures/mutation-mock/." "$mt_repo/pkg/"; rm -rf "$mt_repo/pkg/fakebin"; cp "$L/jest-real.log" "$mt_repo/pkg/"
git -C "$mt_repo" init -q && git -C "$mt_repo" add -A && git -C "$mt_repo" -c user.name=t -c user.email=t@t commit -qm mock
cp "$here/testparse.sh" "$here/mutation-table.sh" "$root/mt/"; cp "$here/fixtures/mutation-mock/$1" "$root/mt/mutants.txt"
mt_out=$(PATH="$here/fixtures/mutation-mock/fakebin:$PATH" FAKE_DOCKER=$mt_fd YGG_WORK=$mt_repo YGG_IMAGE=base YGG_CNAME=job1 YGG_LABEL=eureka-verify-job=job1 \
YGG_CPUS=1 YGG_MEM=1g YGG_KILLED=${MT_KILLED:-} MT_BUNDLE=$root/mt WORKDIR=pkg TEST=$2 BUILD=${3:-} MUT_TIMEOUT=${MT_TO:-15} \
timeout "${MT_WALL:-120}" bash "$here/mutation-table.sh" --host 2>&1)
mt_rc=$?
}
mtcheck() { # mtcheck <name> <must-contain | !must-not-contain>...
local name=$1 s; shift
for s in "$@"; do
case $s in
'!'*) if printf '%s\n' "$mt_out" | grep -qF -- "${s#!}"; then echo "FAIL mutation $name: must not contain: ${s#!}"; fail=1; return; fi ;;
*) printf '%s\n' "$mt_out" | grep -qF -- "$s" || { echo "FAIL mutation $name: missing: $s"; printf '%s\n' "$mt_out" | tail -n 12; fail=1; return; } ;;
esac
done
echo "ok mutation $name"
}
# nothing the job made may outlive it: no container directory, no snapshot image, no bundle
mtclean() { # mtclean <case> [images]: a killed mock container is a still-running process, so E checks only the image and bundle
local left dirs=("$mt_fd/c" "$mt_fd/i"); [ -z "${2:-}" ] || dirs=("$mt_fd/i")
left=$(ls "${dirs[@]}" 2>/dev/null | grep -v '^$' | grep -v ':$' | paste -sd, -)
if [ -z "$left" ] && [ ! -e "$(dirname "$mt_repo")/mt" ]; then echo "ok mutation $1: containers, snapshot image and bundle removed"
else echo "FAIL mutation $1: left behind: ${left:-the bundle dir}"; fail=1; fi
}
# A: a mistyped BUILD must abort, not read as a fully killed suite
mt mutants-a.txt 'bash test.sh' 'bash bulid.sh'
[ "$mt_rc" = 2 ] || { echo "FAIL mutation A: exit $mt_rc, want 2"; fail=1; }
mtcheck A-build-baseline "BASELINE RED: BUILD failed" "!UNVIABLE" "!SNAPSHOT"
mtclean A
# B: a label that spells a kill must not forge one
mt mutants-b.txt 'bash test.sh'
[ "$mt_rc" = 1 ] || { echo "FAIL mutation B: exit $mt_rc, want 1"; fail=1; }
mtcheck B-label-forge "caught=0 survived=1" 'forge \| CAUGHT \| label | SURVIVED |'
mtclean B
# the nonce form ygg-verify.sh carries, and the plain form when no nonce is set
YGG_NONCE=n0nce1 mt mutants-b.txt 'bash test.sh'
mtcheck B-nonce "YGG-SUMMARY/n0nce1: [1/1] " "!YGG-SUMMARY: "
# C: a test that rewrites a tracked file, leaks an untracked one or keeps state in an
# IGNORED file (cache.txt: what `git status` cannot see) must not decide the next mutant
mt mutants-c.txt 'bash test.sh'
mtcheck C-pollution "stamp-bad | CAUGHT | tests::b_write" "untested-equiv | SURVIVED" "!untested-equiv | CAUGHT" "caught=1 survived=1"
[ -z "$(git -C "$mt_repo" status --porcelain --ignored)" ] || { echo "FAIL mutation C: the seed repository changed"; fail=1; }
mtclean C
# D: a test file that fails to load is not a kill; a named failing test is
mt mutants-d.txt 'bash vtest.sh'
mtcheck D-import-failure "syntax-break | UNVIABLE" "wrong-result | CAUGHT | add.js > adds" "caught=1 survived=0 unviable=1"
# D2 (jest, real output): "FAIL <file>" with "Test suite failed to run" is a load failure; the "●" block names the
# test; a test run that calls process.exit prints a "●" line under no FAIL header, which names nothing (ERROR, not CAUGHT)
mt mutants-h.txt 'bash jtest.sh'
mtcheck D2-jest "jest-load | UNVIABLE" "jest-wrong | CAUGHT | add › adds" "jest-exit | ERROR | exit 1, no failing test parsed" "!process.exit called | CAUGHT" "caught=1 survived=0 unviable=1 timeout=0 noapply=0 error=1"
# E: rows print as mutants finish, so a job timeout keeps them; the TERM the timeout sends still cleans up
MT_WALL=8 mt mutants-e.txt 'bash test.sh'
mtcheck E-live-rows "[1/2] quick | CAUGHT" "!MUTATION"
sleep 1; mtclean E images
# E2: a watchdog kill (the flag the stopgap sets) stops the fan-out after the baseline and cleans up
mt_k=$(mktemp); MT_KILLED=$mt_k mt mutants-c.txt 'bash test.sh'; rm -f "$mt_k"
[ "$mt_rc" = 124 ] || { echo "FAIL mutation E2: exit $mt_rc, want 124"; fail=1; }
mtcheck E2-killed "JOB KILLED by the watchdog before mutant 1 of 2" "!CAUGHT"
mtclean E2
# E3 (Soul, F1): a test that cannot run twice is green in the baseline and red in every mutant's
# container; the control run of the unmutated snapshot must abort the job, not report a killed suite
mt mutants-i.txt 'bash oncetest.sh'
[ "$mt_rc" = 2 ] || { echo "FAIL mutation E3: exit $mt_rc, want 2"; fail=1; }
mtcheck E3-snapshot-red "SNAPSHOT RED (exit 101) for: bash oncetest.sh" "no rows counted" "!CAUGHT" "!MUTATION"
mtclean E3
# F: LF before-text matches a CRLF file
mt mutants-f.txt 'bash test.sh'
mtcheck F-crlf "crlf-multiline | CAUGHT | tests::d_win"
# a real BUILD failure on a mutant is still UNVIABLE
mt mutants-g.txt 'bash test.sh' 'bash build.sh'
mtcheck build-unviable "build-break | UNVIABLE | build exit 1" "unviable=1"
# Soul 2026-10-01, finding 1: a run that selected zero tests is not a pass.
check "$(seg soul-s5/pdotnet.log zero)" 0 "ZERO TESTS RAN" "no test counts recognised"
check "$(seg soul-s5/ppy.log zero)" 0 "ZERO TESTS RAN" "no test counts recognised"
check "$(seg soul-s5/ppy.log zerodefault)" 0 "ZERO TESTS RAN" "no test counts recognised"
check "$(seg soul-s5/ppy.log q)" 1 "FRAMEWORK pytest passed=2 failed=1 skipped=1" # -q: no === bars
# finding 2: dotnet names with spaces, arguments, sub-millisecond tails; none dropped.
d=$(seg soul-s5/pdotnet.log full)
check "$d" 1 "FAILED display name with spaces" "NAMES MISSING"
check "$d" 1 'FAILED PNs.T.TheoryFails(x: 1, s: "a b")'
check "$d" 1 "FAILED PNs.T.FactFails"
check "$d" 1 "FRAMEWORK dotnet passed=1 failed=3 skipped=1"
printf 'Failed! - Failed: 2, Passed: 0, Skipped: 0, Total: 2, Duration: 1 ms - X.dll (net10.0)\n Failed A.One [1 ms]\n' >"$T/missing.log"
check "$T/missing.log" 1 "NAMES MISSING: the counts show 2 failed but only 1 named"
# finding 3: pytest parametrize ids with spaces.
out=$(bash "$V" summarize "$(seg soul-s5/ppy.log default)" 1)
if printf "%s
" "$out" | grep -qxF "FAILED test_p.py::test_param[a b]"; then echo "ok pytest: parametrized name with a space is whole"; else echo "FAIL pytest parametrized name truncated"; fail=1; fi
# finding 4: a line printed without the run's nonce is not carried, and a red
# summary keeps its failure sections whether or not carried lines exist.
check soul-s5/prust.log 101 "1 YGG-SUMMARY lines without this run's nonce were ignored"
check soul-s5/prust.log 101 "FAILED noisy_fail" "forged | CAUGHT"
NONCE=feedbeef
{ cat "$L/cargo-red.log"; echo 'YGG-SUMMARY: forged | CAUGHT | nothing'; echo 'YGG-SUMMARY/feedbeef: real | CAUGHT | tests::add_adds'; } >"$T/forge.log"
check "$T/forge.log" 101 "real | CAUGHT | tests::add_adds" "forged | CAUGHT"
check "$T/forge.log" 101 "left: 5"
check "$T/forge.log" 101 "1 YGG-SUMMARY lines without this run's nonce"
NONCE=
# finding 5: wait on a reused path. Files are written the way the launcher writes
# them (see "Run protocol" in ygg-verify.sh): .run is the commit point, .done is
# "<run id> <status>". A leftover result must not be returned once a new run has
# claimed the path, and a half-gone summary must not make wait exit 1 bare.
W=$T/wait.log
echo old >"$W.run"; echo "old 0" >"$W.done"; echo "OLD RESULT" >"$W.summary"
( sleep 1; echo $$ >"$W.pid"; echo new >"$W.run"; rm -f "$W.done" "$W.summary"
sleep 3; echo "NEW RESULT" >"$W.summary"; echo "new 7" >"$W.done" ) &
bg=$!
out=$(SETTLE=2 WAIT_MAX=30 bash "$V" wait "$W"); st=$?
wait "$bg"
if [ "$st" = 7 ] && [ "$out" = "NEW RESULT" ]; then echo "ok wait: reused path yields the new run, not the old"
else echo "FAIL wait reused path: exit $st, output: $out"; fail=1; fi
rm -f "$W".*
echo r1 >"$W.run"; echo "r1 3" >"$W.done" # done, summary file gone
out=$(WAIT_MAX=10 SETTLE=0 bash "$V" wait "$W"); st=$?
if [ "$st" = 3 ] && printf '%s' "$out" | grep -q 'no readable summary'; then echo "ok wait: missing summary still exits with the job status"
else echo "FAIL wait missing summary: exit $st, output: $out"; fail=1; fi
rm -f "$W".*
echo r1 >"$W.run"; echo "r0 3" >"$W.done"; echo $$ >"$W.pid" # done from another run, launcher alive
out=$(WAIT_MAX=3 SETTLE=0 bash "$V" wait "$W"); st=$?
if [ "$st" = 99 ]; then echo "ok wait: a done from another run is not a result"
else echo "FAIL wait foreign done: exit $st, output: $out"; fail=1; fi
# N2 (Soul, 2026-10-01): file-level failures are not named test failures, whichever runner
# prints them. Vitest with a project prefix and with spaces in the path; jest's file header.
check vitest-project-load-failure.log 1 "FAILED TO LOAD |unit| src/add.test.ts" "FAILED |unit|"
check vitest-spaces-load-failure.log 1 "FAILED TO LOAD src/my tests/add.test.ts" "FAILED src/my"
check vitest-project-red.log 1 "FAILED |unit| src/add.test.ts > add > carries" "FAILED TO LOAD"
# jest, real output (jest-real.log): one space after FAIL, no TTY. Names come from the "●" lines under a FAIL header only.
check "$(seg jest-real.log green)" 0 "FRAMEWORK jest passed=2 failed=0 skipped=0" "no test counts recognised"
check "$(seg jest-real.log wrong)" 1 "FRAMEWORK jest passed=1 failed=1 skipped=0"
check "$(seg jest-real.log wrong)" 1 "FAILED add › adds" "FAILED TO LOAD"
check "$(seg jest-real.log load)" 1 "FAILED TO LOAD jt/add.test.js" "FAILED jt/add.test.js"
check "$(seg jest-real.log load)" 1 "FRAMEWORK jest passed=0 failed=0 skipped=0" "FAILED Test suite"
# F3: process.exit(1) in a test run prints a "●" line under no FAIL header; it is a crash, not a named kill
check "$(seg jest-real.log exit)" 1 "exit non-zero with no failing test parsed" "FAILED process.exit"
# F4: the "nothing recognised" line names every framework the parser reads
check "$(seg jest-real.log exit)" 1 "no test counts recognised in the log (cargo, dotnet, vitest, jest, pytest)"
# ---- tools/cargo-mutants.sh: the launcher, host driver and container halves, through the mock docker
# and fixtures/mutation-mock/fakebin/cargo (a fake `cargo mutants` driven by a table of mutant ->
# outcome under a filtered run and under the full suite). The launcher runs for real: its bundle,
# quoting and the environment it hands the driver are the production path; only ygg-verify.sh is
# stubbed, by a script that does what RUN_ON_HOST=1 does (run the command inside the checked-out
# revision). Slot locks are real flock(2) locks on files, taken by the driver and by the test.
CMB=$here/fixtures/mutation-mock/fakebin
cmrepo() { # cmrepo A|B|C: sets cm_repo and cm_wd. A: a workspace at the repo root, workdir crates/x. B: a standalone package pkg/ under the repo root. C: B touching only the two files that have tests.
local r f c; r=$(mktemp -d); cm_repo=$r/repo; mkdir -p "$cm_repo"; git -C "$cm_repo" init -q
c="git -C $cm_repo -c user.name=t -c user.email=t@t"
if [ "$1" = A ]; then
cm_wd=crates/x; mkdir -p "$cm_repo/crates/x/src/alpha" "$cm_repo/crates/x/src/bin"
printf '[workspace]\nmembers = ["crates/x"]\n' >"$cm_repo/Cargo.toml"; printf '[package]\nname = "x"\n' >"$cm_repo/crates/x/Cargo.toml"
: >"$cm_repo/crates/x/src/lib.rs"; $c add -A 2>/dev/null; $c commit -qm c1
for f in src/lib.rs src/main.rs src/gamma.rs src/beta.rs src/alpha/mod.rs src/alpha/inner.rs src/bin/tool.rs build.rs; do echo "// $f" >"$cm_repo/crates/x/$f"; done
else
cm_wd=pkg; mkdir -p "$cm_repo/pkg/src/a" "$cm_repo/pkg/src/bin"
printf '[package]\nname = "p"\n' >"$cm_repo/pkg/Cargo.toml"; : >"$cm_repo/pkg/src/lib.rs"; $c add -A 2>/dev/null; $c commit -qm c1
for f in src/a/mod.rs src/a/b.rs; do echo "// $f" >"$cm_repo/pkg/$f"; done
[ "$1" = C ] || for f in src/lib.rs src/main.rs src/bin/x.rs build.rs; do echo "// $f" >"$cm_repo/pkg/$f"; done
fi
$c add -A 2>/dev/null; $c commit -qm c2
}
cmtable() { # cmtable A|B|C: sets cm_table (name <TAB> outcome under a filtered run <TAB> outcome under the full suite)
cm_table=$(mktemp)
if [ "$1" = A ]; then
local p=crates/x/src
printf '%s\n' \
"$p/alpha/mod.rs:2:5: replace add -> i32 with 0 caught caught" \
"$p/alpha/mod.rs:7:7: replace * with + in double missed caught" \
"$p/alpha/mod.rs:9:9: replace && with || in check missed missed" \
"$p/alpha/mod.rs:12:5: replace f(x) -> Vec<i32> with vec![1] caught caught" \
"$p/alpha/mod.rs:15:5: replace id -> Option<i32> with None unviable unviable" \
"$p/alpha/inner.rs:3:5: replace slow -> u32 with 0 timeout caught" \
"$p/alpha/inner.rs:6:5: replace spin -> u32 with 1 missed timeout" \
"$p/beta.rs:2:5: replace b1 -> i32 with 0 missed caught" \
"$p/beta.rs:3:5: replace b2 -> i32 with 0 missed missed" \
"$p/beta.rs:4:5: replace b3 -> i32 with 0 missed unviable" \
"$p/gamma.rs:2:5: replace g -> i32 with 0 caught caught" \
"$p/lib.rs:2:5: replace l1 -> i32 with 0 missed caught" \
"$p/lib.rs:3:5: replace f(x) -> Vec<i32> with vec![1] missed missed" \
"$p/main.rs:2:5: replace m -> i32 with 0 missed caught" \
"crates/x/build.rs:2:5: replace bu -> i32 with 0 missed caught" \
"$p/bin/tool.rs:2:5: replace t -> i32 with 0 missed caught" >"$cm_table"
elif [ "$1" = C ]; then
printf '%s\n' "src/a/mod.rs:2:5: replace a -> i32 with 0 caught caught" "src/a/b.rs:2:5: replace a -> i32 with 0 caught caught" >"$cm_table"
else
printf '%s\n' "src/lib.rs:2:5: replace a -> i32 with 0 caught caught" "src/main.rs:2:5: replace a -> i32 with 0 caught caught" \
"src/bin/x.rs:2:5: replace a -> i32 with 0 caught caught" "build.rs:2:5: replace a -> i32 with 0 caught caught" \
"src/a/mod.rs:2:5: replace a -> i32 with 0 caught caught" "src/a/b.rs:2:5: replace a -> i32 with 0 caught caught" >"$cm_table"
fi
}
# cm <A|B> [VAR=value]...: runs the launcher; sets cm_out, cm_rc, cm_root, cm_fd, cm_log. HOLD_OWN=1 holds the job's own slot as
# ygg-verify does (a descriptor the driver inherits); HOLD_OTHERS=<n> holds n further slots as other jobs would (separate processes); PSI=<n> is the io pressure file's full avg60.
cm() {
local lay=$1; shift
cmrepo "$lay"; cmtable "$lay"
cm_root=$(dirname "$cm_repo"); cm_fd=$cm_root/fd; cm_log=$cm_root/cargo.log
mkdir -p "$cm_fd" "$cm_root/tools/stopgap" "$cm_root/ygg" "$cm_root/tmp"
cp "$here/cargo-mutants.sh" "$cm_root/tools/"
printf '#!/usr/bin/env bash\n# test stub of ygg-verify.sh RUN_ON_HOST=1: <repo> <rev> <image> <cmd>; runs cmd as the host half inside the revision\ncd "$YGG_WORK" && exec bash -o pipefail -c "$4"\n' >"$cm_root/tools/stopgap/ygg-verify.sh"
local holders=() i n own=0 others=0 psi=0.00 kv env=()
for kv in "$@"; do case $kv in HOLD_OWN=*) own=${kv#*=} ;; HOLD_OTHERS=*) others=${kv#*=} ;; PSI=*) psi=${kv#*=} ;; *) env+=("$kv") ;; esac; done
for i in $(seq 1 "$others"); do
perl -e 'use Fcntl ":flock"; open(F, ">>", $ARGV[0]) or die; flock(F, LOCK_EX) or die; open(R, ">", "$ARGV[0].ready"); close R; sleep 120' "$cm_root/ygg/slot-$((i + 1)).lock" & holders+=($!)
for n in $(seq 1 50); do [ -e "$cm_root/ygg/slot-$((i + 1)).lock.ready" ] && break; sleep 0.1; done
done
cm_out=$( ( if [ "$own" = 1 ]; then exec {ownfd}>"$cm_root/ygg/slot-1.lock"; PATH=$CMB:$PATH flock -n "$ownfd"; fi
export PATH=$CMB:$PATH FAKE_DOCKER=$cm_fd FAKE_CM_TABLE=$cm_table FAKE_CM_LOG=$cm_log FAKE_CM_TESTS=$cm_root/tests.tsv \
FAKE_WSROOT=$([ "$lay" = A ] || echo pkg/) YGG_WORK=$cm_repo YGG_IMAGE=rust YGG_CNAME=job1 YGG_LABEL=eureka-verify-job=job1 YGG_CPUS=1 YGG_MEM=1g \
YGG_SLOTS=5 YGG_ROOT=$cm_root/ygg YGG_NONCE=n0nce1 CM_PSI=$cm_root/psi TMPDIR=$cm_root/tmp
printf 'beta::\t0\n' >"$cm_root/tests.tsv"
printf 'some avg10=0.00 avg60=0.00 avg300=0.00 total=0\nfull avg10=0.00 avg60=%s avg300=0.00 total=0\n' "$psi" >"$cm_root/psi"
for kv in "${env[@]}"; do export "${kv?}"; done
timeout 240 bash "$cm_root/tools/cargo-mutants.sh" "$cm_repo" HEAD HEAD~1 "$cm_wd" 2>&1 ) )
cm_rc=$?
for i in "${holders[@]}"; do kill "$i" 2>/dev/null; done; wait 2>/dev/null
}
cmcheck() { # cmcheck <name> <must-contain | !must-not-contain>...
local name=$1 s; shift
for s in "$@"; do
case $s in
'!'*) if printf '%s\n' "$cm_out" | grep -qF -- "${s#!}"; then echo "FAIL cargo-mutants $name: must not contain: ${s#!}"; printf '%s\n' "$cm_out" | tail -n 12; fail=1; return; fi ;;
*) printf '%s\n' "$cm_out" | grep -qF -- "$s" || { echo "FAIL cargo-mutants $name: missing: $s"; printf '%s\n' "$cm_out" | tail -n 14; fail=1; return; } ;;
esac
done
echo "ok cargo-mutants $name"
}
cmrc() { [ "$cm_rc" = "$2" ] || { echo "FAIL cargo-mutants $1: exit $cm_rc, want $2"; printf '%s\n' "$cm_out" | tail -n 8; fail=1; }; }
cmclean() { # nothing the job made may outlive it: no container directory, no bundle, no staging directory
local left; left=$(ls "$cm_fd/c" "$cm_root/tmp" 2>/dev/null | grep -v '^$' | grep -v ':$' | paste -sd, -)
if [ -z "$left" ]; then echo "ok cargo-mutants $1: containers, bundle and staging removed"; else echo "FAIL cargo-mutants $1: left behind: $left"; fail=1; fi
}
cmlocks() { # cmlocks <case>: after the driver exits, every slot lock can be taken again by a fresh process
local i bad=''
for i in 1 2 3 4 5; do bash -c "exec {fd}>'$cm_root/ygg/slot-$i.lock'; PATH=$CMB:\$PATH flock -n \$fd" || bad="$bad $i"; done
if [ -z "$bad" ]; then echo "ok cargo-mutants $1: every slot lock is free again"; else echo "FAIL cargo-mutants $1: slots still locked:$bad"; fail=1; fi
}
# filter derivation, stage-1 survivors and timeouts confirmed, zero-test filter, no raw stage-1 lines, exit 2 and 3 normal
cm A TEST_ARGS='--skip flaky'
cmrc A 0
cmcheck A-filters \
'FILTER crates/x/src/alpha/mod.rs alpha:: tests=1 mutants=5' 'FILTER crates/x/src/alpha/inner.rs alpha::inner:: tests=1 mutants=2' \
'FILTER crates/x/src/gamma.rs gamma:: tests=1 mutants=1' \
'!FILTER crates/x/src/lib.rs' '!FILTER crates/x/src/main.rs' '!FILTER crates/x/src/bin/tool.rs' '!FILTER crates/x/build.rs'
cmcheck A-zero-test-filter 'FILTER crates/x/src/beta.rs beta:: tests=0 mutants=3' 'caught=10 missed=3 timeout=1 unviable=2'
cmcheck A-confirmed-and-counted 'MUTANTS tested=16 caught=10 missed=3 timeout=1 unviable=2 | stage1 filtered=8 survivors=4 confirmed_caught=2 | containers=3 |' \
'!replace * with + in double' '!replace slow -> u32 with 0' '!replace add -> i32' \
'MISSED crates/x/src/alpha/mod.rs:9:9: replace && with || in check' 'MISSED crates/x/src/beta.rs:3:5: replace b2' 'MISSED crates/x/src/lib.rs:3:5: replace f(x) -> Vec<i32> with vec![1]' \
'TIMEOUT crates/x/src/alpha/inner.rs:6:5: replace spin'
if [ "$(printf '%s\n' "$cm_out" | grep -c "^YGG-SUMMARY/n0nce1: MISSED ")" = 3 ]; then echo "ok cargo-mutants A-missed-once: 3 final MISSED lines"; else echo "FAIL cargo-mutants A-missed-once"; fail=1; fi
if printf '%s\n' "$cm_out" | grep -qE '^(MISSED|TIMEOUT) '; then echo "FAIL cargo-mutants A-no-raw-lines: a MISSED or TIMEOUT line outside the nonce-tagged lines"; fail=1; else echo "ok cargo-mutants A-no-raw-lines"; fi
if grep -qF 'stage=1 filter=alpha:: harness=--skip flaky' "$cm_log" && grep -qF 'stage=1 filter=alpha::inner:: harness=--skip flaky' "$cm_log" && grep -qF 'stage=2 filter= harness=--skip flaky' "$cm_log"
then echo "ok cargo-mutants A-harness-args: TEST_ARGS follow the filter in stage 1 and stand alone in stage 2"; else echo "FAIL cargo-mutants A-harness-args"; sort -u "$cm_log"; fail=1; fi
if [ "$(grep -c '^stage=2' "$cm_log")" = 3 ]; then echo "ok cargo-mutants A-stage2-sharded: 3 full-suite shards"; else echo "FAIL cargo-mutants A-stage2-sharded"; fail=1; fi
# a file whose filter selects 0 tests is found at plan time and never pays a stage-1 pass; its names run in stage 2
if grep -qF 'count filter=beta:: harness=--skip flaky' "$cm_log" && ! grep -q '^stage=1 filter=beta::' "$cm_log"
then echo "ok cargo-mutants A-zero-test-plan: counted at plan time, no stage-1 run for it"; else echo "FAIL cargo-mutants A-zero-test-plan"; sort -u "$cm_log"; fail=1; fi
# the stage-2 timeout bound is the generous default with a floor, not a tight multiple of a quiet-host baseline
if [ "$(grep -c '^stage=2 .* mult=5 min=20$' "$cm_log")" = 3 ] && ! grep '^stage=2' "$cm_log" | grep -qv 'mult=5 min=20$'
then echo "ok cargo-mutants A-timeout-bound: stage 2 runs at 5x with a 20 s floor"; else echo "FAIL cargo-mutants A-timeout-bound"; grep '^stage=2' "$cm_log"; fail=1; fi
cmclean A; cmlocks A
# derivation on a standalone package under the repo root: the diff is taken at the workspace root (--relative)
cm B
cmrc B 0
cmcheck B-derivation 'FILTER src/a/mod.rs a:: tests=1 mutants=1' 'FILTER src/a/b.rs a::b:: tests=1 mutants=1' \
'!FILTER src/lib.rs' '!FILTER src/main.rs' '!FILTER src/bin/x.rs' '!FILTER build.rs' 'MUTANTS tested=6 caught=6 missed=0'
# FILTERS overrides the derived filter; '-' forces the full suite
cm B 'FILTERS=src/a/b.rs=-,src/lib.rs=special::'
cmcheck B-override 'FILTER src/lib.rs special:: tests=1 mutants=1' 'FILTER src/a/mod.rs a:: ' '!FILTER src/a/b.rs' 'tested=6 caught=6'
# stage 1 settles every mutant: the full-suite baseline still runs (stage 2 is never skipped), and a red one is a red job
cm C
cmrc C-settled 0
cmcheck C-settled 'MUTANTS tested=2 caught=2 missed=0 timeout=0 unviable=0 | stage1 filtered=2 survivors=0 confirmed_caught=0'
if [ "$(grep -c '^stage=2' "$cm_log")" = 1 ]; then echo "ok cargo-mutants C-baseline-runs: one full-suite run although stage 1 settled every mutant"; else echo "FAIL cargo-mutants C-baseline-runs"; sort -u "$cm_log"; fail=1; fi
cm C FAKE_CM_RED=2
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants C-red-baseline: exit 0"; fail=1; }
cmcheck C-red-baseline 'ERROR stage 2 (shard 1 of 1)' 'container exited 4' '!MUTANTS'
cmclean C-red-baseline
# CONFIRM_TIMEOUT_MULT and MIN_TEST_TIMEOUT reach the stage-2 run
cm A CONFIRM_TIMEOUT_MULT=9 MIN_TEST_TIMEOUT=33
if grep '^stage=2' "$cm_log" | grep -q 'mult=9 min=33$' && ! grep '^stage=2' "$cm_log" | grep -qv 'mult=9 min=33$'; then echo "ok cargo-mutants timeout-overrides: 9x with a 33 s floor in stage 2"; else echo "FAIL cargo-mutants timeout-overrides"; grep '^stage=2' "$cm_log"; fail=1; fi
# slot lease: the job's own slot is held by ygg-verify (an inherited descriptor), others by other jobs
cm A HOLD_OWN=1 HOLD_OTHERS=1; cmcheck lease-free 'containers=3 |'; cmlocks lease-free
cm A HOLD_OWN=1 HOLD_OTHERS=3; cmcheck lease-one-free 'containers=2 |'
cm A HOLD_OWN=1 HOLD_OTHERS=4; cmcheck lease-none-free 'containers=1 |' 'tested=16'; cmlocks lease-none-free
open_none=$(grep -o 'openfds=[0-9]*' "$cm_log" | sort -u | paste -sd, -)
cm A HOLD_OWN=1 HOLD_OTHERS=1; open_leased=$(grep -o 'openfds=[0-9]*' "$cm_log" | sort -u | paste -sd, -)
if [ "$open_none" = "$open_leased" ]; then echo "ok cargo-mutants lease-not-inherited: containers see the same descriptors with 2 leases held as with none ($open_none)"; else echo "FAIL cargo-mutants lease-not-inherited: $open_none vs $open_leased"; fail=1; fi
cm A PSI=41.00; cmcheck lease-io-pressure 'containers=1 |' 'tested=16'
# a red baseline, in stage 1 or in stage 2, is not a clean run
cm A FAKE_CM_RED=1
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants red-stage1: exit 0"; fail=1; }
cmcheck red-stage1 'ERROR stage 1 (' 'container exited 4' '!MUTANTS' '!MISSED' '!TIMEOUT'
cm A FAKE_CM_RED=2
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants red-stage2: exit 0"; fail=1; }
cmcheck red-stage2 'ERROR stage 2 (shard' 'container exited 4' '!MUTANTS' '!MISSED' '!TIMEOUT'
cmclean red-stage2
# NAMES rerun: no stage-1 container, only the listed names, on the full suite
cm_names=$(mktemp)
printf '%s\n' 'crates/x/src/alpha/mod.rs:9:9: replace && with || in check' 'crates/x/src/lib.rs:3:5: replace f(x) -> Vec<i32> with vec![1]' 'crates/x/src/beta.rs:2:5: replace b1 -> i32 with 0' >"$cm_names"
cm A "NAMES=$cm_names"
cmrc names 0
cmcheck names 'MUTANTS tested=3 caught=1 missed=2 timeout=0 unviable=0 | stage1 filtered=0 survivors=0 confirmed_caught=0' 'MISSED crates/x/src/alpha/mod.rs:9:9' '!FILTER'
if grep -q 'run job1-s1' "$cm_fd/log" || grep -q '^stage=1' "$cm_log"; then echo "FAIL cargo-mutants names: a stage-1 container or a filtered run"; fail=1; else echo "ok cargo-mutants names: no stage-1 container"; fi
# a name the range does not produce is an error that does not echo the name
printf '%s\n' 'crates/x/src/lib.rs:99:9: replace no_such_secret_name -> i32 with 0' >"$cm_names"
cm A "NAMES=$cm_names"
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants names-unknown: exit 0"; fail=1; }
cmcheck names-unknown 'ERROR 1 mutants selected' '!no_such_secret_name' '!MUTANTS'
rm -f "$cm_names"
# killed: a job the watchdog already killed starts nothing and leaves nothing
cm_k=$(mktemp); cm A "YGG_KILLED=$cm_k"; rm -f "$cm_k"
cmrc killed-before 124
if grep -q '^run ' "$cm_fd/log" 2>/dev/null; then echo "FAIL cargo-mutants killed-before: a container started"; fail=1; else echo "ok cargo-mutants killed-before: no container started"; fi
cmclean killed-before
# the flag appears while stage 1 runs: stage 2 never starts
cm_k=$(mktemp -u); cm A "YGG_KILLED=$cm_k" "FAKE_CM_KILLS=$cm_k"; rm -f "$cm_k"
cmrc killed-during 124
cmcheck killed-during 'JOB KILLED' '!MUTANTS'
if grep -q 'run job1-s2' "$cm_fd/log"; then echo "FAIL cargo-mutants killed-during: a stage-2 container started"; fail=1; else echo "ok cargo-mutants killed-during: no stage-2 container"; fi
cmclean killed-during
# the launcher refuses bad input before any job starts, and never echoes the value it refused
cmrepo A
for v in 'SHARDS=0' 'SHARDS=x;SECRET' 'FILTERS=nopairSECRET' 'CONFIRM_TIMEOUT_MULT=2;SECRET'; do
lo=$(env "$v" bash "$here/cargo-mutants.sh" "$cm_repo" HEAD HEAD~1 "$cm_wd" 2>&1); lrc=$?
if [ "$lrc" = 2 ] && ! printf '%s' "$lo" | grep -q SECRET; then echo "ok cargo-mutants launcher refuses ${v%%=*}"; else echo "FAIL cargo-mutants launcher ${v%%=*}: exit $lrc, output: $lo"; fail=1; fi
done
lo=$(bash "$here/cargo-mutants.sh" "$cm_repo" HEAD~1 HEAD "$cm_wd" 2>&1); lrc=$?
if [ "$lrc" = 2 ] && printf '%s' "$lo" | grep -q 'not an ancestor'; then echo "ok cargo-mutants launcher refuses a base that is not an ancestor"; else echo "FAIL cargo-mutants launcher base: exit $lrc, output: $lo"; fail=1; fi
# the -F escaper: every regex metacharacter, the pipe included, is escaped; nothing else is touched
esc_bad=''
for c in '\' . + '*' '?' '(' ')' '|' '[' ']' '{' '}' '^' '$'; do
got=$(printf 'a%sb\n' "$c" | bash "$here/cargo-mutants.sh" --escape)
[ "$got" = '^a'\\"$c"'b$' ] || esc_bad="$esc_bad [$c]"
done
got=$(printf 'replace x -> Vec<i32> with vec![1] & 2 - 3 #\n' | bash "$here/cargo-mutants.sh" --escape)
[ "$got" = '^replace x -> Vec<i32> with vec!\[1\] & 2 - 3 #$' ] || esc_bad="$esc_bad [plain]"
if [ -z "$esc_bad" ]; then echo "ok cargo-mutants escaper: all 14 metacharacters escaped, others literal"; else echo "FAIL cargo-mutants escaper:$esc_bad"; fail=1; fi
# wait-on.sh: pid, log and file forms, and the vanished-process exit
W=$here/wait-on.sh
wo() { local rc; WAIT_MAX=${WM:-4} bash "$W" "$@" >"$T/wo.out" 2>&1; rc=$?; echo "$rc"; }
wchk() { if [ "$3" = "$2" ]; then echo "ok wait-on $1: exit $3"; else echo "FAIL wait-on $1: exit $3, want $2"; fail=1; fi; }
sleep 3 & sp=$!; printf 'one\ntwo\n' >"$T/w.log"
wchk "pid finishes" 0 "$(WM=10 wo pid $sp "$T/w.log")"
sleep 20 & sp=$!
r=$(WM=2 wo pid $sp); wchk "pid still running" 99 "$r"
grep -q '^RUNNING ' "$T/wo.out" || { echo "FAIL wait-on: no RUNNING line"; fail=1; }
kill $sp 2>/dev/null; wait $sp 2>/dev/null
r=$(WM=10 wo pid $sp "$T/w.log"); wchk "killed pid is done" 0 "$r"
grep -q two "$T/wo.out" || { echo "FAIL wait-on pid: no log tail"; fail=1; }
: >"$T/l.log"; ( sleep 2; echo "step 1" >>"$T/l.log"; echo "all done ok" >>"$T/l.log" ) &
r=$(WM=10 wo log "$T/l.log" 'done ok'); wchk "log gains its done line" 0 "$r"
grep -q 'all done ok' "$T/wo.out" || { echo "FAIL wait-on log: no tail"; fail=1; }
r=$(WM=2 wo log "$T/l.log" 'never-appears'); wchk "log never matches" 99 "$r"
printf 'partial\n' >"$T/v.log"; sleep 1 & vp=$!; wait $vp
r=$(WAIT_PID=$vp WM=10 wo log "$T/v.log" 'FINISHED'); wchk "vanished pid, no completion" 98 "$r"
printf 'FINISHED\n' >>"$T/v.log"
r=$(WAIT_PID=$vp WM=10 wo log "$T/v.log" 'FINISHED'); wchk "vanished pid, completion logged" 0 "$r"
r=$(WM=2 wo log "$T/none.log" 'x'); wchk "log missing, no pid, short cap" 99 "$r"
( sleep 2; echo ready >"$T/f.txt" ) &
r=$(WM=10 wo file "$T/f.txt"); wchk "file appears" 0 "$r"
: >"$T/e.txt"; r=$(WM=2 wo file "$T/e.txt"); wchk "file empty is not done" 99 "$r"
# F2: the pid form reports GONE, never anything that reads as success
sleep 1 & gp=$!; wait $gp
r=$(WM=4 wo pid $gp); wchk "pid gone" 0 "$r"
grep -q '^GONE (no completion evidence)' "$T/wo.out" || { echo "FAIL wait-on pid: no GONE line"; fail=1; }
# F4: an invalid ERE is a usage error, not a forever RUNNING
r=$(WM=2 wo log "$T/l.log" '(unclosed'); wchk "invalid regex" 3 "$r"
grep -q 'not a valid extended regex' "$T/wo.out" || { echo "FAIL wait-on: no regex message"; fail=1; }
r=$(wo bogus); wchk "bad usage" 2 "$r"
wait
# context-pack.py: a spec's anchors carried from its base to the worktree (no network)
CP=$here/context-pack.py; CF=$here/fixtures/context-pack
if py -3 -c pass >/dev/null 2>&1; then PY="py -3"; elif python3 -c pass >/dev/null 2>&1; then PY=python3; else PY=python; fi
wrapcp() { $PY -c 'import sys,json; v=json.load(sys.stdin); v=v if "kind" in v else {"kind":"cut_spec","value":dict(campaign="t",cut="x",revision=1,**v)}; json.dump(v,sys.stdout)'; }
wp() { cygpath -m "$1" 2>/dev/null || echo "$1"; }
R=$T/cp; mkdir -p "$R"; Rw=$(wp "$R")
g() { git -C "$R" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
seqf() { local i; for i in $(seq 1 "$2"); do echo "$1$i"; done; }
g init -q
seqf a 40 >"$R/a.txt"; seqf b 40 >"$R/b.txt"; seqf c 10 >"$R/c.txt"; seqf d 20 >"$R/d.txt"
seqf e 10 >"$R/e.txt"; seqf f 20 >"$R/f.txt"; seqf g 300 >"$R/big.txt"; seqf h 5 | sed 's/$/
/' >"$R/h.txt"; seqf m 40 >"$R/m.txt"
printf 's1\ns2\ns3\ns4\ndef needle_three\ns6\ns7 needle_many\ns8\nfoo::bar\ns10\n' >"$R/sym.txt"
printf 'needle_three call 1\nneedle_three call 2\nneedle_three call 3\nneedle_threefold is another word\n' >"$R/zcallers.txt"
echo 'bound_fifty bound_fiftyone spread_sym long_symbol q.zq sym_bad twenty_sym' >"$R/anchors.txt"
seqf "bound_fifty x" 50 >"$R/bound50.txt"; seqf "bound_fiftyone x" 51 >"$R/bound51.txt"; seqf "q.zq x" 51 >"$R/dotted.txt"
printf 'long_symbol %s%s\r' "$(printf '%0150d' 0 | tr 0 z)" "$(printf '%0100d' 0 | tr 0 y)" >"$R/longline.txt"; printf '\n' >>"$R/longline.txt"
printf 'sym_bad \377 endX\r\n' >"$R/bad.txt"
mkdir -p "$R/manyf"; for i in $(seq -w 1 21); do seqf "spread_sym $i-" 3 >"$R/manyf/f$i.txt"; done; echo "spread_sym extra" >>"$R/manyf/f21.txt"
mkdir -p "$R/twf"; for i in $(seq -w 1 20); do seqf "twenty_sym $i-" 3 >"$R/twf/f$i.txt"; done
seqf "needle_many x." 30 >"$R/many1.txt"; seqf "needle_many y." 30 >"$R/many2.txt"
g add -A; g commit -q -m base; BASE=$(git -C "$R" rev-parse HEAD)
{ sed -n 1,5p "$R/a.txt"; printf 'ins1\nins2\nins3\n'; sed -n '6,$p' "$R/a.txt"; } >"$R/a.new"; mv "$R/a.new" "$R/a.txt"
sed -e '2,3d' -e 's/^b10$/b10-EDITED/' "$R/b.txt" >"$R/b.new"; mv "$R/b.new" "$R/b.txt"
sed -e '10,14c\X1\nX2' -e '30,31d' "$R/m.txt" >"$R/m.new"; mv "$R/m.new" "$R/m.txt"
g rm -q c.txt; g mv d.txt d2.txt; g add -A; g commit -q -m second
sed -i 's/^f5$/f5-UNCOMMITTED/' "$R/f.txt"; printf 'n1\nn2\nn3\n' >"$R/n.txt"
sed "s/\"BASE\"/\"$BASE\"/" "$CF/spec.json" | wrapcp >"$T/cp-spec.json"; SP=$(wp "$T/cp-spec.json")
cpo() { local s=$1; shift; $PY "$CP" "$s" "$Rw" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
cchk() { if [ "$3" = "$2" ]; then echo "ok context-pack $1: exit $3"; else echo "FAIL context-pack $1: exit $3, want $2"; fail=1; fi; }
cin() { grep -qF -- "$2" "$T/cp.out" && echo "ok context-pack $1" || { echo "FAIL context-pack $1: missing: $2"; fail=1; }; }
cnot() { if grep -qF -- "$2" "$T/cp.out" "$T/cp.err"; then echo "FAIL context-pack $1: must not contain: $2"; fail=1; else echo "ok context-pack $1"; fi; }
r=$(cpo "$SP"); cchk "fixture (GONE and OUT_OF_RANGE)" 3 "$r"
cin "header" "context-pack t cut-x.r1 base ${BASE:0:12} HEAD"
cin "header counts modified files" " modified 6"
cin "MOVED(+3)" "[1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
cin "insertion at the last line is outside" "[2] a.txt:3-5 SAME -> a.txt:3-5"
cin "insertion before the range shifts it" "[3] a.txt:6-7 MOVED(+3) -> a.txt:9-10"
cin "insertion inside widens" "[4] a.txt:4-8 TOUCHED(+3/-0 inside) -> a.txt:4-11"
cin "delete above, edit inside" "[5] b.txt:8-12 TOUCHED(+1/-1 inside) -> b.txt:6-10"
cin "MOVED(-2)" "[6] b.txt:30-31 MOVED(-2) -> b.txt:28-29"
cin "GONE" "[7] c.txt:2-3 GONE (not named by the spec's deletes)"
cin "RENAMED" "[8] d.txt:4-5 RENAMED -> d2.txt SAME -> d2.txt:4-5"
cin "NEW with basename candidates" "[9] sub/a.txt:1-2 NEW (at base: a.txt)"
cin "OUT_OF_RANGE" "[10] e.txt:5-50 OUT_OF_RANGE (base file has 10 lines)"
cin "uncommitted edit is TOUCHED" "[11] f.txt:5-6 TOUCHED(+1/-1 inside)"
cin "last line of the file is in range" "[12] e.txt:10-10 SAME"
cin "one past the last line is out of range" "[13] e.txt:10-11 OUT_OF_RANGE (base file has 10 lines)"
cin "CRLF file reads SAME" "[14] h.txt:2-3 SAME"
cin "hunk straddling the head clamps to the new block" "[15] m.txt:12-13 TOUCHED(+2/-5 inside) -> m.txt:10-11"
cin "hunk starting inside and ending past the range" "[16] m.txt:8-11 TOUCHED(+2/-5 inside) -> m.txt:8-11"
cin "range just after a replaced block shifts" "[17] m.txt:15-16 MOVED(-3) -> m.txt:12-13"
cin "deletion after the range moves nothing else" "[18] m.txt:28-28 MOVED(-3) -> m.txt:25-25"
cin "deletion at the next line" "[19] m.txt:29-29 MOVED(-3) -> m.txt:26-26"
cin "deletion ending just before the range" "[20] m.txt:32-32 MOVED(-5) -> m.txt:27-27"
cin "range after both hunks" "[21] m.txt:33-33 MOVED(-5) -> m.txt:28-28"
cin "insertion at the first line is inside" "[22] a.txt:5-7 TOUCHED(+3/-0 inside) -> a.txt:5-10"
cin "hunk ending just before the range shifts it" "[23] b.txt:4-5 MOVED(-2) -> b.txt:2-3"
cin "NEW file present in the worktree gets a window" "== [24] n.txt:1-2"
cin "NEW window shows the file" " 2 n2"
cin "CRLF window text" " 2 h2"
{ tr -d '
' <"$T/cp.out" | cmp -s - "$T/cp.out"; } && echo "ok context-pack CRLF is stripped from windows" || { echo "FAIL context-pack CR in output"; fail=1; }
cin "window numbers are worktree lines" " 6 b8"
cin "window shows the edit" "b10-EDITED"
cin "GONE window is labelled AT BASE" "== [7] c.txt:2-3 AT BASE"
cin "GONE window shows base text" " 2 c2"
cin "renamed window reads the new path" "== [8] d2.txt:4-5"
[ "$(grep -c '^\[' "$T/cp.out")" = 24 ] && echo "ok context-pack table lists every anchor" || { echo "FAIL context-pack table count"; fail=1; }
{ sed -n 2p "$T/cp.out" | grep -q '^\[7\]' && sed -n 6p "$T/cp.out" | grep -q '^\[24\]' && sed -n 7p "$T/cp.out" | grep -q '^\[1\]'; } && echo "ok context-pack problem anchors first" || { echo "FAIL context-pack problem anchors first"; fail=1; }
# deletes naming the file, and a valid range, make the same spec exit 0
sed -e 's/"deletes": \[\]/"deletes": [{"path": "c.txt"}]/' -e 's/"end_line": 50/"end_line": 6/' -e 's/"end_line": 11/"end_line": 10/' "$T/cp-spec.json" | wrapcp >"$T/cp-spec0.json"
r=$(cpo "$(wp "$T/cp-spec0.json")"); cchk "GONE named by deletes, no OUT_OF_RANGE" 0 "$r"
cin "GONE in deletes has no qualifier" "[7] c.txt:2-3 GONE #"
# unreachable base
sed "s/\"$BASE\"/\"deadbeef\"/" "$T/cp-spec.json" | wrapcp >"$T/cp-specb.json"
r=$(cpo "$(wp "$T/cp-specb.json")"); cchk "unreachable base" 3 "$r"
cin "UNRESOLVED hint" "UNRESOLVED base not a commit here: git fetch origin deadbeef"
[ "$(grep -c 'UNRESOLVED' "$T/cp.out")" = 24 ] && echo "ok context-pack every anchor UNRESOLVED" || { echo "FAIL context-pack UNRESOLVED count"; fail=1; }
cnot "UNRESOLVED prints no window" "== ["
# --since: every anchor stays in the table; only the edited one has a window
r=$(cpo "$SP" --since HEAD); cchk "--since" 3 "$r"
cin "--since keeps the table" "[1] a.txt:20-22 MOVED(+3)"
cin "--since prints the edited window" "== [11] f.txt:5-6"
cin "--since window shows the edit" "f5-UNCOMMITTED"
cnot "--since omits untouched windows" "== [1] "
cnot "--since omits the GONE window" "== [7] "
r=$(cpo "$SP" --since "$BASE"); cin "--since an older commit widens" "== [4] a.txt:4-11"
cnot "--since an older commit still omits untouched windows" "== [2] "
r=$(cpo "$SP" --since "$BASE" --only 15,16,17,18,19,20,21)
for k in 15 16 19 20; do cin "--since window $k overlaps a hunk" "== [$k] "; done
for k in 17 18 21; do cnot "--since window $k does not overlap" "== [$k] "; done
r=$(cpo "$SP" --only 2,11); cin "--only prints the named window" "== [2] "
cnot "--only omits the others" "== [1] "
r=$(cpo "$SP" --only 99); cchk "--only outside file_changes" 2 "$r"; grep -qF "context-pack: --only: index outside file_changes" "$T/cp.err" && echo "ok context-pack names --only" || { echo "FAIL context-pack names --only"; fail=1; }
r=$(cpo "$SP" --since feedface); cchk "--since not a commit" 2 "$r"; grep -qF "context-pack: --since: not a commit in this worktree" "$T/cp.err" && echo "ok context-pack names --since" || { echo "FAIL context-pack names --since"; fail=1; }
Z70=$(printf 'z%.0s' $(seq 1 70))
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
{"change": "up", "location": {"path": "../a.txt", "line": 1, "end_line": 1}},
{"change": "abs", "location": {"path": "/a.txt", "line": 1, "end_line": 1}},
{"change": "drive", "location": {"path": "c:a.txt", "line": 1, "end_line": 1}},
{"change": "'$Z70'zzzz", "location": {"path": "a.txt", "line": 1, "end_line": 1}}]}' | wrapcp >"$T/cp-specu.json"
r=$(cpo "$(wp "$T/cp-specu.json")"); cchk "unsafe paths" 3 "$r"
[ "$(grep -c 'UNRESOLVED path or git diff unusable' "$T/cp.out")" = 3 ] && echo "ok context-pack unsafe paths are UNRESOLVED" || { echo "FAIL context-pack unsafe paths"; fail=1; }
cin "a long change is cut at 70 characters" "# $Z70"
cnot "a long change is cut at 70 characters (not 71)" "${Z70}z"
# caps
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
for i in $(seq 1 30); do echo '{"change": "c'$i'", "location": {"path": "big.txt", "line": '$((i*5))', "end_line": '$((i*5+9))'}}'$([ $i -lt 30 ] && echo ,); done
echo ']}'; } | wrapcp >"$T/cp-spec30.json"
r=$(cpo "$(wp "$T/cp-spec30.json")" --max-lines 200); cchk "30 anchors" 0 "$r"
[ "$(wc -l <"$T/cp.out")" -le 200 ] && echo "ok context-pack output within --max-lines" || { echo "FAIL context-pack over 200 lines"; fail=1; }
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack capped output keeps the full table" || { echo "FAIL context-pack capped table"; fail=1; }
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && echo "ok context-pack expands 9 windows of 17 lines" || { echo "FAIL context-pack window count: $(grep -c '^== \[' "$T/cp.out")"; fail=1; }
cin "rest listed as not expanded" "not expanded: --only 10,11,12"
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}}]}' | wrapcp >"$T/cp-spec1.json"
S1=$(wp "$T/cp-spec1.json")
r=$(cpo "$S1"); cchk "150-line range" 0 "$r"
cin "window cut at 100 lines" "56 more lines: --only 1 --max-lines 160"
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 100 ] && echo "ok context-pack long window prints 100 lines" || { echo "FAIL context-pack long window"; fail=1; }
r=$(cpo "$S1" --only 1 --max-lines 160); cchk "--only with the suggested --max-lines" 0 "$r"
cnot "full window needs no more-lines note" "more lines"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 160 ] && echo "ok context-pack suggested --max-lines is exact" || { echo "FAIL context-pack suggested max: $(wc -l <"$T/cp.out")"; fail=1; }
# reads, symbols and caller lists
sed "s/\"BASE\"/\"$BASE\"/" "$CF/spec-reads.json" | wrapcp >"$T/cp-spec-reads.json"; SR=$(wp "$T/cp-spec-reads.json")
r=$(cpo "$SR"); cchk "reads and symbols" 3 "$r"
cin "a read that moved" "[r1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
cin "a read on a deleted path is GONE" "[r2] c.txt:2-3 GONE (not named by the spec's deletes)"
cin "a read absent at base is MISSING" "[r3] sub/a.txt:1-2 MISSING (read path absent at base)"
cin "a present symbol adds no suffix" "[1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
cin "a missing symbol is named" "[2] b.txt:30-31 MOVED(-2) SYMBOL_MISSING b99"
cin "an identifier matches whole words only" "MOVED(+3) SYMBOL_MISSING a2 -> a.txt:23-25"
cin "a non-identifier symbol is a literal substring" "[4] sym.txt:9-9 SAME"
cnot "the substring symbol is not missing" "SYMBOL_MISSING ::ba"
cin "the read caption carries its why" "== READ r1: moved below the insertion (a.txt:23-25 MOVED(+3))"
cin "a GONE read prints its window at base" "== READ r2: deleted path (c.txt:2-3 AT BASE"
[ "$(grep -c '^\[' "$T/cp.out")" = 9 ] && echo "ok context-pack the table lists edits and reads" || { echo "FAIL context-pack table rows with reads"; fail=1; }
[ "$(grep -n '^== READ r1' "$T/cp.out" | cut -d: -f1)" -gt "$(grep -n '^== \[4\]' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack read windows follow the edit windows" || { echo "FAIL context-pack read window order"; fail=1; }
[ "$(grep -n '^CALLERS' "$T/cp.out" | head -1 | cut -d: -f1)" -gt "$(grep -n '^== READ r5' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack caller lists follow the read windows" || { echo "FAIL context-pack caller list order"; fail=1; }
cin "3 callers are all listed" "CALLERS needle_three: 3 hits in 1 files"
cin "caller hit format" "zcallers.txt:2: needle_three call 2"
cnot "the hit inside the symbol's own window is dropped" "sym.txt:5: def needle_three"
cnot "a longer word is no caller" "needle_threefold"
cin "60 hits print per-file counts" "CALLERS needle_many: 60 hits in 2 files"
cin "per-file count line" " 30 many1.txt"
cin "the narrowing command" "narrow: git grep -n -I -w -F -e needle_many -- <path>"
cnot "60 hits print no hit lines" "many1.txt:1:"
r=$(cpo "$SR" --only r1); cchk "--only r1 keeps the exit of all anchors" 3 "$r"
cin "--only r1 prints the read" "== READ r1: moved below the insertion"
cnot "--only r1 prints no other read" "== READ r2"; cnot "--only r1 prints no edit window" "== [1]"
r=$(cpo "$SR" --only r9); cchk "--only past the reads" 2 "$r"; grep -qF "context-pack: --only: index outside reads" "$T/cp.err" && echo "ok context-pack names the reads bound" || { echo "FAIL context-pack reads bound"; fail=1; }
r=$(cpo "$SR" --only 1,r4); cin "--only mixes edit and read labels" "CALLERS needle_many: 60 hits"; cnot "--only lists callers of the selected anchors only" "CALLERS b99"
r=$(cpo "$SR" --since "$(git -C "$R" rev-parse HEAD)"); cnot "--since HEAD prints no read window" "== READ"; cnot "--since HEAD prints no caller list" "CALLERS"
r=$(cpo "$SR" --max-lines 3); cchk "--max-lines below the table with reads" 3 "$r"
[ "$(grep -c '^\[' "$T/cp.out")" = 9 ] && echo "ok context-pack a small budget keeps every edit and read row" || { echo "FAIL context-pack small budget with reads"; fail=1; }
cnot "a small budget prints no window" "== "; cnot "a small budget prints no caller list" "CALLERS"
r=$(cpo "$SR" --max-lines 60); cchk "caller lists cut by the budget" 3 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le 60 ] && echo "ok context-pack caller lists stay within --max-lines" || { echo "FAIL context-pack caller lists over budget"; fail=1; }
cin "the cut caller lists are named" "callers not printed: "
SUGG=$(grep -o '^callers not printed: .*--max-lines [0-9]*' "$T/cp.out" | grep -o '[0-9]*$')
r=$(cpo "$SR" --max-lines "$SUGG"); cnot "the suggested --max-lines prints every caller list" "callers not printed"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = "$SUGG" ] && echo "ok context-pack suggested caller --max-lines is exact" || { echo "FAIL context-pack suggested caller max: $(wc -l <"$T/cp.out") vs $SUGG"; fail=1; }
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
for i in 1 2 3 4 5 6 7 8 9; do echo '{"change": "s'$i'", "location": {"path": "big.txt", "line": '$i', "end_line": '$i', "symbol": "g'$i'"}}'$([ $i -lt 9 ] && echo ,); done
echo ']}'; } | wrapcp >"$T/cp-spec9sym.json"
r=$(cpo "$(wp "$T/cp-spec9sym.json")" --max-lines 400); cchk "nine symbols" 0 "$r"
[ "$(grep -c '^CALLERS' "$T/cp.out")" = 8 ] && echo "ok context-pack at most 8 caller lists" || { echo "FAIL context-pack caller list count"; fail=1; }
cin "the ninth symbol is named" "callers not listed: g9"
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [], "reads": [{"why": "w", "location": {"path": "a.txt", "line": 20, "end_line": 22, "symbol": "a21"}}]}' | wrapcp >"$T/cp-spec-r0.json"
r=$(cpo "$(wp "$T/cp-spec-r0.json")"); cchk "a spec of one read, symbol present" 0 "$r"
cin "a read with a symbol present: no suffix" "[r1] a.txt:20-22 MOVED(+3) -> a.txt:23-25 # w"
# caller-list bounds: 50 hits list, 51 count; 20 files; a hit's text is cut at 160; CR and bad bytes
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
n=0; for sy in bound_fifty bound_fiftyone spread_sym long_symbol q.zq sym_bad twenty_sym; do n=$((n+1))
echo '{"change": "'$sy'", "location": {"path": "anchors.txt", "line": 1, "end_line": 1, "symbol": "'$sy'"}}'$([ $n -lt 7 ] && echo ,); done
echo ']}'; } | wrapcp >"$T/cp-specb.json"
r=$(cpo "$(wp "$T/cp-specb.json")" --max-lines 400); cchk "caller list bounds" 0 "$r"
cin "exactly 50 hits are listed" "CALLERS bound_fifty: 50 hits in 1 files"
[ "$(grep -c '^bound50.txt:' "$T/cp.out")" = 50 ] && echo "ok context-pack 50 hits print as lines" || { echo "FAIL context-pack 50 hit lines"; fail=1; }
cin "51 hits are counted" "CALLERS bound_fiftyone: 51 hits in 1 files"
cin "51 hits: the per-file count" " 51 bound51.txt"
cnot "51 hits print no hit lines" "bound51.txt:1:"
grep -qx "(1 more files)" "$T/cp.out" && echo "ok context-pack 21 files: the 21st is named" || { echo "FAIL context-pack 21st file line"; fail=1; }
grep -qx "narrow: git grep -n -I -F -e q.zq -- <path>" "$T/cp.out" && echo "ok context-pack the narrowing line is exact" || { echo "FAIL context-pack narrowing line"; fail=1; }
cin "exactly 20 files name no more" "CALLERS twenty_sym: 60 hits in 20 files"
[ "$(grep -c '(0 more files)' "$T/cp.out")" = 0 ] && echo "ok context-pack 20 files print no more-files line" || { echo "FAIL context-pack 20 files"; fail=1; }
[ "$(grep -c '^ *[34] manyf/' "$T/cp.out")" = 20 ] && echo "ok context-pack at most 20 files are counted" || { echo "FAIL context-pack file cap: $(grep -c '^ *[34] manyf/' "$T/cp.out")"; fail=1; }
cin "files are ranked by hits, largest first" " 4 manyf/f21.txt"
[ "$(grep -n 'manyf/f21.txt' "$T/cp.out" | cut -d: -f1)" -lt "$(grep -n 'manyf/f01.txt' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack the largest file prints first" || { echo "FAIL context-pack file order"; fail=1; }
cin "a non-identifier narrowing command has no -w" "narrow: git grep -n -I -F -e q.zq -- <path>"
LL=$(grep '^longline.txt:1: ' "$T/cp.out"); [ "${#LL}" = 176 ] && echo "ok context-pack a hit's text is cut at 160 characters" || { echo "FAIL context-pack hit text length ${#LL}"; fail=1; }
cnot "a CR in a hit is stripped" "$(printf '\r')"
cin "a hit with invalid UTF-8 still prints" "bad.txt:1: sym_bad"
cin "a hit keeps a trailing X and loses its CR" "endX"
grep -qx "bad.txt:1: sym_bad .* endX" "$T/cp.out" && echo "ok context-pack a hit ends at its last character" || { echo "FAIL context-pack hit end"; fail=1; }
r=$(cpo "$SR" --only 5); cchk "--only past the last edit anchor" 2 "$r"; grep -qxF "context-pack: --only: index outside file_changes" "$T/cp.err" && echo "ok context-pack --only 5 is outside four anchors" || { echo "FAIL context-pack --only 5"; fail=1; }
r=$(cpo "$SR" --only 4); cchk "--only the last edit anchor" 3 "$r"; cin "--only 4 prints that window" "== [4] sym.txt:9-9"
r=$(cpo "$SR" --only r6); cchk "--only past the last read" 2 "$r"; grep -qxF "context-pack: --only: index outside reads" "$T/cp.err" && echo "ok context-pack --only r6 is outside five reads" || { echo "FAIL context-pack --only r6"; fail=1; }
r=$(cpo "$SR" --only r5); cchk "--only the last read" 3 "$r"; cin "--only r5 prints that window" "== READ r5:"
sed -n 3p "$T/cp.out" | grep -q '^\[r3\]' && echo "ok context-pack a MISSING read sorts with the problem anchors" || { echo "FAIL context-pack MISSING read order"; fail=1; }
cin "a GONE read checks its symbol at base" "GONE (not named by the spec's deletes) SYMBOL_MISSING c9"
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": [{"location": {"path": "a.txt", "line": 1, "end_line": 2, "symbol": "zz9"}}],'
echo '"reads": [{"location": {"path": "a.txt", "line": 1, "end_line": 2}}, {"why": "'$(printf '%0250d' 0 | tr 0 w)'", "location": {"path": "a.txt", "line": 3, "end_line": 3}}]}'; } | wrapcp >"$T/cp-specw.json"
r=$(cpo "$(wp "$T/cp-specw.json")"); cchk "a missing symbol alone fails the run" 3 "$r"
cin "a symbol absent from an unmoved window" "[1] a.txt:1-2 SAME SYMBOL_MISSING zz9"
cnot "an anchor without a change or why gets no filler" "XXXX"
W200=$(printf '%0200d' 0 | tr 0 w); cin "the read caption is cut at 200" "== READ r2: $W200 (a.txt:3-3"
# the caller lists obey --max-lines at every budget, and the suggested value prints them all
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
for i in 1 2 3 4 5 6 7 8 9 10; do echo '{"change": "s'$i'", "location": {"path": "big.txt", "line": '$i', "end_line": '$i', "symbol": "g'$i'"}}'$([ $i -lt 10 ] && echo ,); done
echo ']}'; } | wrapcp >"$T/cp-spec10sym.json"; S10=$(wp "$T/cp-spec10sym.json")
r=$(cpo "$S10" --max-lines 400); cin "two symbols past the eighth are named together" "callers not listed: g9,g10"
FULLN=$(wc -l <"$T/cp.out" | tr -d ' ')
for b in $((FULLN-1)) $((FULLN-3)) $((FULLN-6)) $((FULLN-9)) $((FULLN-30)); do
r=$(cpo "$S10" --max-lines "$b")
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le "$b" ] && echo "ok context-pack budget $b holds" || { echo "FAIL context-pack budget $b exceeded: $(wc -l <"$T/cp.out")"; fail=1; }
SG=$(grep -o '^callers not printed: .*--max-lines [0-9]*' "$T/cp.out" | grep -o '[0-9]*$')
if [ -n "$SG" ]; then r=$(cpo "$S10" --max-lines "$SG"); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = "$FULLN" ] && ! grep -q 'callers not printed\|not expanded' "$T/cp.out" && echo "ok context-pack budget $b: the suggestion prints everything" || { echo "FAIL context-pack budget $b: suggestion $SG gave $(wc -l <"$T/cp.out") of $FULLN"; fail=1; }; fi
done
r=$(cpo "$S10" --max-lines $((FULLN-1))); [ "$(grep -c '^CALLERS' "$T/cp.out")" = 6 ] && echo "ok context-pack one line short drops two lists for the note" || { echo "FAIL context-pack one line short: $(grep -c '^CALLERS' "$T/cp.out") lists"; fail=1; }
cin "the dropped lists are named" "callers not printed: g7,g8: --max-lines $FULLN"
# window budgets: the exact thresholds, and what the caller lists leave of them
r=$(cpo "$S1" --max-lines 104); cnot "104 lines expand no 100-line window" "== ["; cin "104 lines skip a 100-line window" "not expanded: --only 1"
r=$(cpo "$S1" --max-lines 105); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 105 ] && grep -qx "56 more lines: --only 1 --max-lines 160" "$T/cp.out" && echo "ok context-pack 105 lines hold a capped window and its note" || { echo "FAIL context-pack 105-line budget"; fail=1; }
S30=$(wp "$T/cp-spec30.json")
r=$(cpo "$S30" --max-lines 186); [ "$(grep -c '^== ' "$T/cp.out")" = 9 ] && [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 186 ] && echo "ok context-pack 186 lines hold 9 windows and the note" || { echo "FAIL context-pack 186-line budget"; fail=1; }
r=$(cpo "$S30" --max-lines 185); [ "$(grep -c '^== ' "$T/cp.out")" = 8 ] && echo "ok context-pack 185 lines hold 8 windows" || { echo "FAIL context-pack 185-line budget"; fail=1; }
r=$(cpo "$S30" --max-lines 542); [ "$(grep -c '^== ' "$T/cp.out")" = 30 ] && echo "ok context-pack 542 lines hold all 30 windows" || { echo "FAIL context-pack 542-line budget"; fail=1; }; cnot "542 lines need no not-expanded note" "not expanded"
for b in 530 531 532 533 534 535 536 537 538 539 540 541; do
r=$(cpo "$S30" --max-lines $b); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le $b ] && grep -q '^not expanded: --only 30$' "$T/cp.out" && echo "ok context-pack budget $b skips only the last window" || { echo "FAIL context-pack budget $b: $(wc -l <"$T/cp.out") lines"; fail=1; }
done
for b in 37 38 39 40 41 42 43 44 45 46 47; do
r=$(cpo "$S30" --only 1,2 --max-lines $b); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = $b ] && [ "$(grep -c '^== ' "$T/cp.out")" = 1 ] && grep -q 'more lines: --only 1 ' "$T/cp.out" && echo "ok context-pack --only 1,2 at $b lines truncates window 1 to fit" || { echo "FAIL context-pack --only 1,2 at $b: $(wc -l <"$T/cp.out") lines"; fail=1; }
done
r=$(cpo "$S30" --only 1,2 --max-lines 36); cnot "--only 1,2 at 36 lines expands nothing" "== ["
r=$(cpo "$S10" --max-lines 94); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 94 ] && [ "$(grep -c '^== ' "$T/cp.out")" = 10 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 6 ] && echo "ok context-pack one line short keeps every window and 6 lists" || { echo "FAIL context-pack 94-line S10"; fail=1; }
r=$(cpo "$S10" --max-lines 86); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 86 ] && [ "$(grep -c '^== ' "$T/cp.out")" = 9 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 5 ] && echo "ok context-pack 86 lines: 9 windows, 5 lists, every note" || { echo "FAIL context-pack 86-line S10"; fail=1; }
r=$(cpo "$S10" --max-lines 65); [ "$(grep -c '^== ' "$T/cp.out")" = 7 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 0 ] && grep -q '^callers not printed: ' "$T/cp.out" && grep -q '^callers not listed: g9,g10' "$T/cp.out" && echo "ok context-pack windows outrank caller lists" || { echo "FAIL context-pack 65-line S10"; fail=1; }
r=$(cpo "$SR" --max-lines 40); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 40 ] && [ "$(grep -c '^== ' "$T/cp.out")" = 3 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 2 ] && echo "ok context-pack 40 lines: 3 windows, 2 lists, no line unused" || { echo "FAIL context-pack 40-line SR"; fail=1; }
r=$(cpo "$SR" --max-lines 44); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 44 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 3 ] && echo "ok context-pack 44 lines: 3 lists" || { echo "FAIL context-pack 44-line SR"; fail=1; }
r=$(cpo "$SR" --max-lines 62); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 61 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 4 ] && echo "ok context-pack 62 lines: a list that needs 2 lines waits" || { echo "FAIL context-pack 62-line SR"; fail=1; }
# usage and the spec document (a saved file or stdin); no error echoes an input value
r=$(cpo "$SP" --max-lines CANARY-usage); cchk "bad --max-lines" 2 "$r"; cnot "usage error echoes no value" "CANARY-usage"
UNREADABLE="context-pack: spec: unreadable; pipe huginn view --raw <id> or pass a saved file"
r=$(cpo "$T/CANARY-nofile.json"); cchk "unreadable spec file" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the unreadable spec" || { echo "FAIL context-pack unreadable message: $(cat "$T/cp.err")"; fail=1; }
cnot "unreadable spec echoes no path" "CANARY"
r=$(cpo "$SP"); cp "$T/cp.out" "$T/cp.want"
r=$($PY "$CP" - "$Rw" <"$T/cp-spec.json" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "spec from stdin" 3 "$r"; [ "$r" = 3 ] || cat "$T/cp.err"
cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack stdin equals the file run" || { echo "FAIL context-pack stdin differs from the file run"; fail=1; }
r=$(: | $PY "$CP" - "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "empty stdin" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the empty spec" || { echo "FAIL context-pack empty stdin message"; fail=1; }
r=$(echo 'absent t:cut_spec:CANARY-x' | $PY "$CP" - "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "an absent line" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the absent spec" || { echo "FAIL context-pack absent message"; fail=1; }
cnot "an absent line echoes no id" "CANARY"
echo '{}' >"$T/cp-empty.json"; r=$(cpo "$(wp "$T/cp-empty.json")"); cchk "a spec that is {}" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the empty object" || { echo "FAIL context-pack {} message"; fail=1; }
printf '' | cat - "$T/cp-spec.json" >"$T/cp-bom.json"; r=$(cpo "$(wp "$T/cp-bom.json")"); cchk "a spec file with a UTF-8 BOM" 3 "$r"
r=$(cat "$T/cp-bom.json" | $PY "$CP" - "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "a spec on stdin with a UTF-8 BOM" 3 "$r"
$PY -c 'import json,sys; v=json.load(open(sys.argv[1])); v["kind"]="ruling"; json.dump(v,open(sys.argv[2],"w"))' "$(wp "$T/cp-spec.json")" "$(wp "$T/cp-notspec.json")"
r=$(cpo "$(wp "$T/cp-notspec.json")"); cchk "a well-shaped document of another kind" 4 "$r"
$PY -c 'import json,sys; json.dump(json.load(open(sys.argv[1]))["value"],open(sys.argv[2],"w"))' "$(wp "$T/cp-spec.json")" "$(wp "$T/cp-bare.json")"
r=$(cpo "$(wp "$T/cp-bare.json")"); cchk "a bare value with no kind" 4 "$r"
for f in campaign cut revision; do
$PY -c 'import json,sys; v=json.load(open(sys.argv[1])); del v["value"][sys.argv[3]]; json.dump(v,open(sys.argv[2],"w"))' "$(wp "$T/cp-spec.json")" "$(wp "$T/cp-noid.json")" $f
r=$(cpo "$(wp "$T/cp-noid.json")"); cchk "a spec without $f" 4 "$r"; cnot "no None for a missing $f" "None"
done
echo '{"kind": "ruling", "value": {"title": "CANARY-title"}}' >"$T/cp-ruling.json"; r=$(cpo "$(wp "$T/cp-ruling.json")"); cchk "a document that is no cut_spec" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the foreign document" || { echo "FAIL context-pack foreign document message"; fail=1; }
cnot "a foreign document echoes no value" "CANARY"
grep -qF "usage: " "$T/cp.err" || echo "ok context-pack spec error prints no usage text"
r=$($PY "$CP" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "usage: no spec source" 2 "$r"
grep -qF "context-pack: usage error" "$T/cp.err" && echo "ok context-pack usage error text" || { echo "FAIL context-pack usage text"; fail=1; }
r=$($PY "$CP" "$SP" "$T/CANARY-notrepo" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "worktree is not a repo" 2 "$r"; grep -qF "context-pack: worktree: not a git worktree" "$T/cp.err" && echo "ok context-pack names the worktree" || { echo "FAIL context-pack worktree message"; fail=1; }; cnot "worktree error echoes no path" "CANARY-notrepo"
# glob characters in a spec path name that file only; straddling hunks; --since boundaries
R2=$T/cp2; mkdir -p "$R2/src/[a]" "$R2/src/a"; R2w=$(wp "$R2")
g2() { git -C "$R2" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
g2 init -q
seqf x 30 >"$R2/src/[a]/f.txt"; seqf x 30 >"$R2/src/a/f.txt"
for n in s t u v; do seqf x 30 >"$R2/$n.txt"; done; seqf w 12 >"$R2/w.txt"; seqf g 200 >"$R2/big.txt"; echo q1 >"$R2/one.txt"
g2 add -A; g2 commit -q -m base; B2=$(git -C "$R2" rev-parse HEAD)
{ printf 'new1\nnew2\n'; cat "$R2/src/a/f.txt"; } >"$R2/src/a/f.new"; mv "$R2/src/a/f.new" "$R2/src/a/f.txt"
sed -i 8,12d "$R2/s.txt"; sed -i 15,25d "$R2/t.txt"
sed -i '8,12c\NEW' "$R2/u.txt"; sed -i '15,25c\NEW' "$R2/v.txt"
g2 add -A; g2 commit -q -m second; S2=$(git -C "$R2" rev-parse HEAD)
sed -i 's/^w5$/w5-EDITED/' "$R2/w.txt"
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
{"change": "glob path", "location": {"path": "src/[a]/f.txt", "line": 10, "end_line": 12}},
{"change": "pure deletion above the start", "location": {"path": "s.txt", "line": 10, "end_line": 20}},
{"change": "pure deletion past the end", "location": {"path": "t.txt", "line": 10, "end_line": 20}},
{"change": "one-line replacement above the start", "location": {"path": "u.txt", "line": 10, "end_line": 20}},
{"change": "one-line replacement past the end", "location": {"path": "v.txt", "line": 10, "end_line": 20}},
{"change": "edit on the last line", "location": {"path": "w.txt", "line": 3, "end_line": 5}},
{"change": "edit on the first line", "location": {"path": "w.txt", "line": 5, "end_line": 7}},
{"change": "after the edit", "location": {"path": "w.txt", "line": 6, "end_line": 8}},
{"change": "before the edit", "location": {"path": "w.txt", "line": 1, "end_line": 4}}]}' | wrapcp >"$T/cp-spec2.json"
SP2=$(wp "$T/cp-spec2.json")
cp2() { local s=$1; shift; $PY "$CP" "$s" "$R2w" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
r=$(cp2 "$SP2"); cchk "straddle fixture" 0 "$r"
cin "a glob path reads its own file: SAME" "[1] src/[a]/f.txt:10-12 SAME -> src/[a]/f.txt:10-12"
cin "pure deletion starting above the anchor starts at the first survivor" "[2] s.txt:10-20 TOUCHED(+0/-5 inside) -> s.txt:8-15"
cin "pure deletion ending past the anchor ends at the last survivor" "[3] t.txt:10-20 TOUCHED(+0/-11 inside) -> t.txt:10-14"
cin "one-line replacement above the start begins at the replacement" "[4] u.txt:10-20 TOUCHED(+1/-5 inside) -> u.txt:8-16"
cin "one-line replacement past the end ends at the replacement" "[5] v.txt:10-20 TOUCHED(+1/-11 inside) -> v.txt:10-15"
cin "straddle window starts at the mapped range" "== [2] s.txt:8-15"
r=$(cp2 "$SP2" --since "$S2" --only 6,7,8,9)
cin "--since: an edit on the anchor's last line is inside" "== [6] w.txt:3-5"
cin "--since: an edit on the anchor's first line is inside" "== [7] w.txt:5-7"
cnot "--since: an anchor after the edit has no window" "== [8] "
cnot "--since: an anchor before the edit has no window" "== [9] "
cin "--since keeps every anchor in the table" "[9] w.txt:1-4 SAME"
# a --since commit whose tree is unreadable: git diff fails, so no anchor is claimed edited
BL=$(echo only | git -C "$R2" hash-object -w --stdin); TR=$(printf '100644 blob %s\tonly.txt\n' "$BL" | git -C "$R2" mktree)
CM=$(git -C "$R2" -c user.name=t -c user.email=t@t commit-tree "$TR" -m orphan)
chmod u+w "$R2/.git/objects/${TR:0:2}/${TR:2}" 2>/dev/null; rm -f "$R2/.git/objects/${TR:0:2}/${TR:2}"
r=$(cp2 "$SP2" --since "$CM"); cchk "--since with an unreadable tree" 0 "$r"
cnot "--since: a failed git diff claims no edited window" "== ["
cin "--since: a failed git diff keeps the table" "[6] w.txt:3-5"
# a spec value of the wrong type is a typed refusal naming the field, never a traceback or the value
$PY - "$T" "$SP" <<'PYE'
import copy, json, sys
t, sp = sys.argv[1], sys.argv[2]
spec = json.load(open(sp, encoding="utf-8"))
cases = {
"line": (lambda v: v["file_changes"][1]["location"].update(line="CANARY-line"), "file_changes[2].location.line"),
"line-bool": (lambda v: v["file_changes"][0]["location"].update(line=True), "file_changes[1].location.line"),
"end_line": (lambda v: v["file_changes"][2]["location"].update(end_line="CANARY-end"), "file_changes[3].location.end_line"),
"path": (lambda v: v["file_changes"][0]["location"].update(path=["CANARY-path"]), "file_changes[1].location.path"),
"nolocation": (lambda v: v["file_changes"][3].pop("location"), "file_changes[4].location"),
"location-str": (lambda v: v["file_changes"][0].update(location="CANARY-loc"), "file_changes[1].location"),
"entry-str": (lambda v: v["file_changes"].__setitem__(0, "CANARY-str"), "file_changes[1]"),
"change": (lambda v: v["file_changes"][0].update(change={"CANARY": 1}), "file_changes[1].change"),
"fcs-str": (lambda v: v.update(file_changes="CANARY-fcs"), "file_changes"),
"base": (lambda v: v.update(base=12345), "base"),
"deletes-str": (lambda v: v.update(deletes="CANARY-del"), "deletes"),
"deletes-nopath": (lambda v: v.update(deletes=[{"note": "CANARY-note"}]), "deletes[1].path"),
"deletes-entry-str": (lambda v: v.update(deletes=["CANARY-d"]), "deletes[1].path"),
"reads-str": (lambda v: v.update(reads="CANARY-reads"), "reads"),
"read-entry-str": (lambda v: v.update(reads=["CANARY-read"]), "reads[1]"),
"read-nolocation": (lambda v: v.update(reads=[{"why": "w"}]), "reads[1].location"),
"read-path": (lambda v: v.update(reads=[{"why": "w", "location": {"path": ["CANARY-p"], "line": 1}}]), "reads[1].location.path"),
"read-line": (lambda v: v.update(reads=[{"why": "w", "location": {"path": "a.txt", "line": "CANARY-l"}}]), "reads[1].location.line"),
"read-why": (lambda v: v.update(reads=[{"why": {"CANARY": 1}, "location": {"path": "a.txt", "line": 1}}]), "reads[1].why"),
"read-symbol": (lambda v: v.update(reads=[{"why": "w", "location": {"path": "a.txt", "line": 1, "symbol": 7}}]), "reads[1].location.symbol"),
"edit-symbol": (lambda v: v["file_changes"][0]["location"].update(symbol=["CANARY-s"]), "file_changes[1].location.symbol"),
}
names = []
for name, (edit, field) in cases.items():
d = copy.deepcopy(spec)
edit(d["value"])
json.dump(d, open("%s/cp-bad-%s.json" % (t, name), "w"))
names.append("%s %s" % (name, field))
open(t + "/cp-bad.list", "w", newline=chr(10)).write("\n".join(names) + "\n")
PYE
while read -r nm fld; do
r=$(cpo "$(wp "$T/cp-bad-$nm.json")"); cchk "wrong-typed spec: $nm" 4 "$r"
grep -qF "context-pack: spec: $fld: InvalidInput" "$T/cp.err" && echo "ok context-pack names $fld" || { echo "FAIL context-pack message for $nm: $(cat "$T/cp.err")"; fail=1; }
cnot "wrong-typed spec $nm: no value" "CANARY"; cnot "wrong-typed spec $nm: no traceback" "Traceback"
done <"$T/cp-bad.list"
echo '{"base": "CANARY-base", "deletes": [], "file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1, "end_line": 2}}]}' | wrapcp >"$T/cp-specnb.json"
r=$(cpo "$(wp "$T/cp-specnb.json")"); cchk "a base that is no sha" 3 "$r"
cin "a base that is no sha reads UNRESOLVED" "UNRESOLVED base not a commit here"
cin "a base that is no sha is not echoed in the header" "base unresolvable HEAD"
echo '{"file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1}}]}' | wrapcp >"$T/cp-spec-nobase.json"
r=$(cpo "$(wp "$T/cp-spec-nobase.json")"); cchk "a spec value without a base" 4 "$r"
grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack a missing base is a typed refusal" || { echo "FAIL context-pack missing base: $(cat "$T/cp.err")"; fail=1; }
# a --max-lines below the table: the table always lists every anchor and no window prints
r=$(cpo "$(wp "$T/cp-spec30.json")" --max-lines 5); cchk "--max-lines below the table" 0 "$r"
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack a small budget keeps the full table" || { echo "FAIL context-pack small budget table"; fail=1; }
cnot "a small budget prints no window" "== ["
[ -z "$(sed -n 32p "$T/cp.out")" ] && echo "ok context-pack a blank line follows the table" || { echo "FAIL context-pack blank line after the table"; fail=1; }
cin "a small budget lists every window as not expanded" "not expanded: --only 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30"
# --only truncates a window to the budget and says how to see the rest
r=$(cpo "$S1" --only 1 --max-lines 50); cchk "--only truncation" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only fills the budget exactly" || { echo "FAIL context-pack --only budget: $(wc -l <"$T/cp.out")"; fail=1; }
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 45 ] && echo "ok context-pack --only truncated window has 45 lines" || { echo "FAIL context-pack --only window lines"; fail=1; }
cin "--only truncation note counts the rest" "111 more lines: --only 1 --max-lines 160"
r=$(cpo "$S1" --only 1 --max-lines 7); cchk "--only at the smallest truncatable budget" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 7 ] && [ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 2 ] && echo "ok context-pack --only budget 7 prints 2 window lines" || { echo "FAIL context-pack --only budget 7"; fail=1; }
r=$(cpo "$S1" --only 1 --max-lines 6); cin "--only below the smallest truncation is not expanded" "not expanded: --only 1"
cnot "--only below the smallest truncation prints no window" "== ["
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
{"change": "short", "location": {"path": "big.txt", "line": 200, "end_line": 210}}]}' | wrapcp >"$T/cp-spec2w.json"
r=$(cpo "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 50); cchk "--only two windows" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only keeps room for the not-expanded line" || { echo "FAIL context-pack --only reserve: $(wc -l <"$T/cp.out")"; fail=1; }
cin "--only: the window that no longer fits is listed" "not expanded: --only 2"
r=$(cpo "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 161); cchk "--only first window needs exactly the remaining budget" 0 "$r"
cin "--only: a window that fills the budget minus the reserve is cut, not skipped" "== [1] big.txt:20-169"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 161 ] && echo "ok context-pack --only cut window keeps the reserve line" || { echo "FAIL context-pack --only exact need: $(wc -l <"$T/cp.out")"; fail=1; }
r=$(cpo "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 8); cchk "--only two windows, too small to cut" 0 "$r"
cnot "--only: below four lines after the reserve nothing is cut" "== ["
cin "--only: both are listed as not expanded" "not expanded: --only 1,2"
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
{"change": "tiny", "location": {"path": "one.txt", "line": 1, "end_line": 1}}]}' | wrapcp >"$T/cp-spec3.json"
r=$(cp2 "$(wp "$T/cp-spec3.json")" --only 1,2 --max-lines 100); cchk "--only cut window then a tiny one" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 100 ] && echo "ok context-pack a cut window is charged what it prints" || { echo "FAIL context-pack cut window charge: $(wc -l <"$T/cp.out")"; fail=1; }
cin "a cut window leaves no room for the next one" "not expanded: --only 2"
r=$(cpo "$S1" --max-lines 104); cin "a 100-line cut window needs 102 lines of budget" "not expanded: --only 1"
r=$(cpo "$S1" --max-lines 105); cin "a 100-line cut window fits in 105 lines" "56 more lines: --only 1 --max-lines 160"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 105 ] && echo "ok context-pack a 100-line cut window fills 105 lines" || { echo "FAIL context-pack cut window at 105: $(wc -l <"$T/cp.out")"; fail=1; }
grep -qxF "56 more lines: --only 1 --max-lines 160" "$T/cp.out" && echo "ok context-pack the more-lines note is its own line" || { echo "FAIL context-pack more-lines note line"; fail=1; }
r=$(cpo "$(wp "$T/cp-spec30.json")" --max-lines 202); cchk "reserve line at the budget edge" 0 "$r"
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && [ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le 202 ] && echo "ok context-pack a window that would crowd out the not-expanded line is skipped" || { echo "FAIL context-pack reserve at 202: $(wc -l <"$T/cp.out")"; fail=1; }
# standard library plus git only
if grep -qE '^import (requests|yaml|tree_sitter)|^from (requests|yaml|tree_sitter)|ctags|tree-sitter|"rg"' "$CP"; then echo "FAIL context-pack: a dependency beyond git"; fail=1; else echo "ok context-pack: standard library and git only"; fi
# ---- ygg-verify.sh transport switch: the local branch (this host is Yggdrasil) and, under a mock
# ssh that runs the remote command string in a shell with a fake remote HOME, the remote branch.
# Docker is the mutation-mock fake; its container is a directory and does not mount the work
# directory, so these jobs prove the transport, slots, clone, verdict and summary, not a toolchain.
YL=$T/yl; mkdir -p "$YL/bin" "$YL/rhome"
cp "$here/fixtures/mutation-mock/fakebin/"{docker,sudo,flock} "$YL/bin/"
printf '#!/usr/bin/env bash\necho "ssh $*" >>"$YL_SSHLOG"\nwhile [ "$1" = -o ]; do shift 2; done\nshift\nHOME=$YL_RHOME exec bash -c "$*"\n' >"$YL/bin/ssh"
printf '#!/usr/bin/env bash\necho "scp $*" >>"$YL_SSHLOG"\nwhile [ "$1" = -q ]; do shift; done\nsrc=$1; dst=${2#*:}; cp "$src" "$YL_RHOME/$dst"\n' >"$YL/bin/scp"
printf '#!/usr/bin/env bash\necho "${YL_HOSTNAME:-starfire}"\n' >"$YL/bin/hostname"
chmod +x "$YL/bin/"*
YLR=$YL/repo; mkdir -p "$YLR"; git -C "$YLR" init -q; echo marker >"$YLR/m.txt"
git -C "$YLR" add m.txt; git -C "$YLR" -c user.name=t -c user.email=t@t commit -qm m
ylrun() { # ylrun <extra env assignments...> -- <args>: ygg-verify under the mocks; sets yl_out yl_st
local envs=(); while [ "$1" != -- ]; do envs+=("$1"); shift; done; shift
: >"$YL/ssh.log"
yl_out=$(env PATH="$YL/bin:$PATH" FAKE_DOCKER="$YL/fd" YL_SSHLOG="$YL/ssh.log" YL_RHOME="$YL/rhome" YGG_VERIFY_ROOT="$YL/root" \
TIMEOUT=60 "${envs[@]}" bash "$V" "$@" 2>&1); yl_st=$?
}
ylchk() { # ylchk <label> <expected status> <must-contain> <ssh used: 0|1>
local used=0; [ -s "$YL/ssh.log" ] && used=1
if [ "$yl_st" != "$2" ]; then echo "FAIL $1: exit $yl_st, wanted $2: $yl_out"; fail=1
elif ! printf '%s\n' "$yl_out" | grep -qF -- "$3"; then echo "FAIL $1: missing: $3 in: $yl_out"; fail=1
elif [ "$used" != "$4" ]; then echo "FAIL $1: ssh used=$used, wanted $4: $(cat "$YL/ssh.log")"; fail=1
else echo "ok $1"; fi
}
ylrun YGG_VERIFY_LOCAL=1 -- "$YLR" HEAD base 'echo localjob-ran; exit 3'
ylchk "ygg-verify local: the job's status and output come back, no ssh" 3 "localjob-ran" 0
[ -d "$YL/root/repos/repo.git" ] && [ -z "$(ls "$YL/root/work" | grep -v '\.killed$')" ] && echo "ok ygg-verify local: mirror under YGG_VERIFY_ROOT, work directory removed" || { echo "FAIL ygg-verify local: mirror or work dir state: $(ls -R "$YL/root" | head)"; fail=1; }
ylrun YGG_VERIFY_LOCAL=1 RUN_ON_HOST=1 -- "$YLR" HEAD base 'cat m.txt; echo "root=$YGG_ROOT slots=$YGG_SLOTS"; test -e "$YGG_WORK/m.txt"'
ylchk "ygg-verify local: RUN_ON_HOST runs in the checked-out revision with its environment" 0 "root=$YL/root slots=5" 0
ylrun YGG_VERIFY_LOCAL=1 SUMMARY="$YL/s.log" -- "$YLR" HEAD base 'echo "test result: ok. 4 passed; 0 failed; 0 ignored"'
ylchk "ygg-verify local: SUMMARY prints the bounded summary" 0 "FRAMEWORK cargo passed=4 failed=0" 0
out=$(WAIT_MAX=20 SETTLE=0 bash "$V" wait "$YL/s.log"); st=$?
[ "$st" = 0 ] && printf '%s\n' "$out" | grep -qF "FRAMEWORK cargo passed=4" && echo "ok ygg-verify local: wait returns the finished job" || { echo "FAIL ygg-verify local wait: $st $out"; fail=1; }
# all slots held by other jobs: the job waits, then takes the slot that frees (real flock on the slot files)
( exec 8>"$YL/root/slot-1.lock"; flock -n 8; sleep 7 ) & sb=$!
sleep 1
ylrun YGG_VERIFY_LOCAL=1 SLOTS=1 -- "$YLR" HEAD base 'echo slotted'
wait "$sb"
ylchk "ygg-verify local: waits for a busy slot, then runs" 0 "all 1 slots busy; waiting" 0
# detection: hostname yggdrasil-candidate is local without the env; another host, or YGG_VERIFY_LOCAL=0, goes over ssh
ylrun YL_HOSTNAME=yggdrasil-candidate -- "$YLR" HEAD base 'echo auto-local'
ylchk "ygg-verify detection: hostname yggdrasil-candidate runs locally" 0 "auto-local" 0
ylrun YL_HOSTNAME=Yggdrasil -- "$YLR" HEAD base 'echo auto-local2'
ylchk "ygg-verify detection: hostname Yggdrasil runs locally" 0 "auto-local2" 0
ylrun YL_HOSTNAME=yggdrasil-candidate YGG_VERIFY_LOCAL=0 -- "$YLR" HEAD base 'echo forced-remote; exit 5'
ylchk "ygg-verify detection: YGG_VERIFY_LOCAL=0 forces ssh (mock ssh: remote command string in a fake HOME)" 5 "forced-remote" 1
ylrun YL_HOSTNAME=starfire -- "$YLR" HEAD base 'echo workstation'
ylchk "ygg-verify detection: another hostname uses ssh" 0 "workstation" 1
[ -d "$YL/rhome/eureka-verify/repos/repo.git" ] && echo "ok ygg-verify remote (mock ssh): mirror pushed to the remote ~/eureka-verify" || { echo "FAIL ygg-verify remote mirror missing: $(cat "$YL/ssh.log")"; fail=1; }
ylrun YL_HOSTNAME=yggdrasil-candidate YGG_HOST=ygg -- "$YLR" HEAD base 'echo explicit-host'
ylchk "ygg-verify detection: an explicit YGG_HOST keeps the ssh path" 0 "explicit-host" 1
ylrun YGG_VERIFY_LOCAL=maybe -- "$YLR" HEAD base 'true'
ylchk "ygg-verify detection: a bad YGG_VERIFY_LOCAL value is refused" 2 "must be 0, 1 or unset" 0
exit $fail