From 797bfc74477e54cd35cc2e1b111ca79e56b98cfa Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:57:44 +0800 Subject: [PATCH] test(control): make a new wire variant break the build, not a link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CONTROL_VERSION`'s doc states the rule — move it whenever a variant is added to or removed from `ControlRequest`, `ReplyOk` or `ControlEvent` — and states what happens when it is missed: the frame fails to decode, the read loop that failed on it takes the whole link down, and a remote workspace opens with no tabs. The same doc records that this is not hypothetical. The v5→v6 drift was most of the dialect, and all of it shipped against a number that never moved. Only one of the three enums was actually protected, and by accident: `ControlRequest::deadline` matches exhaustively, so a new request cannot be added without the compiler asking about it. `ReplyOk` and `ControlEvent` had nothing. Checked rather than assumed — a probe variant added to both compiled clean across the whole workspace, all targets, no warnings. The hand-written `every_reply` / `every_event` lists cannot catch it either. They are cross-checked against other hand-written lists, so a variant missing from the enum coverage *and* the list leaves every count in agreement. So: one exhaustive match per enum, in the test module, called from a test so it cannot rot into dead code. Confirmed it fails for the case it exists for — a probe variant in either enum is now an E0004 naming the variant, at compile time, where it cannot be skipped. --- crates/tty7-core/src/daemon/control.rs | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/tty7-core/src/daemon/control.rs b/crates/tty7-core/src/daemon/control.rs index 5129191f..3b56d027 100644 --- a/crates/tty7-core/src/daemon/control.rs +++ b/crates/tty7-core/src/daemon/control.rs @@ -1557,6 +1557,64 @@ mod tests { ] } + /// Adding or removing a variant of either enum stops this compiling, which + /// is the reminder that [`CONTROL_VERSION`] moves with it. + /// + /// [`ControlRequest`] already gets that for free: `deadline` matches it + /// exhaustively, so a new request cannot be added without the compiler + /// asking about it. These two had nothing — a variant could be added to + /// either and the whole workspace still built — and that is the shape of + /// the drift v6 had to pay off, where the number never moved and every + /// server of that generation answers the hello and then drops the link on + /// the first call. + /// + /// The lists in `every_reply` and `every_event` below are hand-written, so + /// they cannot catch this themselves: a variant missing from both the enum + /// coverage and the list leaves every count in agreement. + fn wire_variants_are_pinned(reply: &ReplyOk, event: &ControlEvent) { + match reply { + ReplyOk::Unit + | ReplyOk::Pong + | ReplyOk::Entries(..) + | ReplyOk::Meta(..) + | ReplyOk::Bool(..) + | ReplyOk::Path(..) + | ReplyOk::OptPath(..) + | ReplyOk::FileMeta { .. } + | ReplyOk::Hits(..) + | ReplyOk::Output(..) + | ReplyOk::WatchId(..) + | ReplyOk::Shells(..) + | ReplyOk::Attached { .. } + | ReplyOk::MachineTree(..) + | ReplyOk::WorkspaceTree(..) + | ReplyOk::TabTree(..) + | ReplyOk::Panes(..) + | ReplyOk::AgentStates(..) + | ReplyOk::Routes(..) + | ReplyOk::Status(..) => {} + } + match event { + ControlEvent::Watch { .. } + | ControlEvent::WatchOverflow { .. } + | ControlEvent::GitChunk { .. } + | ControlEvent::GitEnd { .. } + | ControlEvent::PaneExited { .. } + | ControlEvent::AgentStatus { .. } + | ControlEvent::Preempted { .. } + | ControlEvent::GuiOpen { .. } + | ControlEvent::Layout { .. } + | ControlEvent::LayoutResync => {} + } + } + + #[test] + fn a_new_wire_variant_has_to_move_the_dialect_number() { + // The two values are stand-ins. The exhaustive matches inside are the + // assertion, and the compiler is what checks them. + wire_variants_are_pinned(&ReplyOk::Unit, &ControlEvent::LayoutResync); + } + fn every_reply() -> Vec { vec![ ControlReply::Ok(ReplyOk::Unit),