fix(sync): bound request lifetime

req: operations/003
This commit is contained in:
slhx agent
2026-07-13 21:16:07 +02:00
parent 6d502d8ff5
commit 2a8aa4c07f
3 changed files with 118 additions and 6 deletions
+77 -2
View File
@@ -599,7 +599,7 @@ async fn account_partition_hides_replay_and_export_until_owner_returns() -> WebD
#[tokio::test]
async fn canonical_snapshot_and_history_are_tenant_scoped() -> WebDriverResult<()> {
// test req: sync/007 req: security/004 req: auth/005
// test req: sync/007 req: security/004 req: auth/005 req: performance/004
let app_port = available_port();
let app_addr = format!("127.0.0.1:{app_port}");
let mut app_command = Command::new(env!("CARGO_BIN_EXE_hemx-kanban-example"));
@@ -1708,7 +1708,7 @@ async fn missing_history_rebase_and_user_conflict_resolution_preserve_suffix() -
#[tokio::test]
async fn redacted_sync_diagnostics_are_bounded_and_leak_no_sensitive_material(
) -> WebDriverResult<()> {
// test req: operations/001 req: security/002 req: sync/016 req: sync/021
// test req: operations/001 req: operations/005 req: security/002 req: sync/016 req: sync/021
let app_port = available_port();
let app_addr = format!("127.0.0.1:{app_port}");
let mut app_command = Command::new(env!("CARGO_BIN_EXE_hemx-kanban-example"));
@@ -2077,6 +2077,81 @@ async fn keep_local_retry_preserves_conflicted_command_and_suffix_order() -> Web
result.and(quit)
}
#[tokio::test]
async fn sync_requests_timeout_and_cancel_on_pagehide() -> WebDriverResult<()> {
// test req: operations/003
let app_port = available_port();
let app_addr = format!("127.0.0.1:{app_port}");
let mut app_command = Command::new(env!("CARGO_BIN_EXE_hemx-kanban-example"));
app_command.env("HEMX_KANBAN_ADDR", &app_addr);
let _app = TestProcess::start(app_command, "hemx-kanban", &app_addr, STARTUP_TIMEOUT)
.expect("start hemx-kanban");
let webdriver_port = available_port();
let webdriver_addr = format!("127.0.0.1:{webdriver_port}");
let mut webdriver = Command::new("geckodriver");
webdriver.arg("--port").arg(webdriver_port.to_string());
let _webdriver = TestProcess::start(webdriver, "geckodriver", &webdriver_addr, STARTUP_TIMEOUT)
.expect("start ready geckodriver");
let mut caps = DesiredCapabilities::firefox();
caps.set_headless()?;
let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?;
let result = async {
driver.goto(&format!("http://{app_addr}/sync-demo")).await?;
wait_until(
&driver,
"return document.querySelector('[data-kanban-sync]')?.getAttribute('data-sync-phase') === 'idle'",
)
.await?;
let proof = driver
.execute_async(
r#"
const done = arguments[arguments.length - 1];
(async () => {
const { fetchWithTimeout } = await import('/sync.js');
const pendingFetch = (_input, init) => new Promise((_resolve, reject) => {
init.signal.addEventListener('abort', () => reject(init.signal.reason), { once: true });
});
const started = performance.now();
let timeout;
try {
await fetchWithTimeout('/never-timeout', {}, pendingFetch, 40);
} catch (error) {
timeout = { name: error.name, message: error.message, elapsedMs: performance.now() - started };
}
const cancellationPromise = fetchWithTimeout('/never-pagehide', {}, pendingFetch, 10_000)
.then(() => ({ resolved: true }))
.catch((error) => ({ name: error.name, message: error.message }));
window.dispatchEvent(new PageTransitionEvent('pagehide'));
done({ timeout, cancellation: await cancellationPromise });
})().catch((error) => done({ error: String(error), stack: error?.stack }));
"#,
Vec::new(),
)
.await?
.json()
.clone();
assert!(proof["error"].is_null(), "bounded request failed: {proof}");
assert_eq!(proof["timeout"]["name"], "TimeoutError", "{proof}");
assert!(proof["timeout"]["message"]
.as_str()
.is_some_and(|message| message.contains("40 ms")));
assert!(proof["timeout"]["elapsedMs"]
.as_f64()
.is_some_and(|elapsed| (35.0..1_000.0).contains(&elapsed)));
assert_eq!(proof["cancellation"]["name"], "AbortError", "{proof}");
assert_eq!(
proof["cancellation"]["message"],
"sync cancelled because page is hidden"
);
Ok(())
}
.await;
let quit = driver.quit().await;
result.and(quit)
}
#[tokio::test]
async fn identical_sync_inputs_reconcile_deterministically() -> WebDriverResult<()> {
// test req: sync/022