Skip to content

fix(test): the acceptance harness still installed the relay the removed way - #362

Merged
rappdw merged 5 commits into
mainfrom
fix/acceptance-harness-2.2.0
Sep 21, 2026
Merged

rappdw merged 5 commits into
mainfrom
fix/acceptance-harness-2.2.0

Conversation

@rappdw

@rappdw rappdw commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Found by your integration run on the host, against 2.2.0.

I shipped this broken, in the release that closed the umbrella about exactly this failure.

What happened

Phase E installed the relay by writing SANDY_HANDOFF_RELAY=.sandy/relay.sh into the isolated host config — which #354 made a hard error. So the launch was correctly refused, and the harness reported the relay supervisor as broken when what was broken was the harness. Nineteen FAILs against a feature that works.

It is the same shape #347 recorded throughout: an assertion correct about a mechanism that had been replaced. I updated phases A/B/C/D/F for the removal and never looked at E — because E tests the relay, and the relay survived. What changed underneath it was how you install one.

Fixed

  • phase E installs the relay as a manifest entry (a feature under $SANDY_HOME, privileged by where it lives, so no approval is involved)
  • its fixture reads SANDY_RELAY_STATE, not the retired SANDY_HANDOFF_*
  • state paths move from $SBX/handoff/relay to $SBX/relay-state — the source of the three No such file or directory lines
  • E10 breaks the entry (points it at a non-executable payload file) instead of a removed config key, which also moves that check onto the in-container refusal branch — the one the host-side path never covered
  • phase E removes its own feature before phase G, since its manifest selects every sandbox

Phase G: downstream failure, plus a real defect of its own

G's failure was not independent — E10's restore put SANDY_HANDOFF_RELAY back into the host config, so every later --start in the run hard-errored and CG came back empty.

But G had its own defect worth fixing regardless: three of its checks passed against an empty container id, because docker exec "" ... fails and "the write FAILED" is then true for the wrong reason.

That is precisely what the phase's own premise check exists to catch — and the premise did fail. But a phase whose premise is red must not print green assertions beneath it. Every check in G is now gated on a non-empty container id.

Also

§114(16g) is repointed at the property (refuse, name the rule, leave nothing behind) rather than at the fixture's spelling, so the next change of fixture shape does not read as a regression.

Not in this PR

The §14 below-floor failure in the same run is unrelated and pre-existing since 2.0.0: it greps for "refuses to launch against it", which last existed in v1.15.0 — #248 rewrote that message deliberately and the assertion never followed. Its sibling check is vacuous too. Worth its own PR.

The fleet-update acceptance failure (31 passed, 1 failed) is also not diagnosed here.

Verification

run-tests.sh §97 3/3, §114 124/124, §123 36/36 green; lint-bash32 clean. The acceptance harness itself needs a host run — it is Docker-dependent and cannot be exercised from inside sandy.

🤖 Generated with Claude Code

rappdw and others added 5 commits September 21, 2026 02:29
…ed way

Found by a real integration run on the maintainer's host, against 2.2.0.

I SHIPPED THIS BROKEN, in the release that closed the umbrella about exactly
this failure. Phase E installed the relay by writing
SANDY_HANDOFF_RELAY=.sandy/relay.sh into the isolated host config -- which
#354 made a HARD ERROR. So the launch was correctly refused, and the harness
reported the relay supervisor as broken when what was broken was the
harness. Nineteen FAILs against a feature that works.

It is the same shape this whole umbrella recorded: an assertion correct
about a mechanism that had been replaced. I updated phases A/B/C/D/F for the
removal and did not look at E, because E tests the relay and the relay
survived -- but HOW you install one changed underneath it.

