Skip to content

fix(test): audit batch 3 — resurrect integration gate, vacuous assertions, installer, build freshness, CI hygiene#164

Merged
jraicr merged 6 commits into
devfrom
fix/audit-batch3-infra
Jun 15, 2026
Merged

fix(test): audit batch 3 — resurrect integration gate, vacuous assertions, installer, build freshness, CI hygiene#164
jraicr merged 6 commits into
devfrom
fix/audit-batch3-infra

Conversation

@jraicr

@jraicr jraicr commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Batch 3 (infra / test-harness) of the full-codebase audit of the v0.3.3 feature set: five fixes. The bulk of the diff is test coverage and a resurrected integration gate; ~100 lines of product/CI code. size:exception per the convention used for batches 1 (#162) and 2 (#163).

F1 — resurrect the dead REQ-N10 integration gate (8e40c26)

Two interlocking bugs made test/integration/session_lifecycle_compose_exec.bats — the self-described "empirical gate for REQ-N10" — completely unrunnable: (a) scripts/test.sh ran bats test/, which is NON-recursive, so the file was never collected by the test wrapper, ci.yml, or smoke.yml; (b) its skip guard called bats_skip_file, which does not exist in bats-core — without DRYDOCK_INTEGRATION=1 the file hard-errored (127) instead of skipping. Fixed: scripts/test.sh now collects recursively (bats -r test/); the guard uses the real mechanism (skip in setup_file(), verified on bats 1.13 to mark all tests skipped AND prevent setup() from touching docker); the gate is documented in CONTRIBUTING.md. Without the flag the 4 gate tests now report as clean skips; with it they run.

F2 — neutralize vacuous test assertions (99c0105)

Mid-test ! cmd lines are exempt from bats errexit and can never fail. A full sweep of the 53 candidates found 7 genuinely-vacuous mid-test negations guarding real contracts (cmd_attach, egress_smoke, lib_commands, the lib_compose DRYDOCK_SKIP_ENV_WRITE pair) — converted to run …; [ "$status" -ne 0 ]; 3 adjacent last-line negations converted for consistency; and the cmd_reap empty-uuid safety assertion, which was tautological even as a last line (mock-docker logs echo "$*", so the literal pkill -f "" it grepped for can never appear), rewritten to match the real failure signature (pkill -f[[:space:]]*$) and red-proofed against an injected empty-pattern bug. The remaining ~41 bare ! lines are effective final-command assertions, left unchanged.

F3 — install.sh: PATH line, clone updates, sentinel ordering (ec272c1)

Three fixes: (a) the appended shell-rc PATH line was hardcoded to ~/.local/bin while the prompt advertised $DRYDOCK_BIN_DIR — now built from the chosen dir (portable $HOME form for the default, literal absolute path for a custom one); (b) re-running the installer over an existing clone said "skipping clone" and left the old version — now does an --ff-only pull when the clone is on the tracking branch, warn-and-continue otherwise (detached/diverged/dirty/offline, with the local HEAD proven untouched); (c) the engram-shared INV-5 opt-in sentinel was written before the clone, so an aborted install left the opt-in behind with no drydock — the write now happens only after clone + symlink succeed.

F4 — Dockerfile keyscan honesty + drydock build freshness (2bae3f1)

The Dockerfile comment claimed the GitHub host-key bake was "Refreshed on every drydock build" — false, because cmd_build passed no --pull/--no-cache and the keyscan layer was cached indefinitely. cmd_build now always passes --pull (cheap base-image digest check; the only newly-broken case is a fully-warm offline rebuild, which was a no-op anyway), accepts --no-cache (forwarded to both image builds) and a --help flag, and rejects unknown args. The comment now states the real cache semantics.

F5 — CI hygiene (864f57c)

ci.yml and smoke.yml now also run on dev pushes (two individually-green PRs could otherwise break dev silently until the release PR); bats-core/bats-action pinned to the commit SHA behind tag 3.0.0 (verified via git ls-remote); the shfmt binary download is sha256-verified against the official v3.8.0 release checksum; permissions: contents: read added to the jobs that lacked it; test/integration/test_projects_submount.sh added to the shellcheck list; the CONTRIBUTING bats-tmp path corrected to the code's actual ~/.bats-tmp.

Verification

  • Full suite via the new scripts/test.sh: 1250 ok, 0 failures, 11 skips (4 = the REQ-N10 gate skipping cleanly, 7 = pre-existing DRYDOCK_INTEGRATION-gated managed-settings tests).
  • bash -n, shellcheck (full CI list + the newly-added integration .sh), shfmt -d, scripts/lint-commits.sh: clean.
  • Fresh-context adversarial review: READY-WITH-NITS — bats-action SHA and shfmt checksum independently re-verified, the F2 sweep re-run (all 41 remaining negations confirmed terminal), the F1 skip confirmed to fire before any docker touch. The three nits (drydock build --help, a CHANGELOG ### Fixed dedupe, the README build row) are applied in 3868ee4.

Notes / out of scope

  • --pull recommendation kept as-is (no escape hatch): the repo's only protected offline scenario is "image already present, no rebuild", which ensure_image never routes through cmd_build; a knob with no documented demand would violate the §4 minimalism doctrine.
  • smoke.yml now builds on every dev push (~5 min runner time) — an intentional mirror of main.

jraicr added 6 commits June 11, 2026 09:31
Two interlocking bugs made test/integration/session_lifecycle_compose_exec.bats
— the empirical gate for REQ-N10 — completely dead:

- scripts/test.sh defaulted to `bats test/`, which is non-recursive, so the
  file was never collected by the default run, ci.yml, or smoke.yml.
- The file's guard called `bats_skip_file`, which does not exist in
  bats-core: without DRYDOCK_INTEGRATION=1 the file hard-errored (127)
  instead of skipping.

Fix: default scripts/test.sh to `bats -r test/` and move the guard into
setup_file() using the real bats-core (>= 1.5) whole-file skip mechanism.
Verified on bats 1.13.0: without the flag all four L-A..L-D tests report as
skips and per-test setup() (which touches the Docker daemon) never runs; with
the gate flag set, setup_file falls through and the tests execute.

Document the gate in CONTRIBUTING.md and refresh the stale `bats test/`
references.
A bare `! cmd` line in a bats test is exempt from bats' errexit handling:
unless it is the very last command of the test function, its failure is
silently swallowed and the assertion can NEVER fail. A sweep of the test tree
(grep '^\s*! ' over test/**/*.bats, classifying each hit by whether the next
statement is the function's closing brace) found 7 genuinely vacuous mid-test
negations guarding real contracts:

- test/cmd_attach.bats (S-2B oneoff guard: no compose exec)
- test/egress_smoke.bats (_cleanup: pre-existing networks not created/removed)
- test/lib_commands.bats (cmd_sync: /src must not mount HOST_CLAUDE directly)
- test/lib_compose.bats x4 (submount managed-block removal + the
  DRYDOCK_SKIP_ENV_WRITE no-op contract, where BOTH negative greps were
  mid-test)

All 7 are converted to the effective house pattern
(`run grep ...; [ "$status" -ne 0 ]`), plus 3 adjacent last-line
negations converted alongside for consistency.

Also rewrites the D-7 empty-uuid safety assertion in test/cmd_reap.bats,
which was tautological even as a last line: mock-docker logs `echo "$*"`,
so an empty pkill argument leaves no quote characters in the log and
`! grep 'pkill -f ""'` could never match anything. The real failure
signature of an empty-pattern pkill is a logged line ending at `pkill -f`;
the assertion now matches that. Red-proven: temporarily making
_reap_orphan_claude issue `pkill -f ""` fails the new assertion.

The ~41 remaining `! cmd` lines in the tree are all effective final-line
assertions and are left unchanged.
…clones, defer INV-5 sentinel

Three installer fixes:

- The rc-file PATH export line was hardcoded to ~/.local/bin while the
  prompt advertised "$DRYDOCK_BIN_DIR": accepting the prompt with a custom
  bin dir appended a line that does not put drydock on PATH. The line is now
  built from DRYDOCK_BIN_DIR — the default keeps the portable literal-$HOME
  form, a custom dir is embedded verbatim — and the non-interactive hint
  lines use the same computed line.

- Re-running install.sh over an existing clone said "skipping clone" and
  left the old version in place. The re-run now fast-forwards the install
  branch (pull --ff-only); a clone on a different branch, detached HEAD,
  diverged history, or offline downgrades to warn-and-continue without
  touching local state.

- ask_engram_mode wrote the ~/.config/drydock/engram-shared sentinel at
  question time, BEFORE do_clone/do_symlink: an install aborting after the
  question left a live INV-5 shared-mode opt-in behind with no drydock
  installed. The question stays where it was; the write moves to a new
  write_engram_sentinel step that runs only after the install steps succeed.

All driven by test/install.bats (8 red tests before the fix; 35/35 green
after), HOME-isolated against local bare-repo fixtures.
…n comment

The Dockerfile comment on the github.com ssh-keyscan bake claimed the host
keys were "Refreshed on every drydock build" — false. cmd_build passed no
--pull or --no-cache, FROM debian:12-slim is an unpinned tag docker build
never re-pulls on its own, and the keyscan layer therefore stays cached
indefinitely.

Minimal, honest fix:

- cmd_build now passes --pull to both image builds (base-image freshness;
  cheap — re-downloads only when the tag's digest changes).
- New `drydock build --no-cache` passthrough forwards --no-cache to both
  builds for a full layer rebuild (e.g. after a GitHub host-key rotation);
  unknown arguments fail loudly. Help text updated.
- The Dockerfile comment now states the actual refresh semantics: cached
  until the base digest moves or the cache is bypassed with --no-cache.

No date-based cache-bust ARG — that would rebuild half the image on every
invocation. Driven by 3 red tests in test/lib_commands.bats (169/169 green
after).
… widen shellcheck

CI hygiene batch:

- ci.yml and smoke.yml trigger on pushes to dev as well as main — dev is the
  integration branch, where two individually-green PRs can land and break it
  silently until the release PR.
- The shellcheck gate now covers test/integration/test_projects_submount.sh
  (clean — no findings).
- bats-core/bats-action is pinned to the commit behind the mutable 3.0.0 tag.
- The shfmt v3.8.0 binary download is verified against the official sha256
  from the release's sha256sums.txt before being made executable.
- The lint, test, and smoke jobs carry an explicit
  'permissions: contents: read' block.
- CONTRIBUTING.md described the noexec bats-tmp redirect as
  ~/.cache/drydock/bats-tmp; scripts/test.sh actually uses ~/.bats-tmp. The
  doc now matches the code — the on-disk path is what container users already
  have, and relocating it would orphan existing ~/.bats-tmp dirs for no
  functional gain.

Plus the [Unreleased] changelog entries for the audit batch (F1-F5).
@jraicr jraicr added type:fix Bug fix size:exception Over 400 lines, pre-approved by maintainer labels Jun 15, 2026
@jraicr jraicr merged commit 455cd44 into dev Jun 15, 2026
4 checks passed
@jraicr jraicr deleted the fix/audit-batch3-infra branch June 15, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:exception Over 400 lines, pre-approved by maintainer type:fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant