refactor(cairn): give the effect runner a Runtime, and its lifecycle a test - #207
Merged
Merged
Conversation
9 tasks
…a test `event_loop` owned ten loose control locals — transfer_controls, ai_cancel, log_viewer_controls, pager_controls, session_controls, size_calc_cancel, two in-flight HashSets, an id counter and remote_edit_temps — and threaded every one of them through a sixteen-argument `dispatch` under `#[allow(clippy::too_many_arguments)]`. The cost was not the argument count. It was that the runtime's *lifecycle invariants* — which event releases which entry, and in what order relative to `update` — lived as a hand-maintained chain of eight `if let Msg::Event(..)` arms in the loop body, which needs a real `DefaultTerminal` and so could not be tested at all. Each one leaks something real when it is wrong: a transfer entry never removed holds a concurrency slot forever, an in-flight marker never cleared blocks every later open of that connection, a remote-edit temp never dropped leaves the file on disk. Now `Runtime` holds the tables and `Runtime::reap(&msg)` is that chain as ordinary code, with `RuntimeCtx` carrying the five things effects read and never mutate. `dispatch` takes three arguments. The body is unchanged — destructuring both structs at the top keeps every existing name bound, so this is genuinely a move, not a rewrite. Three tests, two of them mutation-checked, covering the invariants that had none: every terminal event releases its entry, and `WriteBackConflict` deliberately does *not* — that flow continues, and reaping it would delete the temp file out from under the user's next choice. That subtlety was a comment; it is now a test. The id families are deliberately not collapsed into one `TaskId`: a transfer, a log stream, a pager and a session are signalled differently and end differently, and separate keys keep that honest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SM3dDrioC5KntqjB6WDqjW
zoza1982
force-pushed
the
refactor/runtime-struct
branch
from
September 9, 2026 21:32
4d5b5a6 to
179da67
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
event_loop's ten loose control locals become oneRuntime; the five things effects read becomeRuntimeCtx;dispatchgoes from 16 arguments to 3. The pre-updatecleanup chain becomesRuntime::reap, which is testable.Why
The argument count was the symptom. The actual cost: the runtime's lifecycle invariants — which event releases which control entry, and in what order relative to
update— lived as a hand-maintained chain of eightif let Msg::Event(..)arms inside the loop body. That body needs a realDefaultTerminal, so none of it could be tested.grep 'event_loop\|dispatch('in the test module returned nothing.Each invariant leaks something real when it is wrong:
transfer_controlsentryopen_connection_in_flightmarkerremote_edit_tempsentryAnd one of them is subtle in the other direction:
WriteBackConflictmust not reap. That flow continues — the overlay is open andKeepEditing/SaveAsre-use the same temp file — so reaping it would delete the file out from under the user's next choice. That was a comment. It is now a test.Closes #
How
The body of
dispatchis unchanged. Destructuring both structs at the top rebinds every existing name, so this is genuinely a move rather than a rewrite — which is what makes it reviewable at this size.The id families are deliberately not collapsed into a single
TaskId, despite that being the obvious tidy-up and what LLD §5 originally sketched. A transfer, a log stream, a pager and a session are signalled differently and end differently; separate keys keep that honest, and the review that raised this agreed.next_archive_conn_idalso stays separate for a stated reason: unlike the maps, no event can ever reclaim an archive id, so it only grows.Testing
Three tests where there were none, two mutation-checked:
WriteBackConflictto the reap arma_writeback_conflict_keeps_the_remote_edit_tempConnectionOpenedreapevery_terminal_event_releases_its_control_entryOne
#[allow(clippy::too_many_arguments)]removed (7 remain elsewhere, untouched).Screenshots / output
No behaviour change, no UI change, no snapshot changed.
Checklist
maincargo fmt,cargo clippy -D warnings,cargo test,cargo docpass locallyRuntime,reap(including why the ordering matters) andRuntimeCtx; the staledispatchdoc describing the old parameters is replacedCHANGELOG.md— intentionally skipped: no user-facing changeDeferred (the rest of the review's track F)
event_loopstill takes 11 arguments. Most are genuinely per-run (terminal,state, the two receivers,input_gate) and folding them into a struct would obscure rather than clarify. Worth revisiting only if it grows.*Guardtypes (TransferDoneGuard,SessionDoneGuard,ConnectionOpenGuard,ConnectionTestGuard) share an armed/try_send/Drop shape and want one generic. Separate change — it touches every effect runner.update.rs(13k lines) is untouched here. Its seams are real (overlay handlers, transfer, pager, session, connection form) but splitting it is a much larger diff and deserves its own PR rather than riding along with this one.Risk & rollback
Runtime::new()setsnext_archive_conn_idtoARCHIVE_CONN_ID_BASErather thanDefault's0; a test pins that.🤖 Generated with Claude Code
https://claude.ai/code/session_01SM3dDrioC5KntqjB6WDqjW