feat(kanban): deterministically rebase snapshot
req: sync/011
This commit is contained in:
@@ -495,8 +495,8 @@ async fn exhausted_offline_retries_keep_command_until_later_reconnect() -> WebDr
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_history_loads_snapshot_and_preserves_pending_intent() -> WebDriverResult<()> {
|
||||
// test req: sync/007 req: sync/020
|
||||
async fn missing_history_rebase_converges_without_losing_local_intent() -> WebDriverResult<()> {
|
||||
// test req: sync/007 req: sync/011 req: sync/020
|
||||
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"));
|
||||
@@ -564,31 +564,56 @@ async fn missing_history_loads_snapshot_and_preserves_pending_intent() -> WebDri
|
||||
driver.goto(&format!("http://{app_addr}/sync-demo")).await?;
|
||||
wait_until(
|
||||
&driver,
|
||||
"return document.querySelector('[data-kanban-sync]')?.getAttribute('data-sync-phase') === 'rebase-required'",
|
||||
"return document.querySelector('[data-kanban-sync]')?.getAttribute('data-sync-phase') === 'rebased'",
|
||||
)
|
||||
.await?;
|
||||
let fallback = driver
|
||||
.execute(
|
||||
"const root = document.querySelector('[data-kanban-sync]'); return { phase: root.getAttribute('data-sync-phase'), uploadSequence: root.getAttribute('data-sync-upload-sequence'), snapshotSequence: root.getAttribute('data-sync-snapshot-sequence'), snapshotSchema: root.getAttribute('data-sync-snapshot-schema'), snapshotCards: root.getAttribute('data-sync-snapshot-card-count'), pending: root.getAttribute('data-sync-pending-count'), rebasePending: root.getAttribute('data-sync-rebase-pending-count'), canonicalColumn: root.getAttribute('data-sync-canonical-column'), status: root.querySelector('[role=status]').textContent, error: root.getAttribute('data-sync-error') }",
|
||||
"const root = document.querySelector('[data-kanban-sync]'); return { phase: root.getAttribute('data-sync-phase'), uploadSequence: root.getAttribute('data-sync-upload-sequence'), ackSequence: root.getAttribute('data-sync-ack-sequence'), snapshotSequence: root.getAttribute('data-sync-snapshot-sequence'), snapshotSchema: root.getAttribute('data-sync-snapshot-schema'), snapshotCards: root.getAttribute('data-sync-snapshot-card-count'), pending: root.getAttribute('data-sync-pending-count'), rebasePending: root.getAttribute('data-sync-rebase-pending-count'), decision: root.getAttribute('data-sync-rebase-decision'), reason: root.getAttribute('data-sync-rebase-reason'), canonicalColumn: root.getAttribute('data-sync-canonical-column'), status: root.querySelector('[role=status]').textContent, error: root.getAttribute('data-sync-error') }",
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.clone();
|
||||
assert_eq!(fallback["phase"], "rebase-required");
|
||||
assert_eq!(fallback["phase"], "rebased");
|
||||
assert_eq!(fallback["uploadSequence"], "2");
|
||||
assert_eq!(fallback["ackSequence"], "2");
|
||||
assert_eq!(fallback["snapshotSequence"], "2");
|
||||
assert_eq!(fallback["snapshotSchema"], "1");
|
||||
assert_eq!(fallback["snapshotCards"], "3");
|
||||
assert_eq!(fallback["pending"], "1");
|
||||
assert_eq!(fallback["pending"], "0");
|
||||
assert_eq!(fallback["rebasePending"], "1");
|
||||
assert_eq!(fallback["decision"], "converged");
|
||||
assert_eq!(fallback["reason"], "intent-already-canonical");
|
||||
assert_eq!(fallback["canonicalColumn"], "done");
|
||||
assert_eq!(
|
||||
fallback["status"],
|
||||
"History is unavailable; loaded snapshot 2 and preserved 1 pending command for rebase."
|
||||
"Canonical snapshot 2 already satisfies history:2; committed and removed the pending command."
|
||||
);
|
||||
assert!(fallback["error"].is_null());
|
||||
assert_eq!(command_count(&driver).await?, 1);
|
||||
assert_eq!(command_count(&driver).await?, 0);
|
||||
let committed = driver
|
||||
.execute_async(
|
||||
r#"
|
||||
const done = arguments[arguments.length - 1];
|
||||
const open = indexedDB.open('hemx-kanban-v1', 1);
|
||||
open.onsuccess = () => {
|
||||
const tx = open.result.transaction('meta', 'readonly');
|
||||
const meta = tx.objectStore('meta');
|
||||
const cursor = meta.get('acknowledgementCursor');
|
||||
const snapshot = meta.get('canonicalSnapshot');
|
||||
tx.oncomplete = () => done({ cursor: cursor.result, snapshot: snapshot.result });
|
||||
tx.onabort = () => done({ error: tx.error && tx.error.name });
|
||||
};
|
||||
"#,
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.clone();
|
||||
assert_eq!(committed["cursor"], 2);
|
||||
assert_eq!(committed["snapshot"]["schemaVersion"], 1);
|
||||
assert_eq!(committed["snapshot"]["serverSequence"], 2);
|
||||
|
||||
driver.goto(&format!("http://{app_addr}/")).await?;
|
||||
let canonical = driver
|
||||
|
||||
Reference in New Issue
Block a user