* fix(testing): the convergence gate elects sync before cloning identity The browser convergence gate never performed the backup-and-sync election — its preparation only waited for the task list, so both clients ran local-only (lofi's own first-boot contract) and could not converge against any server. That produced the identical-failure 'controlled experiment' that was misread as an upstream jazz-tools defect, with a boot-time CatalogueWriteDenied console warning (benign; emitted once per client on every server, deployed schema or not) misattributed as the cause. The gate now runs the real two-step phrase ceremony with a virtual authenticator in preparePrimary, and its instructions require localhost — an IP address is not a valid WebAuthn RP ID, so on 127.0.0.1 (the previously documented invocation) the election could never start. With the election in place the identical scenario converges in all four directions in about a second, online-only and offline-reconnect, against both the first-party managed server and a lofi-node backend. Decision record 0002 carries the full evidence; troubleshooting's example URL is corrected. Closes the B1/C1 investigation on #123: not upstream. * test: accept the starter snapshot for the convergence gate fix The gate test ships in the generated starter; the snapshot follows its election fix.
- commit
- 1377b7771290d7dc338eca8a750aa9a61ac823cb
- parent
- dd3134529e129f10d3a6a6c63f0eb330507b2f48
- author
- FelineStateMachine <dami.etoile@gmail.com>
| M | apps/ | 44 | ++++++++++++++--- |
| A | docs/ | 78 | ++++++++++++++++++++++++++++++ |
| M | docs/ | 5 | ++- |
| M | package/ | 2 | +- |
@@ -9,11 +9,18 @@ import type {
99 * clients each add a task while offline, then reconnect and converge on both.1010 *1111 * This is an opt-in browser gate, not part of `deno task test`:12- * 1. Configure managed sync (JAZZ_APP_ID + JAZZ_SERVER_URL in .env) so two13- * clients share one synced account, then serve the app — for example12+ * 1. Configure managed sync (JAZZ_APP_ID + JAZZ_SERVER_URL in .env) with the13+ * schema deployed to that store, then serve the app — for example1414 * `deno task build` followed by `deno task preview`, or `deno task dev`.15- * 2. Point the test at that URL and run it directly:16- * LOFI_E2E_BASE_URL=http://127.0.0.1:4321/ deno test -A tests/convergence_e2e_test.ts15+ * 2. Point the test at that URL — it must be `localhost`, not `127.0.0.1`,16+ * because the sync election enrolls a passkey and an IP address is not a17+ * valid WebAuthn RP ID — and run it directly:18+ * LOFI_E2E_BASE_URL=http://localhost:4321/ deno test -A tests/convergence_e2e_test.ts19+ *20+ * The primary client performs the real backup-and-sync election (with a21+ * virtual authenticator) before its state is cloned: lofi is local-only by22+ * design until the account opts in, so without the election there is nothing23+ * to converge and the gate would fail for a reason that indicts no one.1724 *1825 * Without LOFI_E2E_BASE_URL (or without Chromium installed) it skips, so the1926 * default suite stays fast and never launches a browser.@@ -50,10 +57,27 @@ Deno.test("two clients converge on concurrent offline task edits", async () => {
5057 createTwoClientFixture,5158 runConcurrentOfflineConvergence,5259 waitForReady,60+ withVirtualAuthenticator,5361 } = await import("@nzip/lofi/testing");5462 5563 const ready = (client: BrowserTestClient) =>5664 waitForReady(client.page, taskListReady, undefined, { description: `${client.name} ready` });65+ // The backup-and-sync election, driven through the real account UI. Cloning66+ // afterwards carries the elected, synced account into the second client.67+ const electSync = async (client: BrowserTestClient) => {68+ await withVirtualAuthenticator(client.page);69+ await client.page.getByRole("button", { name: "Back up & enable sync" }).click();70+ await client.page.locator('[aria-label="Recovery phrase"] li').first().waitFor({71+ state: "visible",72+ timeout: 30_000,73+ });74+ await client.page.getByRole("button", { name: "I saved my phrase — enable sync" }).click();75+ await client.page.getByRole("heading", { name: "Backed up & syncing" }).waitFor({76+ state: "visible",77+ timeout: 30_000,78+ });79+ await ready(client);80+ };5781 const addTask = async (client: BrowserTestClient, text: string) => {5882 await client.page.fill("#new-task", text);5983 await client.page.press("#new-task", "Enter");@@ -68,9 +92,15 @@ Deno.test("two clients converge on concurrent offline task edits", async () => {
6892 try {6993 fixture = await createTwoClientFixture({7094 baseURL,71- // Shared identity clones the first client's device key in memory, so both72- // browser contexts act as the same synced account.73- identity: { mode: "shared", preparePrimary: ready },95+ // Shared identity clones the first client's state in memory after the96+ // election, so both browser contexts act as the same synced account.97+ identity: {98+ mode: "shared",99+ preparePrimary: async (client) => {100+ await ready(client);101+ await electSync(client);102+ },103+ },74104 artifacts: { directory: "test-results" },75105 });76106 } catch (error) {@@ -0,0 +1,78 @@
1+# Browser convergence: no upstream defect — the gate never elected sync2+3+Date: 2026-07-18\4+Decision: reject the "upstream convergence defect" diagnosis; fix the gate's preconditions; classify5+`CatalogueWriteDenied` at boot as benign upstream console noise, not a sync failure6+7+## Question8+9+The browser convergence gate (`apps/reference/tests/convergence_e2e_test.ts`) failed with10+`CatalogueWriteDenied` "identically against lofi-node and lofi's own managed dev server", and this11+was recorded as an upstream jazz-tools alpha.53 defect (lofi-node `docs/hosting-lofi-apps.md`12+validation status; #123 items B1/B3/C1; the homepage caveat in #122 §1). Is it actually upstream,13+and what is the minimal reproduction?14+15+## Contract IDs16+17+DX-LOCAL-01 and DX-OFFLINE-01 surround the behavior; the affected claim is the documented18+"known-broken two-browser reconciliation" status itself, which gated homepage copy (#122 §1).19+20+## Exact versions21+22+`jazz-tools@2.0.0-alpha.53` / `jazz-napi@2.0.0-alpha.53`, lofi main at the A3-phase-2 merge,23+lofi-node main at the docs-adoption merge, Playwright Chromium (pinned 1.61.1 install).24+25+## Control26+27+Headless two-client convergence (`jazz-tools` `createDb` clients, same account, concurrent offline28+edits) against lofi-node — passing throughout (lofi-node `tests/convergence_test.ts`).29+30+## Procedure31+32+Instrumented probe (per-client console capture, CDP WebSocket frames, per-direction convergence33+checks with independent timeouts), run in four configurations: dev server without managed config;34+managed server with deployed schema but no sync election; managed server with the real35+backup-and-sync election performed through the account UI before identity cloning; the same against36+a lofi-node backend with the schema deployed through its admin path.37+38+## Evidence39+40+1. Both clients — including the first, alone, at boot, before any offline edit — log41+ `CatalogueWriteDenied` (`sync_manager/inbox.rs:1401`) for the same content-addressed object id on42+ every server and store, deployed schema or not. The denial is not correlated with the second43+ client or with reconnection.44+2. Without election, neither client held any `lofi:` state: no `sync-elected`, no managed namespace.45+ Both ran local-only — lofi's own documented first-boot contract — so non-convergence was the46+ designed outcome, identical against every server. The historical "controlled experiment" compared47+ two non-syncing runs; its parity conclusion was vacuous.48+3. With the election performed (virtual authenticator, the real two-step phrase ceremony), the49+ identical scenario converges in all four directions in ~1s — online-only and offline-reconnect —50+ against both the first-party managed server and lofi-node. The `CatalogueWriteDenied` warning51+ still appears once per client at boot while sync works, proving it benign.52+4. A second latent failure hid the first: the documented invocation used `127.0.0.1`, where the53+ election ceremony cannot start because an IP address is not a valid WebAuthn RP ID. On54+ `localhost` the ceremony works.55+56+## Decision57+58+- The gate now performs the backup-and-sync election in `preparePrimary` before identity cloning,59+ and its instructions require `localhost`. It passes against both servers.60+- B1 (distill an upstream repro) resolves as **not upstream** for convergence. The remaining61+ upstream observation is cosmetic: every browser client attempts one unauthorized catalogue write62+ at boot and logs a WARN — worth an upstream polish report, not a standing defect issue.63+- B3 (standing upstream issue for "browser reconnect denial") is **not filed**: its condition — a64+ verifiable upstream defect — does not exist.65+- C1 (expected-fail canary) is moot in its planned form; the fixed gate is the assurance artifact.66+67+## Contract delta68+69+"Two real browser clients reconciling after offline edits" moves from known-broken to validated70+against both servers. The lofi-node validation-status claim and the #122 §1 homepage constraint ("do71+not re-point this copy until the upstream fix lands") are both superseded — the scenario the72+homepage describes works and can be claimed truthfully.73+74+## Follow-up75+76+lofi-node `docs/hosting-lofi-apps.md` validation status rewritten (companion PR); #122 §1 unblocks;77+optional: promote the convergence scenario into the golden suite for continuous coverage, and an78+upstream courtesy report for the boot-time catalogue-write warning.@@ -93,10 +93,13 @@ configuration. Rotate a real credential if it entered source control or an artif
9393 Set the base URL when running the opt-in example:9494 9595 ```sh96-LOFI_E2E_BASE_URL=http://127.0.0.1:4321/ \96+LOFI_E2E_BASE_URL=http://localhost:4321/ \9797 deno test -A tests/convergence_e2e_test.ts9898 ```9999 100+The base URL must say `localhost`, not `127.0.0.1`: the convergence gate enrolls a passkey to elect101+sync, and an IP address is not a valid WebAuthn RP ID, so the ceremony never starts.102+100103 Install the pinned Chromium runtime if needed:101104 102105 ```sh@@ -25,7 +25,7 @@
2525 "tests/auth_e2e_test.ts": "87dc3eb441eeaa96fc82b15df90bf4108096506217e5bfae907b483a3e75b4bf",2626 "tests/author-boundary_test.ts": "8340fda5d2764ed3c57e72e72d36974efca00c1c0ae126adfbdc4b394963d81f",2727 "tests/backup_migration_e2e_test.ts": "7ad87dad687641ba937ab20b0c5d06a34478c7c08527ed6e8c6861ad6c97d622",28- "tests/convergence_e2e_test.ts": "71ec0ac935c25ff0722905e5b195c730f32a3b886dd4172e11d9e0e80f19cb38",28+ "tests/convergence_e2e_test.ts": "191bdcda1dc6bffba354c0d53cdeb7b5b892e2c7face3010a29a61d3cb0569a5",2929 "tests/testing-contract_test.ts": "4257306060d42566b879d92b3a6c395805b20f53717bee55d670c0ef7a117eec",3030 "tsconfig.json": "c632473513c623f3cc69d7476159fb9421624521220029755695defa265c6cab"3131 }