test(protocol): make the cyclic all() prove it reaches every variant (improvise-5ubs)

AppMode::all() and Rejection::all() walk a total cyclic after(), and both doc
comments claimed a new variant could not be omitted. Half true, wrong half:
answering after() FOR a new variant is compiler-forced, pointing an existing
arm AT it is not, so a variant nothing names is never walked -- measured by
the PR #78 reviewer as all().len() == 17 against 18 variants, suite green.

Closing it needs an enumeration independent of after(). Stable Rust has none
for an enum with data -- variant_count is nightly, a derive macro is a new
proc-macro crate, and strum's EnumIter cannot construct AppMode::ItemAdd or
Editing at all. serde can: the Deserialize derive already builds the complete
variant-name list from the enum definition and publishes it in its
"unknown variant ... expected one of ..." message. variants.rs reads that back
and diffs it against the names the cycle reached, each taken off the variant's
own serialization rather than off Debug.

The helper is defensive because an unparsed message would return an empty list,
and an empty list passes every cycle; a should_panic test points it at a struct
and asserts the message, not merely that it panics.

Both overclaiming doc comments replaced. The fourth instance of this shape --
assert_eq!(cases.len(), 13) in ui/effect.rs -- is over a hand-built list of
effects rather than enum variants, so serde has nothing to say about it; left
filed rather than bodged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 03:52:04 -07:00
parent 20e9bd4106
commit 4e5f9f3ab8
5 changed files with 191 additions and 7 deletions
+36
View File
@@ -462,3 +462,39 @@ against an enum that really did have the variant.
Proved discriminating by re-adding the variant with an empty `apply` arm: the
retired spelling parses again and the test fails on `expect_err`.
### boundary_tests::the_app_mode_cycle_reaches_every_variant_serde_declares
### basis::tests::the_rejection_cycle_reaches_every_variant_serde_declares
improvise-5ubs. Both `all()` functions walk a cyclic `after()`, and both doc
comments claimed a new variant could not be omitted. Answering `after()` for a
new variant is compiler-forced; pointing an existing arm at it is not, so a
variant nothing names is never walked. The reviewer of PR #78 measured it:
`AppMode::all().len()` came back 17 against 18 variants, suite green.
The independent enumeration is serde's — the `Deserialize` derive builds the
variant-name list from the enum definition and publishes it in its
`unknown variant ... expected one of ...` message. `variants.rs` reads it back
and diffs it against the names the cycle reached, each name taken off the
variant's own serialization rather than off `Debug`.
Fault injected 2026-09-21: a `Rejection::Probe` variant added to the enum, with
an `after()` arm returning `DocumentReplaced` and **nothing pointing at it** —
precisely the shape the reviewer probed. `the_rejection_cycle_reaches_every_variant_serde_declares`
fails naming `["Probe"]`; the other 120 tests in `improvise-protocol`'s lib pass,
including `the_rejection_cycle` half of every basis test. (The workspace does not
compile with the probe, because `improvise-wasm-client/src/socket.rs` matches
`Rejection` exhaustively — which is a *different* guard and does not catch the
omission, since the omitted variant there is one it was forced to name.)
### variants::tests::a_message_that_is_not_an_unknown_variant_is_refused
The checker's own checker. A parse of serde's message that silently fails would
return an empty name list, and an empty list is a subset of everything — every
cycle would pass and the guard would be worse than absent. Pointing `declared`
at `Basis`, a struct, produces a message with no `expected one of` clause; the
helper must panic rather than return. Asserting the panic *message* and not
merely that it panics, because a helper that panicked for some other reason
would satisfy a bare `should_panic`.