test(kanban): prove public offline sync journey
req: sync/023
This commit is contained in:
+104
-3
@@ -192,13 +192,29 @@ async fn client_handler_applies_effect_batch_without_network() -> WebDriverResul
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn kanban_command_persists_before_projection_and_restores_after_reload() -> WebDriverResult<()>
|
async fn kanban_public_api_offline_sync_journey_converges_without_duplicate_replay(
|
||||||
{
|
) -> WebDriverResult<()> {
|
||||||
// test req: local/001 req: local/002 req: local/003 req: local/004
|
// test req: local/001 req: local/002 req: local/003 req: local/004 req: sync/023
|
||||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.expect("workspace root")
|
.expect("workspace root")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
let host_build = Command::new("cargo")
|
||||||
|
.current_dir(&workspace)
|
||||||
|
.args([
|
||||||
|
"build",
|
||||||
|
"-p",
|
||||||
|
"hemx-kanban-example",
|
||||||
|
"--bin",
|
||||||
|
"hemx-kanban-example",
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.expect("build kanban host server");
|
||||||
|
assert!(host_build.success(), "build kanban host server");
|
||||||
|
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| workspace.join("target"));
|
||||||
|
let host_binary = target_dir.join("debug/hemx-kanban-example");
|
||||||
let (package, bootstrap, rendered) = build_kanban_artifact(&workspace);
|
let (package, bootstrap, rendered) = build_kanban_artifact(&workspace);
|
||||||
let runtime = workspace.join("hemx-js/runtime/hemx.js");
|
let runtime = workspace.join("hemx-js/runtime/hemx.js");
|
||||||
let mut server = StaticServer::start(
|
let mut server = StaticServer::start(
|
||||||
@@ -304,6 +320,91 @@ async fn kanban_command_persists_before_projection_and_restores_after_reload() -
|
|||||||
assert!(restored["error"].is_null());
|
assert!(restored["error"].is_null());
|
||||||
assert_eq!(restored["notice"], "Moved 1 with click");
|
assert_eq!(restored["notice"], "Moved 1 with click");
|
||||||
|
|
||||||
|
let app_addr = server.address.to_string();
|
||||||
|
let mut app_command = Command::new(&host_binary);
|
||||||
|
app_command.env("HEMX_KANBAN_ADDR", &app_addr);
|
||||||
|
let _app = ProcessGuard::start(app_command, &app_addr);
|
||||||
|
driver.goto(&format!("http://{app_addr}/sync-demo")).await?;
|
||||||
|
wait_until(
|
||||||
|
&driver,
|
||||||
|
"const root = document.querySelector('[data-kanban-sync]'); return root?.getAttribute('data-sync-phase') === 'acknowledged' && root?.getAttribute('data-sync-pending-count') === '0'",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let command_id = persisted["persisted"]["detail"]["id"]
|
||||||
|
.as_str()
|
||||||
|
.expect("persisted command id");
|
||||||
|
let duplicate_script = format!(
|
||||||
|
r#"
|
||||||
|
const done = arguments[arguments.length - 1];
|
||||||
|
const commandId = {command_id:?};
|
||||||
|
(async () => {{
|
||||||
|
const duplicateResponse = await fetch(`/sync/commands?command_id=${{encodeURIComponent(commandId)}}&card_id=1&column=done`, {{ method: 'POST' }});
|
||||||
|
const duplicate = await duplicateResponse.json();
|
||||||
|
const conflictResponse = await fetch(`/sync/commands?command_id=${{encodeURIComponent(commandId)}}&card_id=2&column=done`, {{ method: 'POST' }});
|
||||||
|
const conflict = await conflictResponse.json();
|
||||||
|
const rejectionResponse = await fetch('/sync/commands?command_id=journey-rejected&card_id=999&column=done', {{ method: 'POST' }});
|
||||||
|
const rejection = await rejectionResponse.json();
|
||||||
|
const snapshot = await (await fetch('/sync/snapshot', {{ cache: 'no-store' }})).json();
|
||||||
|
const history = await (await fetch('/sync/acknowledgements?after=0', {{ headers: {{ Accept: 'text/event-stream' }}, cache: 'no-store' }})).text();
|
||||||
|
const open = indexedDB.open('hemx-kanban-v1');
|
||||||
|
open.onsuccess = () => {{
|
||||||
|
const count = open.result.transaction('commands', 'readonly').objectStore('commands').count();
|
||||||
|
count.onsuccess = () => done({{
|
||||||
|
duplicateStatus: duplicateResponse.status,
|
||||||
|
duplicate,
|
||||||
|
conflictStatus: conflictResponse.status,
|
||||||
|
conflict,
|
||||||
|
rejectionStatus: rejectionResponse.status,
|
||||||
|
rejection,
|
||||||
|
snapshot,
|
||||||
|
history,
|
||||||
|
queueCount: count.result,
|
||||||
|
}});
|
||||||
|
}};
|
||||||
|
}})().catch((error) => done({{ error: String(error), stack: error.stack }}));
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
let convergence = driver
|
||||||
|
.execute_async(&duplicate_script, Vec::new())
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.clone();
|
||||||
|
assert!(
|
||||||
|
convergence.get("error").is_none(),
|
||||||
|
"sync convergence failed: {convergence}"
|
||||||
|
);
|
||||||
|
assert_eq!(convergence["duplicateStatus"], 200);
|
||||||
|
assert_eq!(convergence["duplicate"]["commandId"], command_id);
|
||||||
|
assert_eq!(convergence["duplicate"]["serverSequence"], 1);
|
||||||
|
assert_eq!(convergence["conflictStatus"], 409);
|
||||||
|
assert_eq!(convergence["conflict"]["kind"], "command-conflict");
|
||||||
|
assert_eq!(
|
||||||
|
convergence["conflict"]["error"],
|
||||||
|
"command_id was already used for a different payload"
|
||||||
|
);
|
||||||
|
assert_eq!(convergence["rejectionStatus"], 400);
|
||||||
|
assert_eq!(convergence["rejection"]["kind"], "invalid-command");
|
||||||
|
assert_eq!(convergence["rejection"]["error"], "unknown card_id");
|
||||||
|
assert_eq!(convergence["queueCount"], 0);
|
||||||
|
assert_eq!(convergence["snapshot"]["serverSequence"], 1);
|
||||||
|
assert_eq!(convergence["snapshot"]["cards"][0]["id"], 1);
|
||||||
|
assert_eq!(convergence["snapshot"]["cards"][0]["column"], "done");
|
||||||
|
assert_eq!(
|
||||||
|
convergence["history"]
|
||||||
|
.as_str()
|
||||||
|
.expect("acknowledgement history")
|
||||||
|
.matches(command_id)
|
||||||
|
.count(),
|
||||||
|
1,
|
||||||
|
"duplicate replay emitted another acknowledgement: {convergence}"
|
||||||
|
);
|
||||||
|
|
||||||
|
driver.goto(&format!("http://{app_addr}/")).await?;
|
||||||
|
wait_until(
|
||||||
|
&driver,
|
||||||
|
"return document.querySelector('[data-hemx-root]')?.hasAttribute('data-kanban-command-ready') === true",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
driver
|
driver
|
||||||
.execute(
|
.execute(
|
||||||
r#"
|
r#"
|
||||||
|
|||||||
Reference in New Issue
Block a user