Merge pull request #174 from fiddlerwoaroof/wire2/gho5

Rolled-back-load tests witness that the load ran (gho5)
This commit is contained in:
Ed Langley
2026-09-22 21:17:51 -07:00
committed by GitHub
2 changed files with 60 additions and 8 deletions
+33 -3
View File
@@ -2382,6 +2382,31 @@ keystroke to land in.
Injected fault: dropping `self.view_state = snapshot.view_state` from Injected fault: dropping `self.view_state = snapshot.view_state` from
`App::restore` fails this test and only this test. `App::restore` fails this test and only this test.
**Addendum (improvise-gho5).** Improvise-gho5 found that this
test and `a_rolled_back_load_tells_nobody_and_puts_every_mirror_back` passed
against a host that **never ran the load**. The review's fault, reproduced
here, was placed at the top of `Host::commands`: refuse any multi-command frame
that contains a `LoadModel` with `Rejected(DocumentReplaced)`, before
`CheckedBatch` runs. Both tests stayed green. A frame that never got that far
leaves the generation, the view, the status line and every mirror untouched,
which is exactly what the tests asserted.
Both tests now assert that the asker's whole reply is
`the_load_ran_then_the_write_broke()`, which is `Rejected(ChainBroken(North))`.
They also assert, before the frame, that `North` resolves. Together these make
the rejection a witness that the load ran. `ChainBroken` comes only from
`reduce::broken_chain`, which runs inside `mutating_frame` just before each
mutation. It names `North` only once the document no longer holds that
record, and only an applied `LoadModel` in this frame can have removed it.
With the same fault re-injected, both tests fail. The left side is
`[Rejected(DocumentReplaced)]` and the right is
`[Rejected(ChainBroken(<North>))]`. The fault was reverted.
The residual: a rogue up-front check that forged `ChainBroken` specifically
would still pass. That is the same forgery a hand-rolled client could commit
against any basis check, and a test cannot guard against it.
### a_refused_mutation_rolls_back_its_predecessor_and_the_abort_flag ### a_refused_mutation_rolls_back_its_predecessor_and_the_abort_flag
The `Rejection::MutationRefused` arm — the one the first hand-back wrongly called The `Rejection::MutationRefused` arm — the one the first hand-back wrongly called
@@ -4885,9 +4910,14 @@ Three things make it non-vacuous:
`LoadModel` silently failed, `Applied::Rebased` would never run and every `LoadModel` silently failed, `Applied::Rebased` would never run and every
assertion here would pass over nothing. The control asserts the generation assertion here would pass over nothing. The control asserts the generation
moves and that *both* clients are owed a `Snapshot`. moves and that *both* clients are owed a `Snapshot`.
- **The rollback is asserted before anything is concluded from it** — the - **The load is witnessed to have RUN before anything is concluded from the
`document_generation` equality is checked first, so "the frame was rejected" rollback** (improvise-gho5). The asker's whole reply must be
cannot be confused with "the frame never got that far". `Rejected(ChainBroken(North))`. `North` is asserted to resolve before the
frame, so only `broken_chain` can give that answer, and only after the
`LoadModel` has replaced the document. `CheckedBatch` never produces
`ChainBroken`. This bullet used to say the `document_generation` equality
did the job, and that was false: generation equality is exactly what holds
when the frame never got that far.
- **A bystander is seated where a wrong relay could move it.** A single-client - **A bystander is seated where a wrong relay could move it.** A single-client
fixture cannot tell "the mirrors were restored" from "there was one mirror and fixture cannot tell "the mirrors were restored" from "there was one mirror and
it happened to be the asker's". it happened to be the asker's".
+27 -5
View File
@@ -1049,6 +1049,12 @@ mod tests {
let was_view = host.app().view_state.active_view.clone(); let was_view = host.app().view_state.active_view.clone();
let was_status = host.app().view_state.status_msg.clone(); let was_status = host.app().view_state.status_msg.clone();
let basis = frame_basis(&host, &["North"]); let basis = frame_basis(&host, &["North"]);
assert!(
host.app()
.resolve_record(addressed("North").record)
.is_some(),
"North must resolve BEFORE the load, or a refusal naming it proves nothing"
);
// The load lands, then the write names a record the incoming document // The load lands, then the write names a record the incoming document
// has never had — so the frame fails at its second command. // has never had — so the frame fails at its second command.
@@ -1069,11 +1075,10 @@ mod tests {
]), ]),
); );
assert!( assert_eq!(
answer to(&answer, asker),
.iter() vec![the_load_ran_then_the_write_broke()],
.any(|a| matches!(a.msg, ServerFrame::Rejected(_))), "only a load that RAN can refuse the write this way: {answer:?}"
"the fixture must actually refuse the frame: {answer:?}"
); );
assert_eq!( assert_eq!(
host.app().view_state.active_view, host.app().view_state.active_view,
@@ -1097,6 +1102,12 @@ mod tests {
); );
} }
/// `North` resolves in the outgoing document, so only `broken_chain` AFTER the
/// load applied can answer this. Evidence: `context/test-evidence/command.md`.
fn the_load_ran_then_the_write_broke() -> ServerFrame {
ServerFrame::Rejected(Rejection::ChainBroken(addressed("North").record))
}
/// Everything one client was told, flattened to the projections it carries. /// Everything one client was told, flattened to the projections it carries.
fn projections(frames: &[Addressed], id: SessionId) -> Vec<Projection> { fn projections(frames: &[Addressed], id: SessionId) -> Vec<Projection> {
to(frames, id) to(frames, id)
@@ -1147,6 +1158,12 @@ mod tests {
let (bystander, _) = host.connect(10, 10); let (bystander, _) = host.connect(10, 10);
let was_generation = host.app().document_generation(); let was_generation = host.app().document_generation();
let basis = frame_basis(&host, &["North"]); let basis = frame_basis(&host, &["North"]);
assert!(
host.app()
.resolve_record(addressed("North").record)
.is_some(),
"North must resolve BEFORE the load, or a refusal naming it proves nothing"
);
// The load lands and rebases every mirror; the write then names a // The load lands and rebases every mirror; the write then names a
// record the incoming document has never had, so the frame fails. // record the incoming document has never had, so the frame fails.
@@ -1167,6 +1184,11 @@ mod tests {
]), ]),
); );
assert_eq!(
to(&answer, asker),
vec![the_load_ran_then_the_write_broke()],
"only a load that RAN can refuse the write this way: {answer:?}"
);
assert_eq!( assert_eq!(
host.app().document_generation(), host.app().document_generation(),
was_generation, was_generation,