FIXED:
  - phase E installs the relay as a manifest `entry` (a feature under
    $SANDY_HOME, privileged by where it lives, so no approval is involved);
  - its fixture reads SANDY_RELAY_STATE, not the retired SANDY_HANDOFF_*;
  - state paths move from $SBX/handoff/relay to $SBX/relay-state, which is
    why three lines were erroring with "No such file or directory";
  - E10 breaks the ENTRY (points it at a non-executable payload file)
    instead of a removed config key -- which also moves that check onto the
    in-container refusal branch, the one the host-side path never covered;
  - phase E now removes its own feature before phase G, since its manifest
    selects every sandbox.

PHASE G'S FAILURE WAS DOWNSTREAM, NOT INDEPENDENT: E10's restore put
SANDY_HANDOFF_RELAY back into the host config, so every later --start in the
run hard-errored. But G had a real defect of its own worth fixing anyway --
THREE OF ITS CHECKS PASSED AGAINST AN EMPTY CONTAINER ID, because `docker
exec "" ...` fails and "the write FAILED" is then true for the wrong reason.
That is precisely what the phase's own premise check exists to catch, and
the premise DID fail -- but a phase whose premise is red must not print
green assertions beneath it. Every check in G is now gated on a non-empty
container id.

§114(16g) is repointed at the property (refuse, name the rule, leave nothing
behind) rather than at the fixture's spelling, so the next change of fixture
shape does not read as a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sts.sh

Found by the maintainer's integration run once an expired host token stopped
masking everything else: acceptance-update-sessions.sh step 4 failed with
`AssertionError: 3` -- a schema_version pin still asserting 2.

#355 bumped schema_version and I updated the pins in run-tests.sh only. The
sweep should have been the whole tree, because a pin is a pin wherever it
lives, and the acceptance harnesses are exactly the ones CI never runs.

Also corrects README's "Also in 2.0" consumer note, which still told readers
schema_version is 2. It now names both values and says the thing the
downstream consumer worked out for themselves this week: treat it as an
OPAQUE TOKEN compared against a reviewed set, never with >=, so a future
bump is something you read rather than something you silently accept.

Verified no `schema_version == 2` pin remains anywhere in test/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The maintainer spotted this in a real run: section 23 printed nineteen FAIL
lines and the suite summary said "1 passed, 0 failed (of 1 run)".

THE SUMMARY BUG IS THE SERIOUS HALF, and it is not new -- it was latent in
all six acceptance wrappers and my broken harness merely triggered it:

    set +e; bash "$harness"; _acc_rc=$?; set -e
    _acc_res="$(grep -oE 'RESULT: ...' "$out" | tail -1)"   # <-- unguarded
    if [ "$_acc_rc" -eq 0 ]; then pass ...; else fail ...; fi

When a harness dies BEFORE printing its RESULT line, that grep matches
nothing and exits 1, which under the suite set -e aborts the whole run
BEFORE fail() is ever reached. The failure is never recorded. Demonstrated
both ways against a stub harness that exits 1: unguarded the wrapper dies
silently with nothing recorded; guarded it records the failure and
continues. All six wrappers are now guarded, and the message says "NO RESULT
LINE -- the harness died before printing one" rather than nothing.

WHY THE HARNESS DIED, which is entirely mine: replacing phase F in #355 also
deleted the file's RESULT/exit block, which lived after it. So the harness
never printed RESULT and never exited on its FAIL count -- it could not fail
the suite even in principle. Restored.

Two more stale assertions in phase E, same class as the last round:

  - pgrep still matched '\.sandy/relay\.sh', the path the fixture relay lived
    at when it was installed by config key. It is a feature payload now, so
    every "is the relay running" check missed a relay that was running. That
    is why E2/E3/E4/E7 failed while E7's state-persistence check PASSED --
    the relay was writing its file the whole time.
  - handoff_relay=true was asserted against the marker, and #355 removed that
    field. Now asserts relay.source=manifest, which is what replaced it.

Guarded by §152, which ratchets that every wrapper guards its grep AND that
every acceptance harness still ends by printing RESULT and exiting on its
FAIL count -- the second half is what would have caught the deleted block.
Both mutations verified: un-guarding one wrapper, and deleting one harness's
RESULT line, each turn it red.

