CB-WP-0035: a session that answers nothing
Some checks failed
ci / check (push) Failing after 4s

Two reports, one cause: the script's fetch had no .catch. When the server
had exited the promise rejected, the chain never ran, and not even the
status line moved — so a dead server and a click that did nothing were
indistinguishable. play again read as a dead button, and the linger
timeout was invisible.

Now a rejection says the session is gone and seals the page, and a
5-second heartbeat against a new /alive notices it without needing a
click, which is what the timeout case requires. The beat carries the
token, and does not extend the linger: that deadline is absolute.

The harness had the same hole. jsrun's fetch stub had no .catch, so the
branch that notices a dead server would have been unreachable in every
test — the very defect the stub's own comment records from CB-WP-0024.
Teaching it __failing, .catch and a recorded setInterval was the fix;
writing the script defensively would have repeated the trap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-07 20:31:32 +02:00
parent c2a7e40b67
commit 82aead6cea
9 changed files with 448 additions and 7 deletions

View file

@ -52,6 +52,7 @@ pub use doc::{document, text_of, Endpoints};
/// The endpoint pair every test in this crate posts to.
#[cfg(test)]
const TEST_ENDPOINTS: Endpoints<'static> = Endpoints {
alive: "/alive?t=x",
command: "/command?t=x",
note: "/note?t=x",
};
@ -608,6 +609,84 @@ mod gamelog {
}
}
/// CB-WP-0035. A server that has gone must be NOTICED.
///
/// The script's `fetch` had no rejection handler, so when the process
/// had exited the promise rejected and the chain never ran — not even
/// the status line moved. Reported twice: *"play again does not
/// report that the session is no longer available"*, and the linger
/// timeout going unnoticed.
///
/// **Both from one cause**, so both are asserted: the click path and
/// the heartbeat that needs no click.
#[test]
fn a_session_that_answers_nothing_is_reported_and_sealed() {
let page = crate::doc::ending(
None,
"the game ended",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
);
for (beat, what) in [(false, "pressing a control"), (true, "the heartbeat")] {
let (live, status) = crate::jsrun::unanswered(&page, beat).expect("run the page");
assert!(
status.contains("no longer available"),
"{what} said nothing about the session: {status:?}"
);
assert!(
status.contains("[sealed]"),
"{what} left the page looking live: {status:?}"
);
assert!(
live.is_empty(),
"{what} left {live:?} still droppable on a dead session"
);
}
}
/// **Both** pages must carry the heartbeat (CB-WP-0035).
///
/// The first cut wired it into the ending page only, and the gate
/// caught it as an unused variable — which is luck, not a control.
/// The timeout matters MOST during play: that is when a player walks
/// away, and the table is the thing that must stop looking live.
#[test]
fn every_page_can_tell_the_session_has_gone() {
let playing = document_with_log(
&crate::testfix::view(Some(PlayerId(0))),
&[],
crate::TEST_ENDPOINTS,
Some(PlayerId(0)),
false,
Account::of(&[]),
&[],
);
let ended = crate::doc::ending(
None,
"m",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
);
for (name, page) in [("the table", &playing), ("the ending page", &ended)] {
assert!(
page.contains("CB_ALIVE"),
"{name} cannot notice the session ending"
);
// With the token: an unauthenticated heartbeat is refused,
// and a refusal every 5s would seal a LIVE session.
assert!(
page.contains("/alive?t="),
"{name}'s heartbeat carries no token"
);
}
}
/// CB-WP-0034. The move button must name **who**.
///
/// Reported three times across three sessions, two days apart, and it
@ -1618,6 +1697,7 @@ mod ending_page {
"the game ended",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
)
@ -1704,6 +1784,7 @@ mod ending_page {
"m",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
),
@ -1762,6 +1843,7 @@ mod ending_page {
"m",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
))
@ -1786,6 +1868,7 @@ mod ending_page {
"P1 ran out of input",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
));
@ -1805,6 +1888,7 @@ mod ending_page {
"m",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
));
@ -1835,6 +1919,7 @@ mod ending_page {
"m",
"/command?t=x",
None,
"/alive?t=x",
crate::doc::Account::of(&[]),
&[],
));