live-server: a departed overlay's report is not a roll-call word

A claim carrying a clientId no connection holds any more (the page
unloaded between the broadcast and the claim landing) was still recorded,
so a departed tab's busy or no_match report could complete the roll call,
or set its verdict, against the overlays that remain. Such a report is
now answered `{granted:false, pending:true}` and not recorded, while any
connection that sent no clientId keeps every id counted as connected.
Eligible claims are left as they were: a lease a departed page holds
lapses and a rescuer takes it, and refusing them would also refuse the
renew a live overlay sends inside an EventSource reconnect gap.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-15 05:45:49 +05:00
committed by Abdul Wahab
co-authored by Claude Fable 5.1
parent 2f4d4fb4fc
commit 2d2009d7c1
3 changed files with 59 additions and 2 deletions
+35
View File
@@ -387,6 +387,41 @@ fn agent_target_roll_call_counts_overlays_not_connections() {
let _ = &mut a2;
}
#[test]
fn agent_target_ignores_the_word_of_a_departed_overlay() {
let s = Server::start("departed");
let mut a = Overlay::connect(s.port, &s.token, "tab-a");
let mut b = Overlay::connect(s.port, &s.token, "tab-b");
a.next(|m| m["type"] == "connected");
b.next(|m| m["type"] == "connected");
// B's page goes away; wait until the helper has seen it leave.
drop(b);
let mut alone = false;
let mut last = serde_json::Value::Null;
for _ in 0..40 {
let (_, body) = http(s.port, "GET", &format!("/status?token={}", s.token), None);
let status: serde_json::Value = serde_json::from_str(&body).unwrap_or(serde_json::Value::Null);
if status["connectedClients"] == serde_json::json!(1) {
alone = true;
break;
}
last = status;
std::thread::sleep(Duration::from_millis(15));
}
assert!(alone, "the helper noticed B leave: {last}");
let held = s.hold(serde_json::json!({}));
let target_id = a.next(|m| m["type"] == "agent_target")["targetId"].as_str().unwrap().to_string();
// A busy report under B's id is nobody's word now: it must neither
// complete the roll call (A has not spoken) nor set its verdict.
assert_eq!(s.claim(&target_id, "tab-b", false), serde_json::json!({ "ok": true, "granted": false, "pending": true }));
std::thread::sleep(Duration::from_millis(60));
assert!(!held.is_finished(), "a departed overlay's busy report answered the request");
assert_eq!(s.claim(&target_id, "tab-a", true)["granted"], serde_json::json!(true));
post_json(s.port, "/agent-target-result", serde_json::json!({ "token": s.token, "targetId": target_id, "clientId": "tab-a", "ok": true, "sessionId": "aabbccdd" }));
let (_, verdict) = held.join().unwrap();
assert_eq!(verdict["sessionId"], serde_json::json!("aabbccdd"));
}
#[test]
fn agent_target_answers_the_resolution_verdict_when_no_page_can_serve() {
let s = Server::start("no-match");