Skip to content

test suite wipes the developer's live radio session 57× per run: isolate $TASK_FORCE_HOME suite-wide (#203) - #205

Merged
martin-conur merged 1 commit into
mainfrom
task/test-radio-home-isolation
Sep 14, 2026
Merged

martin-conur merged 1 commit into
mainfrom
task/test-radio-home-isolation

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Closes #203

Problem

./run_tests.sh wiped the live radio session of whoever ran it, 57 times per run.

tests/task_done.bats and tests/task_done_dispatcher.bats built their setup() without any radio-home helper, so $TASK_FORCE_HOME was never overridden and radio/task-done resolved their mailbox root to the developer's real ~/.task-force. $TASK_FORCE_ROLE is inherited from the tab, so the destructive command those suites exercise — radio unregister --manual, a wipe that is unconditional by design (#198) — landed on the runner's own session file.

Reproduced before touching anything, with $HOME redirected at a tempdir:

$ env HOME=$FAKE ./run_tests.sh task_done
$ grep -c 'unregister role=' $FAKE/.task-force/radio/log
51

51 (task_done) + 6 (task_done_dispatcher) = the 57 the issue measured.

Approach

The issue offered two options and called the second stronger — make isolation the default rather than something each file has to remember. This does that, with the loud guard as a second layer.

One deviation from the spec text: the helper is named setup_task_force_home (tests/helpers/common.bash:133), not setup_radio_home.

tests/setup_suite.bash (new) rather than run_tests.sh. bats resolves setup_suite.bash from the folder of the first test file and runs it once per invocation, so a bare bats tests/foo.bats gets the isolation too — not just ./run_tests.sh. It honours a caller-supplied $TASK_FORCE_HOME but only after proving it isolated.

tests/helpers/radio_home.bash (new) holds task_force_home_is_isolated / require_isolated_task_force_home, shared by setup_suite.bash and common.bash rather than copied.

tests/helpers/common.bash — every suite loads it, so it's the chokepoint: the run aborts with an explanation if $TASK_FORCE_HOME is unset or aimed at the real home. Enforcement is skipped on bats' test-gathering pass (BATS_TEST_NAME=source), which runs before setup_suite and where an unset value proves nothing; a value that is set and wrong is rejected on every pass.

Both suites get the per-test setup_task_force_home they were missing, and teardown_all now removes $TASK_FORCE_HOME only when the test created it (new TASK_FORCE_HOME_OWNED flag) — otherwise a per-test teardown would delete the run-scoped home out from under the rest of the run. The five now-redundant in-test calls in task_done.bats are dropped.

Acceptance

Criterion Result
./run_tests.sh → delta 0 on grep -c 'unregister role=' ✅ 3276 → 3276
./run_tests.sh task_done → delta 0
./run_tests.sh task_done_dispatcher → delta 0
A file omitting the helper fails loudly instead of writing to the real mailbox ✅ (and is isolated by default)
Nothing written under $HOME/.task-force/sessions/, mailbox/, log together ✅ 0 files under a fake $HOME

tests/radio_home_isolation.bats (9 tests) is the standing guard. It drives real bats child-runs of both suites under a throwaway $HOME and asserts all three paths stay absent, with the log pre-created so the check is "nothing was appended", not merely "nothing was created". It also stands up a probe suite carrying only setup_suite.bash + the helper to prove a file that never calls setup_task_force_home still comes out isolated, and asserts that pointing $TASK_FORCE_HOME at the real home aborts loudly.

Confirmed as real regression tests: with both layers reverted, 7 of the 9 fail; with the fix in place, 9/9 pass.

