test(control): make a new wire variant break the build, not a link

`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.
This commit is contained in:
l0ng-ai
2026-08-16 01:57:44 +08:00
parent 652e4a230b
commit 797bfc7447
+58
View File
@@ -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<ControlReply> {
vec![
ControlReply::Ok(ReplyOk::Unit),