Files
codebase-memory-mcp/tests/test_test_impact_selection.sh
Martin Vogel 776bb119ac ci(test-impact): team artifact producer, PR shadow and the PR-CI narrowing switch
Producer. test-impact-artifact.yml publishes, on every push to main, the
commit's team artifact bundle: its frozen graph, built incrementally from the
previous bundle, and its per-test coverage map, re-run only for the suites
the push selects (scripts/ci/test-impact-publish.sh). A weekly run rebuilds
both from scratch and fails when the incremental graph's topology differs
from the full one. Bundles are Actions artifacts kept 30 days, never
committed.

Shadow. The report-only test-impact-shadow job also builds the checkout and
runs `test-impact select` against the base side of the PR merge commit
(scripts/ci/test-impact-select.sh). A bundle is used only when it comes from
a successful main-branch producer run for exactly that commit
(scripts/ci/test-impact-bundle.sh). The job stays continue-on-error and
outside ci-ok.

Narrowing switch, PR CI only. While the repository variable
TEST_IMPACT_MODE is `gate`, a select-tests job turns the engine's answer into
the tests the test-unix and test-windows legs run. Unset, nothing changes.
It fails open by construction:
- scripts/test-impact/selection.py is the only place an answer becomes a
  runner filter. Only a well-formed selected lane whose filter matches its
  own suite list narrows; run-all answers, a lane that selects nothing,
  unknown schemas or tokens, and any crash run everything.
- `test` passes a selection only from a successful, narrowed select-tests
  run; ci-ok does not need select-tests. A broken selection costs minutes,
  never coverage.
- scripts/run-tests-parallel.sh launches only the selected suites and keeps
  every guard on the selected list. A selected suite the build does not
  register fails the run, unless the engine saw its registration as
  conditional; then it is dropped with a notice, exactly as a full run of
  that build never runs it. Each runner judges its own tokens, so a selected
  test that does not exist fails its suite.
- The shard manifest records the selection's digest, and
  verify-shard-union.sh requires every shard of a leg to agree on it.
- Dry runs and releases never pass a selection (lane-wiring contract).

Also fixed on the way:
- publish crashed on any run-all push: a run-all lane carries
  "suites": null.
- the git-facts hostile-diff probe re-ran the runner with the parent's
  CBM_TEST_ONLY_FILE still set; under a narrowed run naming its suite it
  re-ran the spawning test, which spawned again. The child now clears it
  (a new test fails without that).
- the select summary crashed on the same null suites.

Contracts: tests/test_test_impact_selection.sh (Step 0z8),
tests/test_harness_selection.sh (Step 2c, the real runner through the real
harness), and new cases in the lane-gate and lane-wiring contracts.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-05 18:52:31 +02:00

93 lines
5.7 KiB
Bash

