test(kanban): prove server-first asset isolation

req: performance/006
This commit is contained in:
slhx agent
2026-07-13 15:50:35 +02:00
parent 978bf67a2c
commit 1d194e0f23
5 changed files with 107 additions and 6 deletions
Generated
+1
View File
@@ -579,6 +579,7 @@ dependencies = [
"hemx-build",
"hemx-test",
"scraper",
"thirtyfour 0.35.0",
"tokio",
]
+4 -4
View File
@@ -32,17 +32,17 @@ encryption, retention, backup, and deployment policy remain host concerns.
## Slice 3 — durable offline command log
- [ ] **User value:** an opted-in Kanban mutation remains available after network loss and browser reload without storing DOM patches as truth.
- **State:** In progress — the first app-owned `reorder_card` command is transactionally persisted with schema, actor, session, causal id, and app payload before projection; an app-owned service worker caches only the generated shell/resources, and reload restores the projection after the fixture server is stopped and proven unreachable. Native recovery controls export a versioned credential-free command envelope, delete queued commands while preserving actor/causal identity, and reset command data, identity, cache, and registration behind explicit confirmation. Unknown schemas stop with an explicit diagnostic. Transaction conflicts and injected quota exhaustion prove failed persistence neither projects nor emits a durability claim, expose stage/code without command payload, and remain recoverable through export/delete/reset. Malformed current-schema records are rejected field-specifically without partial projection while export/delete recovery remains available. Replay is preflighted before effects, capped by the app at 64 commands, measured against a 100 ms browser budget, and over-limit queues fail closed with export/delete recovery. A programmatic queued/busy state appears within the 100 ms direct-interaction budget before blocked persistence completes, without claiming durability or projecting early. Optional-asset isolation for `performance/006` remains; that requirement is separate from interaction latency.
- [x] **User value:** an opted-in Kanban mutation remains available after network loss and browser reload without storing DOM patches as truth.
- **State:** Complete — the app-owned `reorder_card` command is transactionally persisted with schema, actor, session, causal id, and app payload before projection; an app-owned service worker caches only the generated shell/resources, and reload restores the projection after the fixture server is stopped and proven unreachable. Native recovery controls export a versioned credential-free command envelope, delete queued commands while preserving actor/causal identity, and reset command data, identity, cache, and registration behind explicit confirmation. Unknown schemas and malformed current-schema records stop before projection with explicit recovery diagnostics. Transaction conflicts and injected quota exhaustion neither project nor emit durability claims and remain recoverable through export/delete/reset. Replay is preflighted before effects, capped by the app at 64 commands, and measured against a 100 ms browser budget. A programmatic queued/busy state appears within the 100 ms direct-interaction budget before blocked persistence completes. The ordinary server-first route loads only the fingerprinted base runtime and creates no client root, service worker, or IndexedDB database; PWA/WASM policy remains app-owned and opt-in.
- **Build:** add an optional durable command-log adapter around platform transactional storage; persist versioned command ids and app payload before projection; restore projection after reload; expose queue state, export/delete/reset, quota/corruption failure, and migration refusal.
- **Refusals:** no server reconciliation, CRDT, mandatory IndexedDB, credential storage, or policy hidden in core.
- **Requirements:** `local/001-004`, `sync/009`, `sync/014-015`, `sync/020`, `security/007`, `accessibility/004`, `client_local/013`, `performance/003`, `performance/005-006`.
- **Proof:** `cargo test -p hemx-wasm --test browser kanban_command_persists_before_projection_and_restores_after_reload -- --exact` proves transactional persist-before-project ordering, app-owned shell caching, current-version replay after a real Firefox reload with the fixture server unreachable, stable identity metadata, and explicit unknown-schema refusal through real WASM. `cargo test -p hemx-wasm --test browser kanban_command_export_delete_and_reset_are_recoverable -- --exact` proves accessible export/delete/reset entry points, versioned credential-free export, confirmation before destructive actions, preserved identity after queue deletion, and fresh identity/baseline projection after reset. `cargo test -p hemx-wasm --test browser kanban_persistence_failure_does_not_project_and_recovers -- --exact` proves transactional failure does not project or emit `kanban:command-persisted`, reports non-payload stage/code diagnostics, and recovers through the ordinary deletion path. `cargo test -p hemx-wasm --test browser kanban_corrupt_command_refuses_projection_and_recovers -- --exact` proves strict current-schema validation, no partial projection, visible non-payload diagnostics, raw versioned export for recovery, and ordinary deletion recovery. `cargo test -p hemx-wasm --test browser kanban_replay_is_bounded_and_within_budget -- --exact` proves 64-command preflight/replay within the 100 ms browser budget, zero partial projection at 65 commands, and export/delete recovery. `cargo test -p hemx-wasm --test browser kanban_quota_failure_is_fail_closed_and_recoverable -- --exact` proves quota-specific fail-closed behavior, no false durability event, transactional metadata rollback, and export/delete/reset recovery. `cargo test -p hemx-wasm --test browser kanban_queued_status_precedes_durable_projection_within_budget -- --exact` proves queued/busy feedback within the `performance/003` and `client_local/013` 100 ms budget while persistence is blocked, followed by durable projection only after commit. The completed slice proof must additionally prove optional PWA/WASM assets do not load on the server-first path for `performance/006`.
- **Proof:** `cargo test -p hemx-wasm --test browser kanban_command_persists_before_projection_and_restores_after_reload -- --exact` proves transactional persist-before-project ordering, app-owned shell caching, current-version replay after a real Firefox reload with the fixture server unreachable, stable identity metadata, and explicit unknown-schema refusal through real WASM. `cargo test -p hemx-wasm --test browser kanban_command_export_delete_and_reset_are_recoverable -- --exact` proves accessible export/delete/reset entry points, versioned credential-free export, confirmation before destructive actions, preserved identity after queue deletion, and fresh identity/baseline projection after reset. `cargo test -p hemx-wasm --test browser kanban_persistence_failure_does_not_project_and_recovers -- --exact` proves transactional failure does not project or emit `kanban:command-persisted`, reports non-payload stage/code diagnostics, and recovers through the ordinary deletion path. `cargo test -p hemx-wasm --test browser kanban_corrupt_command_refuses_projection_and_recovers -- --exact` proves strict current-schema validation, no partial projection, visible non-payload diagnostics, raw versioned export for recovery, and ordinary deletion recovery. `cargo test -p hemx-wasm --test browser kanban_replay_is_bounded_and_within_budget -- --exact` proves 64-command preflight/replay within the 100 ms browser budget, zero partial projection at 65 commands, and export/delete recovery. `cargo test -p hemx-wasm --test browser kanban_quota_failure_is_fail_closed_and_recoverable -- --exact` proves quota-specific fail-closed behavior, no false durability event, transactional metadata rollback, and export/delete/reset recovery. `cargo test -p hemx-wasm --test browser kanban_queued_status_precedes_durable_projection_within_budget -- --exact` proves queued/busy feedback within the `performance/003` and `client_local/013` 100 ms budget while persistence is blocked, followed by durable projection only after commit. `cargo test -p hemx-kanban-example --test browser_e2e server_first_route_does_not_load_optional_client_assets -- --exact` proves the server-first route loads only its fingerprinted base runtime and creates no optional client root, PWA/WASM request, service worker, or IndexedDB database.
## Slice 4 — authoritative reconnect and convergence
- [ ] **User value:** offline and concurrent work reconnects without duplicate mutation, silent loss, stale authorization, or ambiguous conflict.
- **State:** Blocked by Slice 3.
- **State:** Ready — Slice 3 is complete; begin with one idempotent server command and canonical acknowledgement through a real reconnect transport before adding conflicts or multi-tab policy.
- **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`.
+2 -1
View File
@@ -35,8 +35,9 @@ tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"]
hemplate = { path = "../../../hemplate/hemplate" }
[dev-dependencies]
scraper = "0.23"
hemx-test = { path = "../../hemx-test" }
scraper = "0.23"
thirtyfour = "0.35"
[build-dependencies]
hemx-build = { path = "../../hemx-build" }
+3 -1
View File
@@ -122,7 +122,9 @@ async fn main() {
.route(runtime_js_path(), get(runtime))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
let addr = std::env::var("HEMX_KANBAN_ADDR")
.map(|value| value.parse::<SocketAddr>().expect("valid HEMX_KANBAN_ADDR"))
.unwrap_or_else(|_| SocketAddr::from(([127, 0, 0, 1], 3001)));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("hemx Kanban example: http://{addr}");
axum::serve(listener, app).await.unwrap();
+97
View File
@@ -0,0 +1,97 @@
use hemx_test::TestProcess;
use std::net::TcpListener;
use std::process::Command;
use std::time::Duration;
use thirtyfour::prelude::*;
const STARTUP_TIMEOUT: Duration = Duration::from_secs(12);
#[tokio::test]
async fn server_first_route_does_not_load_optional_client_assets() -> WebDriverResult<()> {
// test req: performance/006
let app_port = available_port();
let app_addr = format!("127.0.0.1:{app_port}");
let mut app = Command::new(env!("CARGO_BIN_EXE_hemx-kanban-example"));
app.env("HEMX_KANBAN_ADDR", &app_addr);
let _app = TestProcess::start(app, "hemx-kanban", &app_addr, STARTUP_TIMEOUT)
.expect("start ready 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}/")).await?;
driver.find(By::Css("section[data-hemx-root='kanban']")).await?;
let loaded = driver
.execute_async(
r#"
const done = arguments[arguments.length - 1];
navigator.serviceWorker.getRegistrations().then(async (registrations) => {
const databases = indexedDB.databases ? await indexedDB.databases() : [];
done({
resources: performance.getEntriesByType('resource').map((entry) => new URL(entry.name).pathname),
scripts: [...document.scripts].map((script) => ({ src: new URL(script.src).pathname, type: script.type })),
clientRoots: document.querySelectorAll('[data-hemx-client-module]').length,
serviceWorkers: registrations.length,
databases: databases.map((database) => database.name),
});
}).catch((error) => done({ error: String(error) }));
"#,
Vec::new(),
)
.await?
.json()
.clone();
assert!(loaded["error"].is_null(), "browser inspection failed: {loaded}");
assert_eq!(loaded["clientRoots"], 0);
assert_eq!(loaded["serviceWorkers"], 0);
assert_eq!(loaded["databases"].as_array().map(Vec::len), Some(0));
let scripts = loaded["scripts"].as_array().expect("document scripts");
assert_eq!(scripts.len(), 1);
let runtime_path = scripts[0]["src"].as_str().expect("runtime script path");
assert!(
runtime_path.starts_with("/hemx.") && runtime_path.ends_with(".js"),
"unexpected server runtime asset: {loaded}"
);
assert_eq!(scripts[0]["type"], "");
let resources = loaded["resources"]
.as_array()
.expect("resource timing entries")
.iter()
.filter_map(|value| value.as_str())
.collect::<Vec<_>>();
assert!(resources.contains(&runtime_path), "runtime was not loaded: {loaded}");
for optional in [
"/hemx.client.js",
"/kanban_client.js",
"/kanban_client_bg.wasm",
"/app.js",
"/offline.js",
] {
assert!(
!resources.contains(&optional),
"server-first route loaded optional client asset {optional}: {loaded}"
);
}
Ok(())
}
.await;
let quit = driver.quit().await;
result.and(quit)
}
fn available_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.expect("reserve browser test port")
.local_addr()
.expect("browser test address")
.port()
}