Green

  • ./run_tests.sh1000/1000, 0 failures (991 before + 9 new)
  • tools/check-drift.sh — 21 groups checked, clean
  • shellcheck -x tests/helpers/*.bash tests/setup_suite.bash run_tests.sh — clean
  • .github/workflows/ci.yml lint list extended to cover tests/setup_suite.bash (it sits outside the existing tests/helpers/*.bash glob)

Checklist

  • Changelog — entry under ## [Unreleased] / Fixed. Tests + README + CI only, no task-init re-run needed; no installer-written artifact changed.
  • Docs — README gains a "Radio-home isolation" note in the Testing section, a radio_home_isolation.bats row in the suite table, the two new infrastructure files, and a pointer in the Contributing test pattern. The four steering/*.example.md templates and the .claude/*-workflow.md docs are untouched: nothing model-facing or user-visible in the loadouts changed.
  • Reuse — the guard lives in one file sourced by both consumers rather than duplicated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DEuwfwn9bNWj4wc3WPDipT

…ate $TASK_FORCE_HOME suite-wide

task_done.bats and task_done_dispatcher.bats built setup() without any
radio-home helper, so radio/task-done resolved their mailbox root to the real
~/.task-force. $TASK_FORCE_ROLE is inherited from the tab, so the
`radio unregister --manual` those suites exercise — a wipe that is
unconditional by design (#198) — landed on the runner's own session file:
51 + 6 = 57 wipes per ./run_tests.sh, matching the measured full-suite delta.

Isolation is now the default rather than something each file must remember:

- tests/setup_suite.bash runs once per bats invocation and exports a scratch
  $TASK_FORCE_HOME. bats resolves setup_suite.bash from the folder of the
  first test file, so a bare `bats tests/foo.bats` gets it too, not just
  ./run_tests.sh.
- tests/helpers/radio_home.bash holds the guard; common.bash (loaded by every
  suite) aborts with an explanation when $TASK_FORCE_HOME is unset or points
  back at the real home. Skipped only on bats' pre-setup_suite gathering pass,
  where an unset value proves nothing.
- Both suites get the per-test setup_task_force_home they were missing, and
  teardown_all now removes $TASK_FORCE_HOME only when the test created it, so
  it can't delete the run-scoped one.
- tests/radio_home_isolation.bats drives real bats child-runs of both suites
  under a throwaway $HOME and asserts sessions/, mailbox/ and log stay absent
  (log pre-created, so the check is "nothing appended"), plus that a subverted
  $TASK_FORCE_HOME fails loudly.

Verified delta 0 against `grep -c 'unregister role=' ~/.task-force/radio/log`
for the full suite and for each suite individually. Tests only — no installed
artifact changed, so no task-init re-run is needed.

Closes #203

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEuwfwn9bNWj4wc3WPDipT
@martin-conur
martin-conur merged commit 66b349d into main Sep 14, 2026
4 checks passed
martin-conur added a commit that referenced this pull request Sep 14, 2026
…t just a stale binary

Rebased onto #205, which proves the diagnosis in the flapping row was
incomplete. `--manual` short-circuits the `if [[ "$manual" != true ]]` block
that emits BOTH the `skipping` and `proceeding` lines, so a manual wipe
writes `unregister role=` and nothing else. Anything calling it in a loop
therefore inflates that counter on its own — exactly what #205 found, where
an unisolated test suite unregistered the developer's live role 57× per run.

The original wording attributed a role=/proceeding gap solely to an old
`radio` binary on PATH. That would have misdiagnosed the one real incident
we have data for: I hit this exact gap while researching the runbook (1368
wipes in a day against 3 `proceeding` lines) and wrongly concluded stale
binary. #205 is the actual explanation.

All nine docs now name the --manual cause first and keep the stale-binary
one second. New test 8 pins it, asserting both causes appear in every copy
and that bin/radio really does gate both log lines behind the manual check.

Verified against #205's harness: full suite 1013/1013 with delta 0 on
`grep -c 'unregister role=' ~/.task-force/radio/log`, sessions/ and log line
count — this branch does not break the isolation invariant #205 added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzeuHXdqABg52t7ug7UDje
martin-conur added a commit that referenced this pull request Sep 14, 2026
…nostics, post-#187/188/189/190 behavior (#204)

* docs: 'When radio misbehaves' runbook — symptom→cause table, log diagnostics, post-#187/188/189/190 behavior

Radio's docs described the happy path well and the failure path barely: there
was no place a user or an agent could look to answer "radio isn't working —
now what?". Adds a consolidated runbook to the README's radio section and to
all seven workflow templates (plus this repo's dogfooded copy).

Covers the five symptoms people actually report: a ping that went nowhere,
`radio check` sitting unsubmitted in a prompt box (#189), a role that keeps
vanishing from sessions/ (flapping, and how to confirm #187/#198's guard is
the binary actually running), `radio unregister` doing nothing (expected
since #198), and a role idling on a message in its own inbox (fixed in #197,
with BLOCKED_IDS= named).

Documents ~/.task-force/radio/log as a diagnostic for the first time —
delivery rate, wake failures bucketed by reason, wipes vs. refusals, the
#198 stdin shape, the #188 tab_id_src= re-seed fields. Every grep in the
runbook was validated against a real 6,300-line production log.

Two honesty gaps the spec named are closed: undelivered mail is never
dropped (written to the inbox before any wake; gc never touches inbox/ and
refuses to reclaim a mailbox holding unread mail) and there is **no**
dead-letter queue — the spec assumed #188 shipped one, it did not, so the
runbook says so rather than sending readers after a directory radio never
creates. And delivery is not symmetric: the claude copy names the three
backstops it may promise, the kiro copy names none and points at the
agent's own poll (#190).

One asymmetry surfaced while writing and is now documented: kiro's
task-work parses no --auto flag, so AUTO_MODE is never set and
TASK_FORCE_AUTO_SUBMIT is never injected for a kiro worker — its wakes
always end in LF and always need the keypress.

tests/radio_runbook.bats (12 tests) pins block identity within each loadout
group, #177 neutrality, the claude/kiro asymmetry, and — load-bearing —
that every log string the runbook greps for is a literal substring of
bin/radio, so a rename there fails the suite instead of quietly turning the
runbook into a page of commands that match nothing. Mutation-checked.

Also replaces the `--owner ignored` sentinel in the two task-init overwrite
tests: it scans the whole rendered doc, so an ordinary English word
false-fails as soon as the template prose uses it (which this runbook does).

Docs only — bin/radio is untouched.

Closes #191

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzeuHXdqABg52t7ug7UDje

* radio runbook: name --manual as a cause of a role=/proceeding gap, not just a stale binary

Rebased onto #205, which proves the diagnosis in the flapping row was
incomplete. `--manual` short-circuits the `if [[ "$manual" != true ]]` block
that emits BOTH the `skipping` and `proceeding` lines, so a manual wipe
writes `unregister role=` and nothing else. Anything calling it in a loop
therefore inflates that counter on its own — exactly what #205 found, where
an unisolated test suite unregistered the developer's live role 57× per run.

The original wording attributed a role=/proceeding gap solely to an old
`radio` binary on PATH. That would have misdiagnosed the one real incident
we have data for: I hit this exact gap while researching the runbook (1368
wipes in a day against 3 `proceeding` lines) and wrongly concluded stale
binary. #205 is the actual explanation.

All nine docs now name the --manual cause first and keep the stale-binary
one second. New test 8 pins it, asserting both causes appear in every copy
and that bin/radio really does gate both log lines behind the manual check.

Verified against #205's harness: full suite 1013/1013 with delta 0 on
`grep -c 'unregister role=' ~/.task-force/radio/log`, sessions/ and log line
count — this branch does not break the isolation invariant #205 added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzeuHXdqABg52t7ug7UDje

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@martin-conur
martin-conur deleted the task/test-radio-home-isolation branch September 15, 2026 18:57
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.

test suite wipes the developer's live radio session 57× per run — task_done.bats + task_done_dispatcher.bats lack radio-home isolation

1 participant