#!/usr/bin/env bash
# test_test_impact_selection.sh — contract of scripts/test-impact/selection.py,
# the only place a test-impact answer becomes what CI runs. `filter` narrows
# (exit 0, the lane's CBM_TEST_ONLY_FILE lines) only for a well-formed
# selected lane whose filter matches its own suite list; a skipped lane runs
# nothing (exit 11); everything else — run-all answers, unknown schemas or
# encodings, bad tokens, a filter that disagrees with the suites, an
# unreadable answer — must run everything (any other exit, no file left).
# `suites` lists what the team-artifact producer re-runs, `*` for run-all.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SEL="$ROOT/scripts/test-impact/selection.py"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
fail() { echo "FAIL: $*" >&2; exit 1; }
answer() { # name lane-json
printf '{"schema":"cbm.test_impact.v0","decision":"selected","lanes":[%s]}\n' "$2" > "$TMP/$1.json"
}
filter_exit() { # name -> exit code of `filter`
local rc=0
python3 "$SEL" filter "$TMP/$1.json" --lane unit --out "$TMP/$1.only" 2> /dev/null || rc=$?
echo "$rc"
}
# Narrowed: one whole suite, one suite by tests.
answer narrowed '{"lane":"unit","decision":"selected","suites":[
{"suite":"alpha","mode":"whole","whole_reason":["STATIC"]},
{"suite":"beta","mode":"tests","tests":[{"test":"b_one"},{"test":"b_two"}]}],
"runner_filter":{"encoding":"cbm-test-only-lines","value":"alpha\nbeta:b_one\nbeta:b_two\n"}}'
[ "$(filter_exit narrowed)" = 0 ] || fail "a well-formed selection did not narrow"
[ "$(cat "$TMP/narrowed.only")" = "$(printf 'alpha\nbeta:b_one\nbeta:b_two')" ] ||
fail "narrowed filter lines differ: $(cat "$TMP/narrowed.only")"
[ "$(python3 "$SEL" suites "$TMP/narrowed.json")" = "$(printf 'alpha\nbeta')" ] ||
fail "suites of a narrowed answer"
# --optional-out: the selected suites registered only conditionally.
answer conditional '{"lane":"unit","decision":"selected","suites":[
{"suite":"alpha","mode":"whole","whole_reason":["CONDITIONAL","STATIC"]},
{"suite":"gamma","mode":"whole","whole_reason":["STATIC"]}],
"runner_filter":{"encoding":"cbm-test-only-lines","value":"alpha\ngamma\n"}}'
rc=0
python3 "$SEL" filter "$TMP/conditional.json" --lane unit --out "$TMP/conditional.only" \
--optional-out "$TMP/conditional.optional" 2> /dev/null || rc=$?
[ "$rc" = 0 ] || fail "a selection with a conditional suite did not narrow"
[ "$(cat "$TMP/conditional.optional")" = alpha ] ||
fail "optional suites: $(cat "$TMP/conditional.optional")"
echo alpha > "$TMP/stale.optional"
answer stale '{"lane":"unit","decision":"run_all","suites":null,"runner_filter":null}'
rc=0
python3 "$SEL" filter "$TMP/stale.json" --lane unit --out "$TMP/stale.only" \
--optional-out "$TMP/stale.optional" 2> /dev/null || rc=$?
[ "$rc" != 0 ] && [ ! -e "$TMP/stale.optional" ] || fail "a run-all answer left an optional-suites file"
# Skipped lane: nothing to run, an empty file.
answer skipped '{"lane":"unit","decision":"skip","suites":[],"runner_filter":null}'
[ "$(filter_exit skipped)" = 11 ] || fail "a skipped lane did not report nothing-selected"
[ -f "$TMP/skipped.only" ] && [ ! -s "$TMP/skipped.only" ] || fail "a skipped lane must leave an empty file"
[ -z "$(python3 "$SEL" suites "$TMP/skipped.json")" ] || fail "suites of a skipped lane"
# Every one of these must run everything and leave no filter file.
printf '{"schema":"cbm.test_impact.v0","decision":"run_all","lanes":[{"lane":"unit","decision":"run_all","suites":null,"runner_filter":null}]}\n' \
> "$TMP/run_all.json"
answer lane_run_all '{"lane":"unit","decision":"run_all","suites":null,"runner_filter":null}'
printf '{"schema":"cbm.test_impact.v9","decision":"selected","lanes":[]}\n' > "$TMP/schema.json"
answer other_lane '{"lane":"integration","decision":"selected","suites":[{"suite":"alpha","mode":"whole"}],
"runner_filter":{"encoding":"cbm-test-only-lines","value":"alpha\n"}}'
answer encoding '{"lane":"unit","decision":"selected","suites":[{"suite":"alpha","mode":"whole"}],
"runner_filter":{"encoding":"csv","value":"alpha"}}'
answer no_filter '{"lane":"unit","decision":"selected","suites":[{"suite":"alpha","mode":"whole"}],"runner_filter":null}'
answer bad_token '{"lane":"unit","decision":"selected","suites":[{"suite":"alpha","mode":"whole"}],
"runner_filter":{"encoding":"cbm-test-only-lines","value":"alpha\n--all\n"}}'
answer disagrees '{"lane":"unit","decision":"selected","suites":[{"suite":"alpha","mode":"whole"},{"suite":"beta","mode":"whole"}],
"runner_filter":{"encoding":"cbm-test-only-lines","value":"alpha\n"}}'
answer empty_tests '{"lane":"unit","decision":"selected","suites":[{"suite":"alpha","mode":"tests","tests":[]}],
"runner_filter":{"encoding":"cbm-test-only-lines","value":"alpha\n"}}'
answer skip_with_suites '{"lane":"unit","decision":"skip","suites":[{"suite":"alpha","mode":"whole"}],"runner_filter":null}'
printf '{"schema":"cbm.test_impact.v0","decision":"selected","lanes":[' > "$TMP/truncated.json"
for name in run_all lane_run_all schema other_lane encoding no_filter bad_token disagrees empty_tests \
skip_with_suites truncated missing; do
rc="$(filter_exit "$name")"
case "$rc" in 0 | 11) fail "$name narrowed (exit $rc) instead of running everything" ;; esac
[ ! -e "$TMP/$name.only" ] || fail "$name left a filter file behind"
done
# A stale filter from an earlier run is removed, never reused.
echo "alpha" > "$TMP/run_all.only"
[ "$(filter_exit run_all)" != 0 ] && [ ! -e "$TMP/run_all.only" ] || fail "a stale filter file survived"
[ "$(python3 "$SEL" suites "$TMP/run_all.json")" = "*" ] || fail "suites of a run-all answer"
[ "$(python3 "$SEL" suites "$TMP/lane_run_all.json")" = "*" ] || fail "suites of a run-all lane (suites: null)"
echo "test-impact selection: all cases passed"