Skip to content

test(integration): per-section timing, section selection, model pin, image warm-up - #135

Merged
rappdw merged 2 commits into
mainfrom
test/integ-timing-selection-warmup
Aug 12, 2026
Merged

test(integration): per-section timing, section selection, model pin, image warm-up#135
rappdw merged 2 commits into
mainfrom
test/integ-timing-selection-warmup

Conversation

@rappdw

@rappdw rappdw commented Aug 11, 2026

Copy link
Copy Markdown
Owner

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. A section "N. Title" helper replaces the 24 info headers and times each via bash's SECONDS (BSD date has no %N; bash 3.2 has no EPOCHREALTIME). 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, so ONLY=7 does not select 7b and ONLY=12 does not select 12b/12c; SKIP wins over ONLY; whitespace stripped so a natural ONLY=7, 13 works 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.

  • Section 7 deliberately opts back out (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=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 anywhere in this repo, and inventing one would fail the suite. For opencode it is also structurally unsafe — the right provider/model depends 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_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 deliberately not selectable.

5. Fixed the last remaining 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 ~2,000 lines later (8136). It timed out rather than asserting anything.

Seeding the throwaway SANDY_HOME with 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.

Bucket Time Share
Acceptance harnesses (19+20+21) 733s 46%
§15 timeout (fixed here) 306s 19%
gemini 157 + grok 139 + opencode 116 412s 26%
Other 18 sections combined ~156s 10%

This says parallelism is not the next move. The three acceptance harnesses cannot run concurrently (host-wide daemon state; §20's --rebuild deliberately marks other sessions stale) and dominate what remains. SANDY_INTEG_SKIP=19,20,21 alone 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); POSIX case token matching (no awk, avoiding the BWK \$ trap); ${arr[@]+"${arr[@]}"} for possibly-empty arrays under set -u; sort -rn (no -V). No process substitution, no python3 -c "…", no set -E traps — 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 HEAD
  • 24 headers / 24 guards / 24 closers correctly interleaved, with per-section if/fi/case/do balancebash -n cannot catch misnesting
  • No cross-section variable reads — a set -u abort risk if a skipped section owned a later section's variable. The plan claimed "no dependencies"; verified directly rather than trusted
  • All run-tests.sh assertions about this file still pass, including the §80(b) awk range still containing PIPESTATUS/RESULT:
  • Selection semantics unit-tested against the extracted functions
  • Timeout save/restore verified for both the set and previously-unset cases

Independent of #134 (different files); either can merge first.

🤖 Generated with Claude Code

rappdw and others added 2 commits August 11, 2026 22:43
…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
rappdw merged commit 32099b9 into main Aug 12, 2026
2 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant