Skip to content

e2e: the tmux suite never touches the developer's own background timer - #1638

Merged
AbirAbbas merged 3 commits into
devfrom
fix/1631-e2e-timer-stubs
Sep 28, 2026
Merged

AbirAbbas merged 3 commits into
devfrom
fix/1631-e2e-timer-stubs

Conversation

@AbirAbbas

Copy link
Copy Markdown
Collaborator

Closes #1631

The tmux suite's firing subtest approves a standing reminder in the real binary. The approval writes the timer definition under HOME and then runs systemctl --user enable --now codeaf-tick.timer, and the suite launched the binary with the developer's own HOME and PATH. So a run replaced the developer's codeaf-tick unit with one pointing at the test's temporary state root and the checkout's bin/codeaf, and every five-minute pass failed once that checkout was gone.

Every codeaf process the e2e package starts now goes through one of two doors: the tmux rig (startWithEnv, and so start and startFresh) or guardedCommand (tick, chat --once, engine --stop, do, manual). Both put systemctl, launchctl and crontab stubs first on PATH, which record their arguments and exit 0, and point HOME at a throwaway login folder beside the state root, unless the scenario named a HOME of its own. Stubs alone would not be enough: the product writes the unit files itself, in Go, before it calls the scheduler, so HOME has to move too. The state root, the provider key the rig hands the child, and the engine socket path are unchanged. The engine daemon the window spawns inherits the same HOME and PATH.

Validation contract

  • Every codeaf launch in the package goes through a guarded door, and a new launch that doesn't fails the untagged package test, naming the file and line. Proved by TestEveryLaunchOfCodeafStandsBehindTheHostGuard. It failed on the dev sources before the fix, naming all seven launchers, and it failed again when tick was temporarily put back to a bare exec.Command.
  • The gate catches each unguarded shape: a dynamic exec program, a literal codeaf, a tmux respawn built outside the rig, and an Env reassigned after the door. Proved by TestTheHostGuardGateCatchesAnUnguardedLauncher, against a synthetic source.
  • Under the guard, systemctl, launchctl and crontab resolve to the stubs, succeed, and log one line per call. A scenario's own HOME is kept, the developer's HOME is replaced, and a scenario's own PATH gets the stubs prepended. Proved by TestTheHostGuardStandsInForTheMachinesScheduler.
  • The product's own standing install, run under exactly the environment the doors hand a child, writes its definition into the throwaway login folder and leaves the machine's timer files byte-identical. Proved by TestAnInstallUnderTheGuardNeverReachesTheMachinesTimer, which refuses to call Install until both the stub and the throwaway HOME are confirmed.
  • End to end: TestTUIE2E/the_firing_reaches_the_person still passes. The machine's timer files hash the same before and after, as the subtest asserts itself. The stub log shows systemctl --user enable --now codeaf-tick.timer after the approval, and the rig's own service definition names the rig's CODEAF_HOME.

How it was verified

  • The tagged firing subtest was run once against a real model: it passed in 403s and did not skip. The stub log read systemctl --user daemon-reload and then systemctl --user enable --now codeaf-tick.timer. The definition sat under the rig's login folder with the rig's CODEAF_HOME and the checkout's binary. The SHA-256 of the developer's codeaf-tick.service and codeaf-tick.timer was identical before and after.
  • The engine daemon left running after the subtest carried the guarded HOME and PATH.
  • make pr-ready BASE=origin/dev passed, and go vet passed under the e2e, docker_e2e and ssh_bench tags.

What it deliberately does not do

🤖 Generated with Claude Code

AbirAbbas and others added 3 commits September 27, 2026 21:14
…duler stand-ins

Approving a standing reminder in the real binary writes the timer
definition under HOME and then runs `systemctl --user enable --now
codeaf-tick.timer`. The tmux suite launched the binary with the
developer's HOME and PATH, so the firing subtest replaced the
developer's own codeaf-tick unit with one pointing at the test's
temporary state root and the checkout's bin/codeaf, and every
five-minute pass failed once that checkout was removed (#1631).

Every launch in the package now goes through one of two doors: the tmux
rig (startWithEnv) or guardedCommand. Both put systemctl, launchctl and
crontab stubs first on PATH, which record their argv and exit 0, and
point HOME at a throwaway login folder beside the state root unless the
scenario named its own. CODEAF_HOME, the provider key the rig hands the
child and the engine socket path are unchanged.

The firing subtest now asserts the machine's timer files are
byte-identical before and after, that the approval asked the stub for
`enable --now codeaf-tick.timer`, and that the definition landed in the
rig's own login folder naming the rig's CODEAF_HOME.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…host guard

An untagged go/parser test reads every source in internal/e2e, tagged or
not, and names the file and line of any launch that could start codeaf
outside the two guarded doors: a dynamic exec program, a literal codeaf,
a tmux respawn or new-window built outside startWithEnv, a raw
os.StartProcess or exec.Cmd literal, or an Env/Path reassigned after
guardedCommand. It also fails when either door stops calling guardHost.
A synthetic source proves each shape is caught.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ground timer

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas merged commit b267d39 into dev Sep 28, 2026
4 checks passed
@AbirAbbas
AbirAbbas deleted the fix/1631-e2e-timer-stubs branch September 28, 2026 01:40
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.

e2e: the tmux suite takes over the developer's own background timer and leaves it pointing at a deleted checkout

1 participant