From 98b00920b91e9c843f3ed85554dcfe58befd9afb Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 13 Jul 2026 22:46:03 +0200 Subject: [PATCH] test(sync): audit browser recovery paths --- PLAN.md | 2 +- examples/kanban/src/main.rs | 37 ++++++++++++++++------------ examples/kanban/static/sync.js | 20 ++++++++++++++- examples/kanban/tests/browser_e2e.rs | 16 +++++++----- hemx-wasm/tests/browser.rs | 26 +++++++++++++------ 5 files changed, 70 insertions(+), 31 deletions(-) diff --git a/PLAN.md b/PLAN.md index 88dcf60..9aaa085 100644 --- a/PLAN.md +++ b/PLAN.md @@ -46,7 +46,7 @@ encryption, retention, backup, and deployment policy remain host concerns. - **Build:** materialize `hemx-sync` over an integration transport with idempotent server command processing, snapshot/change cursor, durable acknowledgements, bounded ordered replay, current auth checks, rejection/conflict results, canonical replacement, reconnect jitter/backoff, multi-tab coordination, and redacted diagnostics. - **Refusals:** no default CRDT, transport in core, cached enqueue-time permission, unbounded queue, or silent last-write-wins policy. - **Requirements:** `sync/001-023`, `operations/001-005`, `security/002-005`, `performance/004-005`. -- **Proof:** `cargo test -p hemx-wasm --test browser flat_patch_persists_offline_then_uploads_with_same_operation_identity -- --exact` proves `sync/002` and `sync/003` through generated real WASM: `SyncEffect::send_patch` travels beside ordinary DOM effects, the strict flat scalar schema is accepted, the offline interaction performs zero patch requests while remaining durable, and reconnect uploads with identical opaque operation/idempotency identity before matching acknowledgement removes it. `cargo test -p hemx-wasm --test browser kanban_public_api_offline_sync_journey_converges_without_duplicate_replay -- --exact` proves `sync/023` through the public generated client handler and real WASM: offline durable mutation, unavailable-origin reload projection, reconnect upload, canonical convergence, duplicate idempotency without a second event, conflicting duplicate rejection, invalid-command rejection, and an empty local queue. `cargo test -p hemx-kanban-example --test browser_e2e idempotent_server_command_is_acknowledged_after_reconnect -- --exact` proves duplicate POST delivery yields one identical canonical acknowledgement/sequence, conflicting id reuse is rejected, EventSource reconnects after a server-closed first stream, the acknowledgement is delivered once with its sequence as event id, and a page reload shows the authoritative card in the canonical column. `cargo test -p hemx-kanban-example --test browser_e2e pending_local_command_uploads_with_bounded_retry_and_is_removed_on_ack -- --exact` proves automatic platform-store upload, one explicit transient failure, bounded exponential backoff plus jitter, reconnect acknowledgement, pending-before-ack ordering, acknowledged removal, canonical board convergence, and non-retried 409 rejection remaining durable with a visible reason. `cargo test -p hemx-kanban-example --test browser_e2e canonical_acknowledgement_survives_server_restart -- --exact` proves the versioned store is materialized before success, a real process restart reloads the same idempotent acknowledgement/sequence, EventSource replays it by id, and canonical board state is rebuilt. `cargo test -p hemx-kanban-example --test browser_e2e exhausted_offline_retries_keep_command_until_later_reconnect -- --exact` proves three bounded retries exhaust into visible offline/manual-recovery state while the command remains durable, then a later retry acknowledges/removes it and converges canonically. `cargo test -p hemx-kanban-example --test browser_e2e missing_history_rebase_and_user_conflict_resolution_preserve_suffix -- --exact` proves retained-history gap detection, typed/versioned snapshot fallback, deterministic already-canonical convergence, atomic snapshot/cursor commit, then a divergent canonical update producing explicit conflict with a two-command queue; the user’s canonical-state action removes only the conflicted head, retains and resumes the suffix, and reaches canonical convergence with an empty queue. `cargo test -p hemx-kanban-example --test browser_e2e keep_local_retry_preserves_conflicted_command_and_suffix_order -- --exact` proves the keep-local action derives one resolution identity; a permanent rejection preserves order and re-enables the choice; three later transport failures still preserve the original conflicted head plus suffix and expose manual recovery; final acceptance removes only the head and converges the untouched suffix without duplication or loss. `cargo test -p hemx-kanban-example --test browser_e2e redacted_sync_diagnostics_are_bounded_and_leak_no_sensitive_material -- --exact` proves a 15-second-old queue is bucketed rather than timestamped, cursor and acknowledgement latency advance after recovery, diagnostics stay under a fixed small readout, and command id/payload/actor/session/cookie material never reaches the diagnostics surface; the conflict and keep-local tests additionally assert conflict/rejection counters at their actual failure boundaries. `cargo test -p hemx-kanban-example --test browser_e2e two_tabs_coordinate_single_uploader_and_takeover_without_duplicate_application -- --exact` proves one retry-exhausted leader/one explicit standby, zero follower upload before takeover, lease takeover after the leader closes, one canonical sequence/event, one queue removal, and one board application. `cargo test -p hemx-kanban-example --test browser_e2e upload_backpressure_keeps_pending_work_visible_and_recoverable -- --exact` proves one in-flight upload, a two-acknowledgement activation limit, one retained durable command with visible recovery state, and explicit retry draining the final command without loss. `cargo test -p hemx-kanban-example --test browser_e2e mixed_queue_removes_accepted_prefix_and_retains_rejected_tail -- --exact` proves an accepted prefix is canonically applied and removed once before a permanent rejection stops processing, exposes its typed HTTP/server cause, disables blind retry, and leaves both the rejected command and untouched suffix durable. `cargo test -p hemx-kanban-example --test browser_e2e schema_upgrade_preserves_queued_order_and_local_intent -- --exact` proves a three-command v1 queue migrates atomically to the explicit-target v2 schema, remains byte-for-intent ordered after interrupted upload, then receives canonical sequences 1–3 in original order and drains without loss. `cargo test -p hemx-kanban-example --test browser_e2e account_partition_hides_replay_and_export_until_owner_returns -- --exact` proves a beta editor and alpha viewer enumerate zero commands and issue no replay for an alpha owner queue, signed-out startup cannot open a partition, no foreign id/export surface leaks, and only the returning alpha owner can export the intact command then resume it exactly once. `cargo test -p hemx-kanban-example --test browser_e2e canonical_snapshot_and_history_are_tenant_scoped -- --exact` proves alpha and beta sessions receive only their own canonical cards and acknowledgement events with independent sequence 1 cursors, while signed-out snapshot/history reads receive 401 and expose no stream. `cargo test -p hemx-kanban-example --test browser_e2e identical_sync_inputs_reconcile_deterministically -- --exact` proves `sync/022` through a real browser and live sync endpoints: one accepted server result and canonical snapshot plus an ordered command sequence produce byte-equivalent structured reconciliation records on independent cloned inputs, do not mutate inputs, and select the explicit server-authoritative model. `cargo test -p hemx-kanban-example --test browser_e2e sync_requests_timeout_and_cancel_on_pagehide -- --exact` proves the sync request boundary enforces a named finite timeout and reports `TimeoutError`, while page teardown cancels a pending request with `AbortError`, closes the acknowledgement stream, and clears retry/lease timers. Per-requirement audit: every `sync/001-023` row now has concrete browser proof; existing live-browser proofs also cover `operations/001-002`, `operations/005`, `security/002`, `security/004`, `performance/004`, and `performance/005`, while this slice closes the sync request/stream leg of `operations/003`. `cargo test -p hemx-kanban-example --test browser_e2e adversarial_wire_inputs_are_rejected_before_partial_application -- --exact` proves malformed, oversized, truncated, trailing, unknown-version, and invalid-kind batch/state/command inputs fail without UI mutation; `cargo test -p hemx-wasm --test browser flat_patch_persists_offline_then_uploads_with_same_operation_identity -- --exact` additionally proves unknown-version and oversized event/state inputs are rejected at the generated real-WASM handler boundary before application code. The runtime now bounds total payloads, fields, vectors, varints, UTF-8, enums, and option discriminants before allocation or dispatch, and the durable queue validates every loaded command before replay, closing `security/005`. `cargo test -p hemx-axum --test response interaction_boundary_honors_media_type_and_host_body_limit -- --exact` proves the mutation extractor rejects missing/unexpected media types with 415 and an Axum host-configured oversized body with 413 before handler dispatch, while a valid bounded form dispatches exactly once; the extractor documentation names `DefaultBodyLimit`, and CSRF remains the existing documented application/middleware policy, closing `security/003`. `cargo test -p hemx-kanban-example --test browser_e2e acknowledgement_stream_bounds_reconnect_buffering_heartbeat_and_cancellation -- --exact` closes `operations/004`: the live stream exposes a finite 15-second production heartbeat (25 ms under proof configuration), exactly three bounded reconnect delays (100/250/500 ms), converts 65 pending events over its 64-event transport budget into one canonical snapshot recovery event, then emits heartbeats after recovery; both proof streams are explicitly closed, and the page teardown path closes its owned stream and reports cancellation. `cargo test -p hemx-kanban-example --bin hemx-kanban-example timeout -- --nocapture` completes `operations/003`: ordinary server routes are cancelled after a finite 10-second production deadline and return 504, startup store read/replay is cancelled after five seconds, oversized startup state is rejected before decoding, and focused tests prove dropped in-flight work plus named timeout diagnostics. `cargo test -p hemx-kanban-example --test browser_e2e ordinary_browser_request_exposes_deadline_and_cancels_on_pagehide -- --exact` proves the ordinary browser request boundary exposes its finite 10-second deadline, aborts the real form request with `AbortError` on page teardown, and clears pending UI state; stream routes remain governed by the `operations/004` lifecycle rather than the ordinary-handler timeout. Every Slice 4 requirement now has concrete proof; the next coherent slice is to run the complete Slice 4 browser/requirements audit, close it if clean, and advance to Slice 5 rather than adding more sync surface. +- **Proof:** `cargo test -p hemx-wasm --test browser flat_patch_persists_offline_then_uploads_with_same_operation_identity -- --exact` proves `sync/002` and `sync/003` through generated real WASM: `SyncEffect::send_patch` travels beside ordinary DOM effects, the strict flat scalar schema is accepted, the offline interaction performs zero patch requests while remaining durable, and reconnect uploads with identical opaque operation/idempotency identity before matching acknowledgement removes it. `cargo test -p hemx-wasm --test browser kanban_public_api_offline_sync_journey_converges_without_duplicate_replay -- --exact` proves `sync/023` through the public generated client handler and real WASM: offline durable mutation, unavailable-origin reload projection, reconnect upload, canonical convergence, duplicate idempotency without a second event, conflicting duplicate rejection, invalid-command rejection, and an empty local queue. `cargo test -p hemx-kanban-example --test browser_e2e idempotent_server_command_is_acknowledged_after_reconnect -- --exact` proves duplicate POST delivery yields one identical canonical acknowledgement/sequence, conflicting id reuse is rejected, EventSource reconnects after a server-closed first stream, the acknowledgement is delivered once with its sequence as event id, and a page reload shows the authoritative card in the canonical column. `cargo test -p hemx-kanban-example --test browser_e2e pending_local_command_uploads_with_bounded_retry_and_is_removed_on_ack -- --exact` proves automatic platform-store upload, one explicit transient failure, bounded exponential backoff plus jitter, reconnect acknowledgement, pending-before-ack ordering, acknowledged removal, canonical board convergence, and non-retried 409 rejection remaining durable with a visible reason. `cargo test -p hemx-kanban-example --test browser_e2e canonical_acknowledgement_survives_server_restart -- --exact` proves the versioned store is materialized before success, a real process restart reloads the same idempotent acknowledgement/sequence, EventSource replays it by id, and canonical board state is rebuilt. `cargo test -p hemx-kanban-example --test browser_e2e exhausted_offline_retries_keep_command_until_later_reconnect -- --exact` proves three bounded retries exhaust into visible offline/manual-recovery state while the command remains durable, then a later retry acknowledges/removes it and converges canonically. `cargo test -p hemx-kanban-example --test browser_e2e missing_history_rebase_and_user_conflict_resolution_preserve_suffix -- --exact` proves retained-history gap detection, typed/versioned snapshot fallback, deterministic already-canonical convergence, atomic snapshot/cursor commit, then a divergent canonical update producing explicit conflict with a two-command queue; the user’s canonical-state action removes only the conflicted head, retains and resumes the suffix, and reaches canonical convergence with an empty queue. `cargo test -p hemx-kanban-example --test browser_e2e keep_local_retry_preserves_conflicted_command_and_suffix_order -- --exact` proves the keep-local action derives one resolution identity; a permanent rejection preserves order and re-enables the choice; three later transport failures still preserve the original conflicted head plus suffix and expose manual recovery; final acceptance removes only the head and converges the untouched suffix without duplication or loss. `cargo test -p hemx-kanban-example --test browser_e2e redacted_sync_diagnostics_are_bounded_and_leak_no_sensitive_material -- --exact` proves a 15-second-old queue is bucketed rather than timestamped, cursor and acknowledgement latency advance after recovery, diagnostics stay under a fixed small readout, and command id/payload/actor/session/cookie material never reaches the diagnostics surface; the conflict and keep-local tests additionally assert conflict/rejection counters at their actual failure boundaries. `cargo test -p hemx-kanban-example --test browser_e2e two_tabs_coordinate_single_uploader_and_takeover_without_duplicate_application -- --exact` proves one retry-exhausted leader/one explicit standby, zero follower upload before takeover, lease takeover after the leader closes, one canonical sequence/event, one queue removal, and one board application. `cargo test -p hemx-kanban-example --test browser_e2e upload_backpressure_keeps_pending_work_visible_and_recoverable -- --exact` proves one in-flight upload, a two-acknowledgement activation limit, one retained durable command with visible recovery state, and explicit retry draining the final command without loss. `cargo test -p hemx-kanban-example --test browser_e2e mixed_queue_removes_accepted_prefix_and_retains_rejected_tail -- --exact` proves an accepted prefix is canonically applied and removed once before a permanent rejection stops processing, exposes its typed HTTP/server cause, disables blind retry, and leaves both the rejected command and untouched suffix durable. `cargo test -p hemx-kanban-example --test browser_e2e schema_upgrade_preserves_queued_order_and_local_intent -- --exact` proves a three-command v1 queue migrates atomically to the explicit-target v2 schema, remains byte-for-intent ordered after interrupted upload, then receives canonical sequences 1–3 in original order and drains without loss. `cargo test -p hemx-kanban-example --test browser_e2e account_partition_hides_replay_and_export_until_owner_returns -- --exact` proves a beta editor and alpha viewer enumerate zero commands and issue no replay for an alpha owner queue, signed-out startup cannot open a partition, no foreign id/export surface leaks, and only the returning alpha owner can export the intact command then resume it exactly once. `cargo test -p hemx-kanban-example --test browser_e2e canonical_snapshot_and_history_are_tenant_scoped -- --exact` proves alpha and beta sessions receive only their own canonical cards and acknowledgement events with independent sequence 1 cursors, while signed-out snapshot/history reads receive 401 and expose no stream. `cargo test -p hemx-kanban-example --test browser_e2e identical_sync_inputs_reconcile_deterministically -- --exact` proves `sync/022` through a real browser and live sync endpoints: one accepted server result and canonical snapshot plus an ordered command sequence produce byte-equivalent structured reconciliation records on independent cloned inputs, do not mutate inputs, and select the explicit server-authoritative model. `cargo test -p hemx-kanban-example --test browser_e2e sync_requests_timeout_and_cancel_on_pagehide -- --exact` proves the sync request boundary enforces a named finite timeout and reports `TimeoutError`, while page teardown cancels a pending request with `AbortError`, closes the acknowledgement stream, and clears retry/lease timers. Per-requirement audit: every `sync/001-023` row now has concrete browser proof; existing live-browser proofs also cover `operations/001-002`, `operations/005`, `security/002`, `security/004`, `performance/004`, and `performance/005`, while this slice closes the sync request/stream leg of `operations/003`. `cargo test -p hemx-kanban-example --test browser_e2e adversarial_wire_inputs_are_rejected_before_partial_application -- --exact` proves malformed, oversized, truncated, trailing, unknown-version, and invalid-kind batch/state/command inputs fail without UI mutation; `cargo test -p hemx-wasm --test browser flat_patch_persists_offline_then_uploads_with_same_operation_identity -- --exact` additionally proves unknown-version and oversized event/state inputs are rejected at the generated real-WASM handler boundary before application code. The runtime now bounds total payloads, fields, vectors, varints, UTF-8, enums, and option discriminants before allocation or dispatch, and the durable queue validates every loaded command before replay, closing `security/005`. `cargo test -p hemx-axum --test response interaction_boundary_honors_media_type_and_host_body_limit -- --exact` proves the mutation extractor rejects missing/unexpected media types with 415 and an Axum host-configured oversized body with 413 before handler dispatch, while a valid bounded form dispatches exactly once; the extractor documentation names `DefaultBodyLimit`, and CSRF remains the existing documented application/middleware policy, closing `security/003`. `cargo test -p hemx-kanban-example --test browser_e2e acknowledgement_stream_bounds_reconnect_buffering_heartbeat_and_cancellation -- --exact` closes `operations/004`: the live stream exposes a finite 15-second production heartbeat (25 ms under proof configuration), exactly three bounded reconnect delays (100/250/500 ms), converts 65 pending events over its 64-event transport budget into one canonical snapshot recovery event, then emits heartbeats after recovery; both proof streams are explicitly closed, and the page teardown path closes its owned stream and reports cancellation. `cargo test -p hemx-kanban-example --bin hemx-kanban-example timeout -- --nocapture` completes `operations/003`: ordinary server routes are cancelled after a finite 10-second production deadline and return 504, startup store read/replay is cancelled after five seconds, oversized startup state is rejected before decoding, and focused tests prove dropped in-flight work plus named timeout diagnostics. `cargo test -p hemx-kanban-example --test browser_e2e ordinary_browser_request_exposes_deadline_and_cancels_on_pagehide -- --exact` proves the ordinary browser request boundary exposes its finite 10-second deadline, aborts the real form request with `AbortError` on page teardown, and clears pending UI state; stream routes remain governed by the `operations/004` lifecycle rather than the ordinary-handler timeout. The complete Slice 4 browser audit now passes (`cargo test -p hemx-kanban-example --test browser_e2e -- --test-threads=1` and `cargo test -p hemx-wasm --test browser -- --test-threads=1`). The audit also fixed three stale v1 command fixtures to exercise the real v2 durable schema, preserved an offline leader lease without retrying while connectivity is absent, and made finite acknowledgement responses close unless the caller explicitly requests a reconnecting stream. Slice 4 cannot close yet: citations labeled `sync/001` and `sync/004-006` prove app-local equivalents, not the required public `hemx-sync` presence tracking, `SyncEffect::broadcast(channel, effect_batch)`, `#[hemx_sync::presence]`, or `SyncEffect::ack(atom)` APIs; those symbols do not exist in `hemx-sync`. The next coherent slice is `sync/004`: add the public typed broadcast primitive and prove one generated `EffectBatch` crosses the real SSE/browser path before implementing presence or acknowledgement wrappers. ## Slice 5 — local-first multiplayer Kanban milestone diff --git a/examples/kanban/src/main.rs b/examples/kanban/src/main.rs index 5dc0308..5d1c442 100644 --- a/examples/kanban/src/main.rs +++ b/examples/kanban/src/main.rs @@ -842,6 +842,7 @@ async fn sync_acknowledgements( .get("reconnect") .filter(|value| !value.is_empty()) .cloned(); + let persistent_stream = reconnect_key.is_some(); let mut sync = state.sync.lock().unwrap(); if let Some(key) = reconnect_key { @@ -921,22 +922,26 @@ async fn sync_acknowledgements( }; drop(sync); let heartbeat_interval = state.acknowledgement_heartbeat_interval; - let heartbeat = stream::unfold(heartbeat_interval, |interval| async move { - tokio::time::sleep(interval).await; - Some(( - Ok(Event::default() - .event("heartbeat") - .data("{\"status\":\"ok\"}")), - interval, - )) - }); - Ok( - Sse::new(stream::iter(events).chain(heartbeat).boxed()).keep_alive( - KeepAlive::new() - .interval(heartbeat_interval) - .text("heartbeat"), - ), - ) + let event_stream = stream::iter(events).boxed(); + let response_stream = if persistent_stream { + let heartbeat = stream::unfold(heartbeat_interval, |interval| async move { + tokio::time::sleep(interval).await; + Some(( + Ok(Event::default() + .event("heartbeat") + .data("{\"status\":\"ok\"}")), + interval, + )) + }); + event_stream.chain(heartbeat).boxed() + } else { + event_stream + }; + Ok(Sse::new(response_stream).keep_alive( + KeepAlive::new() + .interval(heartbeat_interval) + .text("heartbeat"), + )) } fn registry(state: Arc) -> impl DispatchRegistry { diff --git a/examples/kanban/static/sync.js b/examples/kanban/static/sync.js index 1c9d214..4ae4f8f 100644 --- a/examples/kanban/static/sync.js +++ b/examples/kanban/static/sync.js @@ -443,6 +443,21 @@ async function continuePendingWork() { setTimeout(() => synchronize(validatePending(commands[0])).catch(failPermanently), 0); } +async function renewOfflineLease(command) { + if (stopped || root.getAttribute("data-sync-phase") !== "offline") return; + const lease = await claimUploaderLease(database); + publishLease(lease); + if (!lease.leader) { + setPhase("standby", "Another tab owns sync; waiting for lease takeover."); + leaseTimer = setTimeout(() => runLeaseLoop(command).catch(failPermanently), LEASE_POLL_MS); + return; + } + leaseTimer = setTimeout( + () => renewOfflineLease(command).catch(failPermanently), + LEASE_MS / 2, + ); +} + async function synchronize(command) { if (synchronizing) return; synchronizing = true; @@ -455,7 +470,10 @@ async function synchronize(command) { } clearTimeout(leaseTimer); leaseTimer = setTimeout(() => { - if (!stopped && root.getAttribute("data-sync-phase") !== "acknowledged") { + if (stopped || root.getAttribute("data-sync-phase") === "acknowledged") return; + if (root.getAttribute("data-sync-phase") === "offline") { + renewOfflineLease(command).catch(failPermanently); + } else { synchronize(command).catch(failPermanently); } }, LEASE_MS / 2); diff --git a/examples/kanban/tests/browser_e2e.rs b/examples/kanban/tests/browser_e2e.rs index e008aac..9ed372a 100644 --- a/examples/kanban/tests/browser_e2e.rs +++ b/examples/kanban/tests/browser_e2e.rs @@ -829,12 +829,16 @@ async fn schema_upgrade_preserves_queued_order_and_local_intent() -> WebDriverRe ]) ); - driver.find(By::Css("[data-sync-retry]")).await?.click().await?; - wait_until( - &driver, - "const root = document.querySelector('[data-kanban-sync]'); return root?.getAttribute('data-sync-phase') === 'backpressured' && root?.getAttribute('data-sync-pending-count') === '1'", - ) - .await?; + for expected_pending in [2, 1] { + driver.find(By::Css("[data-sync-retry]")).await?.click().await?; + wait_until( + &driver, + &format!( + "const root = document.querySelector('[data-kanban-sync]'); return root?.getAttribute('data-sync-phase') === 'offline' && root?.getAttribute('data-sync-pending-count') === '{expected_pending}'" + ), + ) + .await?; + } driver.find(By::Css("[data-sync-retry]")).await?.click().await?; wait_until( &driver, diff --git a/hemx-wasm/tests/browser.rs b/hemx-wasm/tests/browser.rs index c3c14be..fbf6f21 100644 --- a/hemx-wasm/tests/browser.rs +++ b/hemx-wasm/tests/browser.rs @@ -675,7 +675,7 @@ async fn kanban_command_export_delete_and_reset_are_recoverable() -> WebDriverRe let first_export = export_commands(&driver).await?.json().clone(); assert_eq!(first_export["schemaVersion"], 1); assert_eq!(first_export["commands"].as_array().map(Vec::len), Some(1)); - assert_eq!(first_export["commands"][0]["schemaVersion"], 1); + assert_eq!(first_export["commands"][0]["schemaVersion"], 2); assert_eq!(first_export["commands"][0]["kind"], "reorder_card"); assert_eq!(first_export["commands"][0]["cardId"], "1"); let mut exported_keys = first_export["commands"][0] @@ -688,6 +688,7 @@ async fn kanban_command_export_delete_and_reset_are_recoverable() -> WebDriverRe assert_eq!( exported_keys, [ + "accountPartition", "actor", "cardId", "causal", @@ -695,8 +696,10 @@ async fn kanban_command_export_delete_and_reset_are_recoverable() -> WebDriverRe "id", "key", "kind", + "queuedAt", "schemaVersion", "session", + "targetColumn", ] ); let first_actor = first_export["commands"][0]["actor"] @@ -1205,7 +1208,7 @@ async fn kanban_replay_is_bounded_and_within_budget() -> WebDriverResult<()> { assert_eq!(within_bound["order"], "2|1"); assert_eq!(within_bound["limit"], REPLAY_LIMIT.to_string()); assert_eq!(within_bound["budget"], REPLAY_BUDGET_MS); - assert_eq!(within_bound["over"], false); + assert_eq!(within_bound["over"], false, "{within_bound}"); assert!( within_bound["elapsed"].as_f64().is_some_and(|elapsed| elapsed <= REPLAY_BUDGET_MS), "replay exceeded budget: {within_bound}" @@ -1884,12 +1887,15 @@ async fn store_replay_commands(driver: &WebDriver, first: u64, last: u64) -> Web for (let causal = first; causal <= last; causal += 1) { commands.add({ id: `replay:${causal}`, - schemaVersion: 1, + schemaVersion: 2, + accountPartition: 'demo:demo', actor: 'replay', session: 'replay', causal, + queuedAt: Date.now(), kind: 'reorder_card', cardId: '1', + targetColumn: 'done', eventKind: 'click', key: null, }); @@ -1922,12 +1928,15 @@ async fn store_malformed_command(driver: &WebDriver) -> WebDriverResult<()> { const tx = open.result.transaction('commands', 'readwrite'); tx.objectStore('commands').add({ id: 'corrupt:1', - schemaVersion: 1, + schemaVersion: 2, + accountPartition: 'demo:demo', actor: 'corrupt', session: 'corrupt', causal: 1, + queuedAt: Date.now(), kind: 'reorder_card', cardId: '', + targetColumn: 'done', eventKind: 'click', key: null, }); @@ -1967,19 +1976,22 @@ async fn occupy_next_command_id(driver: &WebDriver) -> WebDriverResult<()> { const next = causal + 1; commands.add({ id: `${actor}:${next}`, - schemaVersion: 1, + schemaVersion: 2, + accountPartition: 'demo:demo', actor, session: 'fault-injection', causal: next, + queuedAt: Date.now(), kind: 'reorder_card', cardId: 'fault-injection', + targetColumn: 'done', eventKind: 'click', key: null, }); }; - const actorRequest = meta.get('actor'); + const actorRequest = meta.get('actor:demo:demo'); actorRequest.onsuccess = () => { actor = actorRequest.result; addCollision(); }; - const causalRequest = meta.get('causal'); + const causalRequest = meta.get('causal:demo:demo'); causalRequest.onsuccess = () => { causal = causalRequest.result; addCollision(); }; tx.oncomplete = () => done({ id: `${actor}:${causal + 1}` }); tx.onabort = () => done({ error: tx.error && tx.error.name });