test(integration): per-section timing, section selection, model pin, image warm-up - #135
Merged
Merged
Conversation
…image warm-up
The suite took ~60 minutes with no way to see where the time went, no way to
re-run a single section, and no protection against cold image builds landing
inside a 300s per-test timeout. Five changes, all confined to the harness.
1. Per-section timing + a slowest-first table. There was NO instrumentation at
all, so every optimization was guesswork. A `section "N. Title"` helper
replaces the 24 `info` headers, timing each via bash's SECONDS (BSD date
has no %N and bash 3.2 has no EPOCHREALTIME). Prints total wall-clock, and
still emits the table on the abort path.
2. Section selection: SANDY_INTEG_ONLY / SANDY_INTEG_SKIP (comma lists, exact
token match so ONLY=7 does not select 7b, SKIP wins over ONLY). Whitespace
is stripped, so a natural `ONLY=7, 13` works instead of silently running
nothing. Chasing one failure no longer costs a full run.
3. Fast-model pin: claude runs use Haiku instead of the default Opus, since
nearly every assertion here is about sandy's PLUMBING, not model quality.
Section 7 deliberately opts back out (SANDY_MODEL= -> sandy's own default,
which works because sandy uses ${SANDY_MODEL:-...} colon-dash at both
sites) so one canonical test still proves the default-model path.
SANDY_INTEG_NO_MODEL_PIN=1 restores previous behavior; an explicitly-set
SANDY_MODEL always wins. gemini/codex/opencode/grok are NOT pinned: no fast
model id for them is documented in this repo and inventing one would fail
the suite.
4. Image warm-up preflight. The per-test timeout bounds an agent TURN, not a
multi-minute build — but any base-Dockerfile change invalidates every image,
so the next run rebuilt the whole stack inside whichever test fired first.
That is exactly how the bookworm->trixie migration produced three
unrelated-looking failures (section 15b died with SIGALRM mid apt-get; the
codex launch blew 300s building base+proxy+codex, so it returned no answer
AND left no sandbox dir -> two failures). Warm-up builds credentialed
agents' images up front under SANDY_INTEG_BUILD_TIMEOUT (1800s), restoring
the per-test budget afterwards. ensure_image_built short-circuits on an
existing image, so this is a no-op on a warm host. Recorded as pseudo-
section 0 so build cost is visible but not selectable.
5. Fixed the last real failure. The corrupt-credentials test points SANDY_HOME
at a throwaway dir, so sandy found no .base_build_hash (sandy:6013) and
rebuilt the base with --no-cache --pull (6016) BEFORE reaching the
credential check ~2000 lines later (8136) — it timed out rather than
asserting. Seeding the throwaway SANDY_HOME with the real Dockerfiles and
build hashes skips every build: section 15 drops from 306s to seconds and
the test finally exercises what it is about.
First timed run (before fix 5): 1607s total. Acceptance harnesses 19/20/21 are
733s (46%); section 15's timeout 306s; gemini/grok/opencode 412s; the other 18
sections ~156s combined. That data says parallelism is NOT the next move — the
three harnesses cannot run concurrently (host-wide daemon state) and dominate
what remains. SANDY_INTEG_SKIP=19,20,21 alone takes a routine run to ~9.5min.
Portability: SECONDS (never assigned), parallel indexed arrays + scalar count
(no bash-4 associative arrays), POSIX case token matching (no awk, avoiding the
BWK \$ trap), ${arr[@]+"${arr[@]}"} for possibly-empty arrays under set -u,
sort -rn (no -V). Verified: 24 headers/24 guards/24 closers correctly
interleaved with per-section if/fi/case/do balance (bash -n cannot catch
misnesting); no cross-section variable reads (a set -u abort risk if a skipped
section owned a later section's variable); all run-tests.sh assertions about
this file still pass, including the section-80(b) awk range; zero new
shellcheck warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rappdw
added a commit
that referenced
this pull request
Aug 12, 2026
…136) Base-image modernization plus two additive features and a LAN-allowlist fix. Every pinned toolchain was on an unsupported or maintenance-only release: - Debian 12 bookworm left regular security support 2026-07-12 -> trixie (13), which brings system Python 3.11 -> 3.13. (#133) - Go 1.24 left its two-release support window entirely, and the pin was a Mar-2025 patch untouched for 16 months -> 1.26, with the newest patch now resolved at build time instead of frozen. The egress proxy's documented monthly --pull refresh had been silently no-op since ~Feb 2026 (no further 1.24.x pushes existed), so it was refreshing Debian but not the Go stdlib implementing its TLS/HTTP/CONNECT I/O; pinning a supported minor restores it. Node 22 (Maintenance LTS) -> 24 (Active LTS). (#131) - SANDY_EFFORT pins Claude Code reasoning effort and records it in the session marker, so a run's effort is provable rather than inferred. (#115) - SANDY_SESSION_NONCE lets an operator pin the attestation nonce so a harness can prove a run is the one it launched; env-only, so a committed workspace config cannot set it. (#118) - SANDY_ALLOW_LAN_HOSTS no longer reports success when iptables rejected the rule — a silently missing hole is the worst direction to be wrong in. (#119) Additive minor per the CLAUDE.md semver rule: new keys, no retiering or renames, introspection schema_version stays 1. SANDY_SANDBOX_MIN_COMPAT stays 0.7.10 — the Python bump moves where pip --user packages live, but the sandbox still works, so this is not a compat-floor event and no sandbox needs recreating. Four user-visible upgrade consequences are documented at the top of RELEASE_NOTES.md: the first launch rebuilds every image; persistent pip --user packages become invisible to 3.13 (sandy now detects and reports the stale tree); native Node addons may need npm rebuild; and binaries built inside sandy now link glibc 2.41. Also lands the test-suite work: three macOS-only failures CI structurally cannot see (#134) — including §68 executing the real sandy binary via a backtick inside a double-quoted python3 -c string — and per-section timing, section selection, a fast-model pin, and an image warm-up preflight for the integration suite (#135). Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The integration suite took ~60 minutes with no way to see where the time went, no way to re-run one section, and no protection against cold image builds landing inside a 300s per-test timeout. Five changes, all confined to the harness.
1. Per-section timing (the one that made the rest decidable)
There was zero instrumentation — no
SECONDS, no durations. Every optimization was guesswork. Asection "N. Title"helper replaces the 24infoheaders and times each via bash'sSECONDS(BSDdatehas no%N; bash 3.2 has noEPOCHREALTIME). Prints a slowest-first table plus total wall-clock, and still emits it on the abort path.2. Section selection
SANDY_INTEG_ONLY=13,19/SANDY_INTEG_SKIP=19,20,21. Exact-token matching, soONLY=7does not select7bandONLY=12does not select12b/12c; SKIP wins over ONLY; whitespace stripped so a naturalONLY=7, 13works rather than silently running nothing. Unknown tokens warn.Chasing one failure no longer costs a full run — the codex investigation that prompted this would have been
SANDY_INTEG_ONLY=2, about two minutes.3. Fast-model pin (claude only)
Nearly every assertion here is about sandy's plumbing — launch, mounts, proxy allow/deny, credentials, sandbox layout — not model quality. Claude runs now use Haiku instead of the default Opus.
SANDY_MODEL=→ sandy's own default), so one canonical test still proves the default-model path end to end. This works because sandy uses${SANDY_MODEL:-…}— colon-dash — at both sites, so empty is treated as unset. Verified in source before relying on it.SANDY_INTEG_NO_MODEL_PIN=1restores previous behavior; an explicitly-setSANDY_MODELalways wins.provider/modeldepends on which credential the runner has.4. Image warm-up preflight — and the real bug it fixes
The per-test timeout bounds an agent turn, not a multi-minute build. But any base-Dockerfile change invalidates every image, so the next run rebuilds the whole stack inside whichever test fires first.
That is exactly how the bookworm→trixie migration produced three unrelated-looking failures: §15b died with SIGALRM mid
apt-get install, and the codex launch blew its 300s budget building base+proxy+codex — returning no answer (→ one failure) and leaving no sandbox dir (→ a second failure). None of it was an agent bug, and a naive reading blamed codex.Warm-up builds credentialed agents' images up front under
SANDY_INTEG_BUILD_TIMEOUT(1800s), restoring the per-test budget afterwards.ensure_image_builtshort-circuits on an existing image, so this is a no-op on a warm host. Recorded as pseudo-section0so build cost is visible but deliberately not selectable.5. Fixed the last remaining failure
The corrupt-credentials test points
SANDY_HOMEat a throwaway dir, so sandy found no.base_build_hash(sandy:6013) and rebuilt the base with--no-cache --pull(6016) before reaching the credential check ~2,000 lines later (8136). It timed out rather than asserting anything.Seeding the throwaway
SANDY_HOMEwith the real Dockerfiles and build hashes skips every build: §15 drops from 306s to seconds and the test finally exercises what it is actually about.The data (first timed run, before fix 5)
Total 1607s.
This says parallelism is not the next move. The three acceptance harnesses cannot run concurrently (host-wide daemon state; §20's
--rebuilddeliberately marks other sessions stale) and dominate what remains.SANDY_INTEG_SKIP=19,20,21alone takes a routine run to ~9.5 minutes. Next best targets are §20's 434s (it runs--rebuild) and fast models for the three unpinned agents.Portability (this repo's recurring failure mode)
SECONDS(never assigned — an assignment would reset it); parallel indexed arrays + scalar count (no bash-4 associative arrays); POSIXcasetoken matching (no awk, avoiding the BWK\$trap);${arr[@]+"${arr[@]}"}for possibly-empty arrays underset -u;sort -rn(no-V). No process substitution, nopython3 -c "…", noset -Etraps — the exact classes that caused three macOS-only failures earlier today.Verification (static — Docker unavailable in the authoring environment)
bash -n; zero new shellcheck warnings vs HEADif/fi/case/dobalance —bash -ncannot catch misnestingset -uabort risk if a skipped section owned a later section's variable. The plan claimed "no dependencies"; verified directly rather than trustedrun-tests.shassertions about this file still pass, including the §80(b) awk range still containingPIPESTATUS/RESULT:Independent of #134 (different files); either can merge first.
🤖 Generated with Claude Code