It is a STATIC ratchet deliberately: the wrappers are inline in a 2000-line
script with no extractable seam, so a behavioural check would have to
re-implement the thing under test, which is how a guard ends up asserting
its own copy (§148(12), the same week).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…se premise

E1: the env-forwarding check still grepped for the relay at .sandy/relay.sh.
My earlier edit to that line did not apply (the search string carried a
`|| true` the file did not have) and nothing caught it, because a wrong
pattern makes grep -c return 0 rather than erroring. Verified the expected
value by running _sandy_fm_apply against an acc-relay-shaped feature rather
than reasoning about it: the entry resolves to
/opt/sandy/features/acc-relay/relay.

E10: "no daemon container was left behind" was true of the HOST-side refusal,
which happened before `docker run`. A manifest entry resolves to an
image-only path the host cannot stat, so the refusal is IN-CONTAINER:
user-setup.sh exits 1, the container dies, and --start classifies it as
CRASH-LOOPING (exit 7) rather than refused-before-launch (exit 6). The old
assertion was demanding a property this branch does not promise -- so it was
correct about the mechanism it was written for and wrong about the one that
replaced it, which is the fourth instance of that shape this week and the
second one I wrote myself.

It now asserts what criterion 7 actually requires: the launch reports
crash-looping rather than ready, and --stop cleans it up. A container that
exists but is crash-looping is LOUD; the failure the rule exists to prevent
is a container that is up with nothing delivering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The structural guard demanded 'no daemon container was left behind', which
is the HOST-side refusal's promise. A manifest entry is an image-only path,
so the refusal is in-container: exit 7 (crash-looping) rather than exit 6
(refused before launch), and a container does exist.

It now asserts what criterion 7 requires on either branch -- the launch
refuses, names the rule, reports crash-looping rather than ready, and leaves
nothing running.

Caught by run-tests.sh immediately after the harness change, which is the
structural guard doing exactly its job: it exists so a phase cannot be
quietly reduced to something weaker, and a reworded phase is indistinguishable
from a deleted one until someone looks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rappdw
rappdw merged commit e725b83 into main Sep 21, 2026
2 checks passed
rappdw added a commit that referenced this pull request Sep 21, 2026
…emoved way (#365)

Integration §25 failed all three cases at "daemon container is running",
in 5s, with no cause anywhere in the log. The harness sets the relay by
writing SANDY_HANDOFF_RELAY into the isolated host config -- a hard
error since 2.2.0 (#354), so every --start refused before docker run and
no container ever appeared.

This is #362 again in a different file. That fix swept
acceptance-handoff-dirs.sh and stopped there; the retired mechanism has
to be grepped for in BOTH roles -- what ASSERTS it and what SETS IT UP
-- across every harness, including the ones CI never runs. CI runs
neither of these.

Three changes:

- Install the relay as a feature manifest `entry` under $SANDY_HOME,
  the same shape acceptance-handoff-dirs.sh now uses. No config key is
  involved, and a manifest under $SANDY_HOME is privileged by location
  so no approval prompt appears -- which is what the surrounding `env -u
  SANDY_AUTO_APPROVE_PRIVILEGED` exists to prove.

- Move the three in-container paths off ~/.handoff/relay/, removed in
  2.2.0 (#353/#355), onto /opt/sandy/relay-state/ -- the rw mount that
  replaced it. The sentinel, the injector log and Claude Code's
  --debug-file all write there, so even a launch that had succeeded
  would have had nowhere to put them.

- KEEP THE --start OUTPUT. It was redirected to /dev/null, which is the
  whole reason a launch that refused in one line on stderr surfaced as
  three identical container-missing failures with the cause discarded.
  It now prints the tail of that output, and the exit code, when no
  container appears.

Not run here: this harness needs Docker, which Claude Code inside sandy
cannot reach. It needs a host run.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@rappdw rappdw mentioned this pull request Sep 21, 2026
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