Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_sync.mjs

142 KiB, 10 runs

created by r2519314175:715, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_sync.mjs — a user's work travels between devices through the gateway's
2// encrypted mailbox, and two devices editing at once converge rather than clobber.
3//
4// This drives the real client engine (sync.js) against the REAL gateway (/api/sync), so
5// it needs the dev stack up: the app (DAIMOND_PORT, default 8777) and the gateway on
6// :9002.
7//
8// 1. Sign in, make a chat, push. The mailbox holds a version >= 1, and its blob
9// is ciphertext — the plaintext codeword must NOT appear in it.
10// 2. Simulate a second device: wipe local chats and the version cursor, pull, and
11// confirm the chat's transcript comes back decrypted and merged.
12// 3. Conflict: another device bumps the mailbox out of band, then this device
13// pushes from its now-stale version; the engine must 409, pull, merge and retry
14// to success — the version advances, no work lost.
15// 4. Diamonds travel too. A Diamond is a DIRECTORY, not a record, so the whole
16// of it must arrive — name, tags, crystal, log, links, and which model it
17// thinks with. A deletion must travel and STAY travelled, the freshest copy
18// must win, and a parcel from a device that predates any of this must still
19// apply.
20// 5. The identity export bundle carries the salt, without which a second device
21// could never derive the key to open any of this.
22// 6. A parcel the gateway refuses as too large (413) is SAID so — on the sync
23// chip, held, with the reason on hover — and the stall clears on the next
24// push that works.
25// 7. A parked tab converges on window focus, without a reload, and a focus
26// storm coalesces into one pull.
27// 8. Provider keys and model lists travel too: sealed keys only, deterministic
28// to the byte, freshest-wins per provider, and a union so neither device
29// loses a provider the other has never seen.
30// 10. One section of a parcel that cannot be merged costs only itself: the
31// Diamonds beside it still arrive, and the merge says what it could not do.
32// 11. A reconcile that gives up says so on the chip, rather than leaving the
33// "Synced" its own pull put there.
34// 12. Two REAL devices, on two browser profiles, converge in BOTH directions --
35// including the one that is not being typed at, which raises no focus event
36// and ends no turn and therefore never asked the gateway anything at all.
37// 13. And it converges with NOTHING happening on it at all: the gateway taps
38// every other device of the account when the mailbox moves, so a window
39// left open on a second desk applies the other one's work within seconds --
40// no focus, no visibility change, no settling event, no reload. Over a
41// socket where the front door carries one and over a parked request where
42// it does not, across a gateway restart, carrying version integers only.
43import { open, chat, signInAs } from './harness.mjs';
44import { makePagePro } from './pro.mjs';
45import { GW_PORT, GW_URL } from './ports.mjs';
46import { spawn, execFileSync } from 'node:child_process';
47import fs from 'node:fs';
48import path from 'node:path';
49import { fileURLToPath } from 'node:url';
50
51const ok = [], bad = [];
52const check = (name, pass, detail) => {
53 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
54 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
55};
56
57/// Push, and wait until THIS DEVICE'S OWN PARCEL is what the mailbox holds.
58///
59/// A single push() call is not enough: one that finds another in flight (or the
60/// app busy) only reschedules and returns, so awaiting it proves nothing.
61///
62/// THE VERSION ADVANCING IS NOT ENOUGH EITHER, which is what this helper used
63/// to wait for, and it is why one check per run went red and never the same
64/// one. `serverVersion` moves on any completed round, and three of them are not
65/// this push:
66///
67/// * a round that was ALREADY IN FLIGHT when the caller made its change. It
68/// collected the parcel before the change existed, and it lands carrying
69/// the state as it was. The version advances, the wait is satisfied, and
70/// the pull that follows reads a mailbox that never saw the change --
71/// "the second device pulls the shared Diamond down" and "a deleted
72/// workspace file is restored by pull", both of which failed exactly here.
73/// * `pull()` adopting a higher version from another device.
74/// * push()'s own idle branch: with nothing new to send it pulls instead,
75/// which can advance the version having sent nothing at all.
76///
77/// So the wait is on the only fact the callers actually depend on: the mailbox
78/// decrypts to a parcel THIS device produced after the change. The parcel is
79/// resampled each round and every sample kept, because a stray landing round
80/// may carry a perfectly good newer parcel; what can never be accepted is the
81/// one collected before the caller's change, which is never sampled at all.
82///
83/// A push that does not land is a FAILURE, not a note. It used to print a line
84/// and carry on, so the red surfaced two hundred lines later as an unrelated
85/// merge fault.
86async function pushLanded(pg, where) {
87 const r = await pg.evaluate(async (ms) => {
88 const mailbox = async () => {
89 const res = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
90 const j = await res.json();
91 if (!j.present) return null;
92 try { return await window.DaimondIdentity.unwrap(j.blob); }
93 catch (e) { return null; }
94 };
95 const mine = new Set();
96 const t0 = Date.now();
97 let rounds = 0, held = null, sample = '';
98 while (Date.now() - t0 < ms) {
99 rounds++;
100 await window.DaimondSync.push();
101 sample = JSON.stringify(await window.DaimondSync.parcel());
102 mine.add(sample);
103 held = await mailbox();
104 if (held !== null && mine.has(held)) return { landed: true, rounds, took: Date.now() - t0 };
105 await new Promise(r => setTimeout(r, 200));
106 }
107 return {
108 landed: false, rounds, took: Date.now() - t0,
109 // Which it is: a mailbox holding somebody else's parcel and a mailbox
110 // holding nothing readable are different faults.
111 mailbox: held === null ? '(absent or undecryptable)' : String(held.length) + ' bytes',
112 parcel: String(sample.length) + ' bytes',
113 state: JSON.stringify(window.DaimondSync.state()),
114 };
115 }, 25000);
116 if (!r.landed) {
117 check('a push reached the mailbox' + (where ? ' (' + where + ')' : ''), false,
118 r.rounds + ' rounds in ' + r.took + 'ms, mailbox ' + r.mailbox
119 + ' vs parcel ' + r.parcel + ' — ' + r.state);
120 }
121 return r.landed;
122}
123
124const sleep = ms => new Promise(r => setTimeout(r, ms));
125
126/// Put `window.__bump(tag)` on the page: one genuine local change, so the push
127/// that follows is not skipped as a no-op.
128///
129/// THE BLINDING THIS EXISTS FOR. Every stubbed refusal below — 413, 402, 409 —
130/// is only reached if a parcel is actually SENT, and the engine sends one only
131/// when the parcel differs from the last one it sent. The helper this replaces
132/// wrote `daimond-chats` in localStorage. Transcripts moved into IndexedDB, so
133/// those writes changed nothing `collectSync()` reads: every push was skipped
134/// before any request was made, no stub was ever reached, and eighteen checks
135/// reported on something other than the behaviour they name.
136///
137/// Two things are done about that. The change goes through the app's own chat
138/// store — `DaimondCore.chatStore()`, the very object `collectSync()` packs, so
139/// a store that moves again takes this with it LOUDLY (the call throws) rather
140/// than silently. And the parcel is COMPARED across the change, so a bump that
141/// stops moving it says so at the call site instead of surfacing as an
142/// unrelated red four sections later. Each caller asserts on what it returns.
143///
144/// The tombstone map `daimond-msgs-deleted` would also have worked (see
145/// verify_sessionrenew, which uses it) and is inert by construction. This is
146/// preferred here because these sections say "a genuine local change" and mean
147/// a transcript: what travels is the same section of the parcel the 413 and 409
148/// arms exist to protect.
149const installBump = (pg) => pg.evaluate(() => {
150 window.__bump = async function (tag) {
151 const before = JSON.stringify(await window.DaimondCore.collectSync());
152 const store = window.DaimondCore.chatStore();
153 const list = store.stored();
154 if (!list.length) return { moved: false, why: 'no chat in the store to change' };
155 list[0].messages = (list[0].messages || []).concat([
156 { role: 'user', content: tag, mid: tag, ts: Date.now() },
157 ]);
158 list[0].updatedAt = Date.now();
159 store.save(list);
160 const after = JSON.stringify(await window.DaimondCore.collectSync());
161 return { moved: after !== before, why: after === before ? 'the parcel did not move' : '' };
162 };
163
164 /// Push until a parcel really LEAVES this device, and say whether one did.
165 ///
166 /// THE SECOND BLINDING. `__bump` above makes sure there is something to send;
167 /// this makes sure it is sent. `push()` answers a caller no differently
168 /// whether it sent or stood aside -- over a live turn, and over a round
169 /// already in flight, it only reschedules and returns -- so every stubbed
170 /// refusal below (413, 402, 409) could be asserted against a gateway nobody
171 /// had spoken to. Observed: "a permanent conflict is retried, and bounded"
172 /// red at posts=0, with the chip still reading "Syncing…" from the round
173 /// that had swallowed the call.
174 ///
175 /// `count` is the section's own POST counter, so what is waited for is the
176 /// thing the section measures. Re-bumped each round because a round that was
177 /// in flight may have sent the change for real before the stub went on,
178 /// leaving the engine with nothing to send and no reason to speak again.
179 window.__pushSent = async function (count, tag, ms) {
180 const t0 = Date.now();
181 let rounds = 0;
182 while (count() === 0 && Date.now() - t0 < (ms || 15000)) {
183 rounds++;
184 if (rounds > 1) await window.__bump(tag + '-' + rounds);
185 await window.DaimondSync.push();
186 if (count() === 0) await new Promise(r => setTimeout(r, 250));
187 }
188 return { sent: count() > 0, rounds: rounds, took: Date.now() - t0 };
189 };
190});
191
192/// One local change, from Node. Returns `{ moved, why }`.
193const bumped = (pg, tag) => pg.evaluate(t => window.__bump(t), tag);
194
195/// Did every change a section made really move the parcel? The sections that
196/// bump from inside their own `evaluate` collect the answers and hand them back
197/// as `tag: moved` / `tag: <why not>` lines.
198const allMoved = (lines) => Array.isArray(lines) && lines.length > 0
199 && lines.every(l => / moved$/.test(l));
200
201/// Is the gateway answering?
202async function gatewayUp() {
203 try {
204 const r = await fetch(`${GW_URL}/api/health`, { signal: AbortSignal.timeout(2000) });
205 return r.ok;
206 } catch (e) { return false; }
207}
208
209/// Stop the running gateway and start it again exactly as it was.
210///
211/// Exactly as it was matters: the suite may be running it from `gateway/` or
212/// from the generated `dev/devgw/`, and which one decides what config it reads.
213/// Both are taken from the live process rather than guessed, so this restarts
214/// whatever is actually there and hands it back in the state it found it.
215///
216/// Returns `true`, or a string saying what went wrong -- this test is the one
217/// place in the suite that takes the gateway down, and it must not leave the
218/// verifiers that run after it wondering why.
219///
220/// WHICH PROCESS. Not `pgrep -x daimond_gateway`. A libtest harness built from
221/// `src/app_main.rs` is called `daimond_gateway-<hash>`, and Linux truncates a
222/// process name to fifteen characters -- which is exactly `daimond_gateway`. So on
223/// a machine where any lane is running `cargo test` in gateway/, that pgrep matches
224/// the test harness, `[0]` picks it, and this function reads /proc/<it>/exe and
225/// spawns A TEST BINARY as the gateway. The port is the identity: the gateway is
226/// whatever answers on :9002, because that is the only thing the rest of the suite
227/// means by "the gateway".
228async function restartGateway() {
229 let pid;
230 try {
231 const ss = execFileSync('ss', ['-ltnp', `sport = :${GW_PORT}`], { encoding: 'utf8' });
232 pid = (/pid=(\d+)/.exec(ss) || [])[1];
233 } catch (e) { return `could not ask which process holds :${GW_PORT}: ` + e.message; }
234 if (!pid) return 'no daimond_gateway process to restart';
235
236 let cwd, exe;
237 try {
238 cwd = fs.readlinkSync(`/proc/${pid}/cwd`);
239 // Linux appends " (deleted)" to this link once the binary has been
240 // REPLACED under the running process -- which is what a rebuild does, and
241 // a rebuild during a test run is an ordinary Tuesday. Spawning the link
242 // verbatim then failed ENOENT on a path ending in that suffix, and the
243 // failure arrived as an ChildProcess 'error' EVENT rather than a throw,
244 // so it went round the whole file's try/catch and killed the run outright
245 // with everything before it green and nothing said about why.
246 exe = fs.readlinkSync(`/proc/${pid}/exe`).replace(/ \(deleted\)$/, '');
247 } catch (e) { return 'could not read the gateway process: ' + e.message; }
248 if (!fs.existsSync(exe)) return 'the gateway binary is no longer at ' + exe;
249
250 // THE ONE PROCESS, never `pkill -x daimond_gateway`. That form signals every
251 // process on the machine whose (truncated) name is `daimond_gateway`, which
252 // includes every other lane's libtest harness and every other worktree's
253 // gateway. The pid is already known; use it.
254 try { process.kill(Number(pid), 'SIGTERM'); } catch (e) { /* already gone */ }
255 for (let i = 0; i < 20 && await gatewayUp(); i++) await sleep(500);
256 if (await gatewayUp()) return 'the gateway would not stop';
257
258 let spawnErr = '';
259 const child = spawn(exe, [], {
260 cwd, detached: true, stdio: 'ignore',
261 env: { ...process.env, APP_MODE: process.env.APP_MODE || 'sandbox' },
262 });
263 // A spawn failure is an EVENT, not a throw. Unhandled it is fatal to the
264 // whole run — see the note on the " (deleted)" suffix above.
265 child.on('error', (e) => { spawnErr = String(e && e.message || e); });
266 child.unref();
267 await sleep(200);
268 if (spawnErr) return 'the gateway would not start: ' + spawnErr;
269 for (let i = 0; i < 60; i++) {
270 if (await gatewayUp()) return true;
271 await sleep(500);
272 }
273 return `the gateway did not come back on :${GW_PORT}`;
274}
275
276const s = await open({ name: 'sync', signIn: true, connect: true, defaults: false });
277// NOTHING LEAVES THIS MACHINE. The sync checks seed a real Groq provider with a key to
278// prove the provider store travels, and from 2026-08-20 a catalogue over a day old asks
279// for itself -- so a suite that had never been a client of anybody's API quietly became
280// one. The seeded row is a fixture and its host is stubbed rather than reached.
281await s.page.route('https://api.groq.com/**', (r) => r.fulfill({
282 status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }),
283}));
284const { page } = s;
285let child = null; // a second REAL device, paired in at (12)
286
287// The engine and its dependencies must be live and the session authed.
288await page.waitForFunction(
289 () => !!window.DaimondSync && !!window.DaimondCore && !!window.DaimondGateway
290 && DaimondGateway.state().authed,
291 null, { timeout: 12000 },
292).catch(() => {});
293
294try {
295 const authed = await page.evaluate(() => DaimondGateway.state().authed);
296 check('gateway session is authed (sync can reach its mailbox)', authed);
297 await installBump(page);
298
299 // Sync IS the Pro capability under test, so the account has to hold Pro:
300 // without it the gateway answers 402 and there is nothing below to measure.
301 const GWDIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'gateway');
302 const lic = await makePagePro(page, GWDIR);
303 // AN UNPROVISIONED IDENTITY REFUSES BY NAME, rather than reddening every check below it.
304 //
305 // `makePagePro` answers `{ id: '', status: 0, pro: false }` when this page's identity has
306 // no gateway account at all -- the binding `/api/account` makes, which `dev/provision.mjs`
307 // is what calls. Until 2026-08-24 `run_all.sh` provisioned one identity, `compose`, and
308 // this file drives its own, `sync`. So it came out of every gate at 37 failed / 68 passed,
309 // with `pro=false` on the second line and all thirty-five others downstream of it: sync is
310 // Pro-gated, so without Pro there is nothing below to measure. Nothing was wrong with the
311 // engine -- provisioned by hand it answers 177/177 -- and the report said "regression".
312 //
313 // So the missing thing is named, and the file stops. A suite reads `SKIPPED:` and counts
314 // it as neither a pass nor a fail, which is what it is.
315 if (!lic.id) {
316 console.log('SKIPPED: this file drives the `sync` identity, and it has no gateway '
317 + 'account. Nothing here can be measured without one, because sync is behind Pro '
318 + 'and Pro is bought for an account.');
319 console.log(' To provision it: node dev/provision.mjs "$DAIMOND_SCRATCH/sync-profile" '
320 + 'sync, then `daimond_ctl topup <id> 5000`, RESTART the gateway (o3db holds its '
321 + 'key index per process), then node dev/pro.mjs <id> gateway.');
322 console.log(' dev/run_all.sh does all of that: `sync` is in NEEDS_GRANT and ident_for '
323 + 'names its identity. If you are seeing this from a suite run, that provisioning '
324 + 'failed -- read $DAIMOND_SCRATCH/suite-provision.log.');
325 process.exit(2);
326 }
327 check('the account holds Pro, so sync may run at all',
328 lic.pro === true, `webhook ${lic.status}, pro=${lic.pro}`);
329 // A refusal must never put anything over the app -- that dialog is gone, and
330 // this is what keeps it gone.
331 const overlay = await page.evaluate(() =>
332 [...document.querySelectorAll('.modal')]
333 .filter(m => getComputedStyle(m).display !== 'none')
334 .map(m => (m.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60)));
335 check('no dialog was raised over the app on the way here', overlay.length === 0,
336 overlay.join(' | '));
337
338 // A distinctive codeword, carried in a real message so it lands in the synced
339 // transcript. We then hunt for it in the ciphertext to prove it is sealed.
340 const MARK = 'ZEBRA-' + '7788';
341 await chat(s, 'Remember the codeword ' + MARK + ' for later.');
342
343 // Push and read the mailbox straight back.
344 //
345 // The push is RETRIED until the mailbox actually moves, because `push()`
346 // stands aside over a live turn -- "never over a live turn", sync.js -- and
347 // answers a caller no differently than when it sent. `chat()` returns when
348 // the send button stops looking busy, which is not the same instant as
349 // `DaimondCore.busy()` going false, so a single push here could return
350 // having only scheduled one. The mailbox then still held the round BEFORE
351 // the conversation: version 1, one chat, no messages in it, and the second
352 // device below duly got an empty transcript back. That read as a sync defect
353 // for weeks; it was this race.
354 //
355 // Waiting on the version rather than on a timer, and reporting how many
356 // rounds it took, so a push that stops landing altogether still fails here
357 // instead of being papered over by a longer wait.
358 const mailboxWas = await page.evaluate(async () => {
359 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
360 return ((await r.json()).version) | 0;
361 });
362 await pushLanded(page, 'the conversation');
363 const pushed = await page.evaluate(async () => {
364 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
365 const j = await r.json();
366 let name = '';
367 try { name = String(window.DaimondIdentity.displayName() || ''); } catch (e) { name = ''; }
368 return { version: j.version, present: j.present, blob: j.blob || '', device: j.device || '', account: name };
369 });
370 // Asserted against the MAILBOX, not against `DaimondSync.state().version`
371 // which `pushLanded` waits on: the client's own counter is this device's
372 // belief about the round, and the question here is whether the gateway
373 // really holds the conversation. `pushLanded` notes a push that never went
374 // and carries on; this is where that becomes a failure.
375 check('the push carrying the conversation actually landed',
376 (pushed.version | 0) > mailboxWas, 'v' + mailboxWas + '→' + pushed.version);
377 check('after a push the mailbox holds a version >= 1', pushed.present && pushed.version >= 1,
378 'version=' + pushed.version);
379
380 // The record's display label is the one thing the gateway keeps in the clear
381 // beside the blob. A browser-and-platform description is fine; the account
382 // name is the user's own words and must never leave the browser readable.
383 check('the plaintext device label is not the account\'s chosen name',
384 pushed.device !== '' && (pushed.account === '' || !pushed.device.includes(pushed.account)),
385 'device="' + pushed.device + '" account="' + pushed.account + '"');
386
387 // The blob is ciphertext: the plaintext codeword must not be in it, and it must
388 // not decode to readable JSON.
389 check('the stored blob is ciphertext (plaintext codeword absent)', !pushed.blob.includes(MARK));
390 const looksEncrypted = await page.evaluate((blob) => {
391 try { const t = atob(blob); return !(t.trim().startsWith('{') || t.includes('"chats"') || t.includes('messages')); }
392 catch (e) { return true; }
393 }, pushed.blob);
394 check('the blob does not decode to plaintext JSON', looksEncrypted);
395
396 // (2) Second device: wipe local chats + version cursor, then pull.
397 //
398 // The chats are wiped through the store that holds them. They are in
399 // IndexedDB, so removing the old localStorage key emptied nothing and this
400 // measured a device that had never lost anything.
401 // The shape of what came back is carried out with the count, because this
402 // check asserts TWO things -- a chat arrived, and the words in it did -- and
403 // reporting only the count says "chats=1" for both a pass and the failure
404 // where the transcript is empty. That reads as a passing check that failed.
405 const restored = await page.evaluate(async () => {
406 const store = window.DaimondCore.chatStore();
407 await store.wipe();
408 localStorage.removeItem('daimond-sync-version');
409 const emptied = store.stored().length;
410 const v = await window.DaimondSync.pull();
411 const arr = store.stored();
412 const text = JSON.stringify(arr);
413 return {
414 version: v, emptied, chatCount: arr.length, text,
415 // Per chat: its id, how many messages it holds, and which roles they
416 // are. A chat that arrives with an empty transcript and one that never
417 // arrived are different defects and this is what tells them apart.
418 shape: arr.map(c => ({
419 id: c && c.id,
420 msgs: (c && Array.isArray(c.messages)) ? c.messages.length : -1,
421 roles: (c && Array.isArray(c.messages)) ? c.messages.map(m => m && m.role).join(',') : '',
422 })),
423 };
424 });
425 check('the second device really started with no transcripts',
426 restored.emptied === 0, 'held=' + restored.emptied);
427 check('a fresh device pulls and decrypts the chat transcript back',
428 restored.chatCount >= 1 && restored.text.includes(MARK),
429 'chats=' + restored.chatCount
430 + ' codeword=' + (restored.text.includes(MARK) ? 'present' : 'ABSENT')
431 + ' ' + JSON.stringify(restored.shape));
432
433 // (3) Conflict: another device bumps the mailbox out of band (a garbage blob is
434 // fine — the gateway is opaque and checks only the version), so THIS device's
435 // known version is now stale. Its next push must 409, pull, merge and retry.
436 const conflict = await page.evaluate(async () => {
437 const before = window.DaimondSync.version();
438 // The "other device" pushes over the current version, advancing it by one.
439 const otherWrite = await fetch('/api/sync', {
440 method: 'POST', credentials: 'same-origin',
441 headers: { 'content-type': 'application/json', 'x-daimond-api': '1' },
442 body: JSON.stringify({ base_version: before, device: 'other-device', blob: 'AAAABBBBCCCCDDDD' }),
443 });
444 const otherJson = await otherWrite.json();
445 // A genuine local change, or the push is skipped before it is ever sent and
446 // there is no 409 to reconcile.
447 const changed = await window.__bump('conflict-note');
448 // This device still thinks the version is `before`. Push the fresh change.
449 //
450 // Pushed until the mailbox moves, not once: a push that finds a round
451 // already in flight reschedules and returns, and reading the version in
452 // the same tick then reported a reconcile that had not been attempted
453 // yet as one that failed. The base is still stale whichever call ends up
454 // sending, so what is waited for is still the 409-pull-retry path.
455 let after = otherJson.version;
456 const t0 = Date.now();
457 while (after <= otherJson.version && Date.now() - t0 < 15000) {
458 await window.DaimondSync.push(); // base=before → 409 → pull → retry → success.
459 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
460 after = ((await r.json()).version) | 0;
461 if (after <= otherJson.version) await new Promise(x => setTimeout(x, 250));
462 }
463 return { before, bumped: otherJson.version, after, changed };
464 });
465 check('the out-of-band write advanced the mailbox', conflict.bumped === conflict.before + 1,
466 'before=' + conflict.before + ' bumped=' + conflict.bumped);
467 check('and this device really had something of its own to send',
468 conflict.changed.moved === true, conflict.changed.why);
469 check('a stale push reconciles (409 → pull → retry) and advances past the conflict',
470 conflict.after > conflict.bumped, 'bumped=' + conflict.bumped + ' after=' + conflict.after);
471 // After reconciling, the mailbox is this device's real (decryptable) state again.
472 const reopened = await page.evaluate(async () => {
473 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
474 const j = await r.json();
475 try { const plain = await window.DaimondIdentity.unwrap(j.blob); JSON.parse(plain); return true; }
476 catch (e) { return false; }
477 });
478 check('the reconciled blob is this device’s own decryptable state', reopened);
479
480 // (3b) Workspace files travel too: write one, push, confirm it is sealed, then
481 // delete it locally and pull it back.
482 //
483 // The push is driven by the helper, not called once: a bare push() that finds
484 // a round in flight only reschedules, and the mailbox then held a parcel with
485 // no file in it at all. That read as "a deleted workspace file is restored by
486 // pull" failing -- while the sealed check above it stayed green, because a
487 // blob that never carried the file passes "the mark is absent" perfectly.
488 // Which is why the file's presence is now asserted too: a check that only
489 // looks for something's ABSENCE is answered by having nothing.
490 const FILEMARK = 'FILEMARK-' + '5566';
491 await page.evaluate(async (mark) => {
492 const mod = await import('../pkg/oxedyne_daimond.js');
493 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
494 await app.run_tool('file_write', JSON.stringify({ path: 'sync-note.txt', content: 'workspace ' + mark }));
495 }, FILEMARK);
496 await pushLanded(page, 'the workspace file');
497 const filePush = await page.evaluate(async () => {
498 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
499 const j = await r.json();
500 let plain = '';
501 try { plain = await window.DaimondIdentity.unwrap(j.blob || ''); } catch (e) { plain = ''; }
502 return { version: j.version, blob: j.blob || '', plain };
503 });
504 check('the workspace file really travelled — the mailbox holds it, sealed',
505 filePush.plain.includes(FILEMARK), 'parcel ' + filePush.plain.length + ' bytes');
506 check('a workspace file is sealed in the pushed blob (content absent from ciphertext)',
507 !filePush.blob.includes(FILEMARK));
508
509 const fileRestore = await page.evaluate(async () => {
510 const mod = await import('../pkg/oxedyne_daimond.js');
511 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
512 await app.run_tool('file_delete', JSON.stringify({ path: 'sync-note.txt' }));
513 const gone = await app.run_tool('file_read', JSON.stringify({ path: 'sync-note.txt' }));
514 localStorage.removeItem('daimond-sync-filebase'); // a fresh device has no baseline.
515 await window.DaimondSync.pull();
516 const back = await app.run_tool('file_read', JSON.stringify({ path: 'sync-note.txt' }));
517 return { gone: String(gone), back: String(back) };
518 });
519 check('a deleted workspace file is restored by pull',
520 fileRestore.back.includes(FILEMARK) && /error|not found|no such/i.test(fileRestore.gone),
521 'back=' + fileRestore.back.slice(0, 40));
522
523 // (3c) A deletion on another device propagates here (an unchanged local copy
524 // is removed; an edit would have beaten the delete).
525 // The agreeing push is driven by the helper for the same reason as (3b): the
526 // deletion below is only meaningful against a mailbox that HELD the file, and
527 // a bare push() that stood aside left there being nothing to delete.
528 await page.evaluate(async () => {
529 const mod = await import('../pkg/oxedyne_daimond.js');
530 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
531 await app.run_tool('file_write', JSON.stringify({ path: 'DELME.txt', content: 'delete me across devices' }));
532 localStorage.removeItem('daimond-sync-filebase');
533 });
534 await pushLanded(page, 'DELME.txt enters the baseline');
535 const delProp = await page.evaluate(async () => {
536 const mod = await import('../pkg/oxedyne_daimond.js');
537 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
538 const present = await app.run_tool('file_read', JSON.stringify({ path: 'DELME.txt' }));
539 // The OTHER device deletes DELME.txt and pushes the reduced state.
540 const state = await window.DaimondCore.collectSync();
541 delete state.files['DELME.txt'];
542 const blob = await window.DaimondIdentity.wrap(JSON.stringify(state));
543 const ver = window.DaimondSync.version();
544 await fetch('/api/sync', {
545 method: 'POST', credentials: 'same-origin',
546 headers: { 'content-type': 'application/json', 'x-daimond-api': '1' },
547 body: JSON.stringify({ base_version: ver, device: 'other', blob: blob }),
548 });
549 await window.DaimondSync.pull(); // honour the remote deletion.
550 const after = await app.run_tool('file_read', JSON.stringify({ path: 'DELME.txt' }));
551 return { present: String(present), after: String(after) };
552 });
553 check('a file deleted on another device is removed here',
554 delProp.present.includes('delete me') && /error|not found|no such/i.test(delProp.after),
555 'after=' + delProp.after.slice(0, 40));
556
557 // ── (4) Diamonds ───────────────────────────────────────────────────
558 // A Diamond is a directory in OPFS, so "it synced" means the whole directory
559 // arrived: the crystal and its versions, the metadata that carries the name
560 // and the tags, the append-only log, and the link sidecar. Each of those is a
561 // separate file, and a merge that carried only some of them would look like a
562 // working sync right up until the user asked the Graph pane a question.
563 const DMARK = 'DIAMOND-' + '3344';
564
565 /// Reach the real wasm on the page's own OPFS — the same store the app reads,
566 /// not a copy of it.
567 const wasm = (fn, arg) => page.evaluate(async ({ src, arg }) => {
568 const m = await import('/pkg/oxedyne_daimond.js');
569 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
570 return await (new Function('app', 'arg', `return (${src})(app, arg);`))(app, arg);
571 }, { src: fn.toString(), arg });
572
573 /// Make a Diamond the way a person does: the rail's button, the dialog, the
574 /// name, Create. Going through the UI is what records the model choice, which
575 /// is half of what this section is checking travels.
576 const newDiamond = async (name) => {
577 await page.evaluate(() => document.getElementById('new-diamond-btn').click());
578 await page.waitForSelector('.dlg-card', { timeout: 8000 });
579 const r = await page.evaluate((nm) => {
580 const card = [...document.querySelectorAll('.dlg-card')].find(c => c.getClientRects().length);
581 if (!card) return 'no dialog';
582 const inp = card.querySelector('input.dlg-input');
583 if (!inp) return 'no name field';
584 inp.value = nm;
585 inp.dispatchEvent(new Event('input', { bubbles: true }));
586 const btn = card.querySelector('.dlg-ok');
587 if (!btn) return 'no create button';
588 btn.click();
589 return 'ok';
590 }, name);
591 await page.waitForTimeout(1000);
592 return r;
593 };
594
595 /// Delete the named Diamond the way a person does — the cog, then Delete at
596 /// the foot of its dialog. THERE IS NO CONFIRM ANY MORE: since the trash,
597 /// deleting is reversible and asks nothing, and this is the call site that
598 /// has to put the Diamond into the state that travels.
599 const removeDiamond = async (name) => {
600 const found = await page.evaluate((nm) => {
601 const box = [...document.querySelectorAll('#diamond-list .diamond-box')]
602 .find(b => ((b.querySelector('.session-box-name') || {}).textContent || '').trim() === nm);
603 if (!box) return false;
604 const cog = box.querySelector('.tile-cog');
605 if (!cog) return false;
606 cog.click();
607 return true;
608 }, name);
609 if (!found) return 'not in the rail';
610 await page.waitForSelector('.tile-dlg-delete', { timeout: 8000 });
611 await page.evaluate(() => document.querySelector('.tile-dlg-delete').click());
612 await page.waitForTimeout(1500);
613 return 'ok';
614 };
615
616 /// Destroy a Diamond for good, out of the trash. THIS is now the call site
617 /// that writes the tombstone.
618 ///
619 /// By ID rather than by looking the name up in the panel: this file runs
620 /// sections that empty the store on purpose, and a lookup that went through
621 /// the panel would report "not in the trash" for a record that is perfectly
622 /// present — testing the fixture rather than the tombstone.
623 const purgeDiamond = async (id) => {
624 await page.evaluate((i) => window.DaimondCore.trashPurge(i), id);
625 await page.waitForTimeout(1200);
626 return 'ok';
627 };
628
629 /// A crystal as the store now holds one: `crystal.json`, conforming to the core
630 /// schema. These fixtures only ever needed a crystal they could tell apart, and
631 /// the schema's `title` is where a short distinguishing string goes.
632 ///
633 /// It has to be REAL JSON, not merely a different string. The parcel carries the
634 /// Diamond's directory file for file, so whatever this writes is what the app
635 /// parses on the other side — a markdown fixture would leave the merge under test
636 /// carrying something no reader downstream can open.
637 const crystalOf = (title) => JSON.stringify({ title: title });
638
639 /// What another device would have pushed: this device's own state with one
640 /// Diamond's entry rewritten to a different crystal at a different stamp,
641 /// sealed under the shared key, written over the current version and pulled
642 /// straight back — which is the merge under test.
643 ///
644 /// BOTH stamps move, in both the places a real export carries them: the entry
645 /// the merge compares, and the `meta.json` inside the packed directory. A
646 /// real device's copies always agree, so a fixture whose copies disagree
647 /// would be testing a state that cannot occur -- and a crystal edit on a real
648 /// device moves `updated` (it was worked on) AND `touched` (it changed).
649 const otherDeviceShifts = (id, crystal, delta) => page.evaluate(async (arg) => {
650 const state = await window.DaimondCore.collectSync();
651 const e = (state.diamonds || []).find(d => d.id === arg.id);
652 if (!e) return 'no entry for that Diamond';
653 const pack = JSON.parse(e.data);
654 pack.files['crystal.json'] = arg.crystal;
655 const meta = JSON.parse(pack.files['.daimond/meta.json']);
656 meta.updated = (e.updated || 0) + arg.delta;
657 meta.touched = (e.touched || e.updated || 0) + arg.delta;
658 pack.files['.daimond/meta.json'] = JSON.stringify(meta);
659 e.updated = meta.updated;
660 e.touched = meta.touched;
661 e.data = JSON.stringify(pack);
662 const blob = await window.DaimondIdentity.wrap(JSON.stringify(state));
663 await fetch('/api/sync', {
664 method: 'POST', credentials: 'same-origin',
665 headers: { 'content-type': 'application/json', 'x-daimond-api': '1' },
666 body: JSON.stringify({ base_version: window.DaimondSync.version(), device: 'other', blob: blob }),
667 });
668 await window.DaimondSync.pull();
669 return 'ok';
670 }, { id, crystal, delta });
671
672 /// One Diamond's whole visible state, read back from the store.
673 const readDiamond = (id) => wasm(async (app, id) => {
674 const list = JSON.parse(await app.list_diamonds()).find(d => d.id === id) || null;
675 if (!list) return null;
676 return {
677 name: list.name,
678 tags: list.tags || [],
679 version: list.crystal_version,
680 updated: list.updated,
681 crystal: await app.read_crystal_data(id),
682 log: JSON.parse(await app.log_read(id)).length,
683 links: JSON.parse(await app.links_touching('diamond:' + id)),
684 };
685 }, id);
686
687 // The fixture: two Diamonds, one of them tagged, steered (so its log has an
688 // edit record beside the create) and linked to the other.
689 await newDiamond('Sync-Alpha');
690 await newDiamond('Sync-Bravo');
691 const ids = await wasm(async (app, crystal) => {
692 const list = JSON.parse(await app.list_diamonds());
693 const find = (n) => (list.find(d => d.name === n) || {}).id || '';
694 const A = find('Sync-Alpha'), B = find('Sync-Bravo');
695 if (!A || !B) return { A, B };
696 await app.set_tags(A, JSON.stringify(['travel', 'sync']));
697 await app.write_crystal_data(A, crystal);
698 await app.add_link(A, 'diamond:' + A, 'diamond:' + B, 'part-of', 'bravo sits under alpha', 'user');
699 return { A, B };
700 }, crystalOf('Alpha ' + DMARK));
701 check('the fixture Diamonds were made through the rail', !!(ids.A && ids.B),
702 'alpha=' + (ids.A || '-') + ' bravo=' + (ids.B || '-'));
703
704 const before = await readDiamond(ids.A);
705 // What the fixture actually laid down, before any of it travels. Without this
706 // a fixture that quietly failed to write reads afterwards as a sync that
707 // quietly failed to carry, and the two want opposite fixes.
708 check('the fixture took locally before anything synced',
709 !!before && before.tags.join(',') === 'travel,sync' && before.crystal.includes(DMARK)
710 && before.log >= 2 && before.links.length >= 1,
711 before ? 'tags=[' + before.tags.join(',') + '] v' + before.version
712 + ' log=' + before.log + ' links=' + before.links.length : 'absent');
713 await pushLanded(page, 'the fixture Diamonds');
714
715 // A second device: it has never seen these Diamonds, holds no per-Diamond
716 // model choice, and has no version cursor. Wiping all three is what "another
717 // device" means here.
718 const arrived = await page.evaluate(async (id) => {
719 const m = await import('/pkg/oxedyne_daimond.js');
720 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
721 for (const d of JSON.parse(await app.list_diamonds())) await app.delete_diamond(d.id);
722 localStorage.removeItem('daimond-diamond-models');
723 localStorage.removeItem('daimond-sync-version');
724 const gone = JSON.parse(await app.list_diamonds()).length;
725 await window.DaimondSync.pull();
726 const models = JSON.parse(localStorage.getItem('daimond-diamond-models') || '{}');
727 return { gone: gone, model: models[id] || null };
728 }, ids.A);
729 check('the second device really started without them', arrived.gone === 0, 'held=' + arrived.gone);
730
731 const after = await readDiamond(ids.A);
732 check('a Diamond arrives on the second device at all', !!after,
733 after ? after.name : 'absent');
734 check('its name and tags arrive with it',
735 !!after && after.name === 'Sync-Alpha' && after.tags.join(',') === 'travel,sync',
736 after ? after.name + ' [' + after.tags.join(',') + ']' : 'absent');
737 check('its crystal arrives, at the version it was left at',
738 !!after && after.crystal.includes(DMARK) && after.version === before.version,
739 after ? 'v' + after.version + ' len=' + after.crystal.length : 'absent');
740 check('its log arrives whole (the create record and the edit)',
741 !!after && after.log === before.log && after.log >= 2,
742 after ? 'records=' + after.log : 'absent');
743 check('the link to the other Diamond arrives, and is found from this end',
744 !!after && after.links.some(l => l.other === 'diamond:' + ids.B && l.rel === 'part-of'),
745 after ? JSON.stringify(after.links.map(l => l.other + '/' + l.rel)) : 'absent');
746 check('the model the Diamond thinks with arrives with it',
747 !!(arrived.model && arrived.model.model), JSON.stringify(arrived.model));
748 // The other Diamond came too, or the rail below has nothing to delete.
749 const bravoBack = await readDiamond(ids.B);
750 check('the second Diamond arrives as well', !!bravoBack && bravoBack.name === 'Sync-Bravo',
751 bravoBack ? bravoBack.name : 'absent');
752
753 // (4b) Deleting through the rail must put the Diamond into a state the
754 // parcel CARRIES — and, since the trash, that state is not a tombstone.
755 //
756 // The two are different promises and both are asked here. Trashing has to
757 // travel WITH THE BYTES, or the other device holds a name it cannot restore;
758 // destroying has to travel as a tombstone, or the other device still holds
759 // the Diamond and hands it straight back on the next pull. The old shape of
760 // this check — delete in the rail, expect a tombstone — would now pass with
761 // a trash that quietly destroyed everything, which is the failure the panel
762 // exists to prevent.
763 // What the parcel carried BEFORE the delete, so "its bytes still travel" is a
764 // comparison rather than an absolute: earlier sections of this file empty the
765 // store on purpose, and a Diamond whose bytes were not in the parcel to begin
766 // with cannot be asked to still be in it.
767 const carriedBefore = await page.evaluate(async (id) => {
768 const state = await window.DaimondSync.parcel();
769 return (state.diamonds || []).some(d => d.id === id);
770 }, ids.B);
771 const removed = await removeDiamond('Sync-Bravo');
772 const parcel = await page.evaluate(async (id) => {
773 const state = await window.DaimondSync.parcel();
774 const rec = (state.trash && state.trash.items) ? state.trash.items[id] : null;
775 return {
776 v: state.v,
777 tombed: !!(state.diamondTombs && state.diamondTombs[id]),
778 trashed: !!(rec && rec.at > rec.back),
779 listed: (state.diamonds || []).some(d => d.id === id),
780 count: (state.diamonds || []).length,
781 };
782 }, ids.B);
783 check('a Diamond deleted in the rail travels as TRASHED, so the other device can restore it',
784 removed === 'ok' && parcel.trashed, removed + ', trashed=' + parcel.trashed);
785 check('and it is NOT tombstoned by that — deleting it is not destroying it',
786 !parcel.tombed, 'tombed=' + parcel.tombed);
787 check('while its bytes travel exactly as before, or there would be nothing to restore',
788 parcel.listed === carriedBefore,
789 'carried before=' + carriedBefore + ', after=' + parcel.listed + ' (of ' + parcel.count + ')');
790 check('the parcel declares itself v3', parcel.v === 3, 'v=' + parcel.v);
791
792 // And destroying it from the trash is what lays the tombstone.
793 const purged = await purgeDiamond(ids.B);
794 const afterPurge = await page.evaluate(async (id) => {
795 const state = await window.DaimondSync.parcel();
796 return {
797 tombed: !!(state.diamondTombs && state.diamondTombs[id]),
798 listed: (state.diamonds || []).some(d => d.id === id),
799 };
800 }, ids.B);
801 check('destroying it from the trash IS what tombstones it in the parcel',
802 purged === 'ok' && afterPurge.tombed, purged + ', tombed=' + afterPurge.tombed);
803 check('and it is no longer offered as a live Diamond',
804 !afterPurge.listed, 'listed=' + afterPurge.listed);
805
806 // (4c) A deletion made on ANOTHER device reaches this one, and does not come
807 // back on the cycle after — the failure a tombstone-less delete would show as
808 // a Diamond that reappears every time the app is opened.
809 const delCycle = await page.evaluate(async (id) => {
810 const post = async (state) => {
811 const blob = await window.DaimondIdentity.wrap(JSON.stringify(state));
812 await fetch('/api/sync', {
813 method: 'POST', credentials: 'same-origin',
814 headers: { 'content-type': 'application/json', 'x-daimond-api': '1' },
815 body: JSON.stringify({ base_version: window.DaimondSync.version(), device: 'other', blob: blob }),
816 });
817 await window.DaimondSync.pull();
818 };
819 const m = await import('/pkg/oxedyne_daimond.js');
820 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
821 const here = async () => JSON.parse(await app.list_diamonds()).some(d => d.id === id);
822 const held = await here();
823 // The other device deletes it: gone from its Diamonds, present in its tombs.
824 const state = await window.DaimondCore.collectSync();
825 state.diamonds = (state.diamonds || []).filter(d => d.id !== id);
826 state.diamondTombs = state.diamondTombs || {};
827 state.diamondTombs[id] = Date.now();
828 await post(state);
829 // SETTLE, DO NOT SNAPSHOT. `pull()` resolving is not the same instant as the
830 // store having finished with the directory it just removed: two
831 // `list_diamonds()` calls a few microseconds apart were observed
832 // disagreeing, the first still carrying the Diamond and the second empty.
833 // Reading once, in the same tick, therefore reported a deletion that HAD
834 // happened as one that had not -- and it read as sync losing a tombstone,
835 // which is about as alarming as this suite gets.
836 //
837 // Bounded, so a deletion that genuinely never lands still fails rather than
838 // hanging: half a second is already hundreds of times what the settle costs.
839 const gone = async () => {
840 for (let i = 0; i < 25; i++) {
841 if (!(await here())) return false;
842 await new Promise(r => setTimeout(r, 20));
843 }
844 return true;
845 };
846 const afterPull = await gone();
847 // A full cycle later — this device pushes its own view, then pulls again.
848 await window.DaimondSync.push();
849 await window.DaimondSync.pull();
850 const afterCycle = await gone(); // same reason: settle, do not snapshot
851 return { held, afterPull, afterCycle };
852 }, ids.A);
853 check('a Diamond deleted on another device is deleted here',
854 delCycle.held === true && delCycle.afterPull === false,
855 'held=' + delCycle.held + ' after=' + delCycle.afterPull);
856 check('and does not resurrect on the next cycle', delCycle.afterCycle === false,
857 'after a push+pull, present=' + delCycle.afterCycle);
858
859 // (4d) Freshest wins, wholesale, and the comparison is STRICT: an equal stamp
860 // keeps what is here, so an unchanged Diamond is not rewritten on every pull.
861 await newDiamond('Sync-Charlie');
862 const cid = await wasm(async (app, crystal) => {
863 const list = JSON.parse(await app.list_diamonds());
864 const id = (list.find(d => d.name === 'Sync-Charlie') || {}).id || '';
865 if (id) await app.write_crystal_data(id, crystal);
866 return id;
867 }, crystalOf('HERE-' + DMARK));
868 await pushLanded(page, 'Sync-Charlie');
869
870 /// Which of the four copies below is on this device, named by the one field they
871 /// differ by. PARSED rather than compared as text: a copy that arrives as
872 /// anything but the JSON a crystal now is fails here, where it reads as a merge
873 /// that carried the wrong thing, rather than passing as bytes that merely match.
874 const crystalNow = async () => {
875 const text = await wasm((app, id) => app.read_crystal_data(id), cid);
876 try { return JSON.parse(text).title; }
877 catch (e) { return 'not JSON: ' + String(text).slice(0, 30); }
878 };
879
880 const shifted = await otherDeviceShifts(cid, crystalOf('OLDER-COPY'), -60000);
881 const keptOlder = await crystalNow();
882 await otherDeviceShifts(cid, crystalOf('EQUAL-COPY'), 0);
883 const keptEqual = await crystalNow();
884 await otherDeviceShifts(cid, crystalOf('NEWER-COPY'), 60000);
885 const tookNewer = await crystalNow();
886 check('the freshest-wins fixture reached the other device', shifted === 'ok', shifted);
887 check('an older copy from another device does not overwrite this one',
888 keptOlder === 'HERE-' + DMARK, keptOlder);
889 check('an equally-stamped copy keeps what is here (the comparison is strict)',
890 keptEqual === 'HERE-' + DMARK, keptEqual);
891 check('a fresher copy from another device replaces this one',
892 tookNewer === 'NEWER-COPY', tookNewer);
893
894 // (4e) A device that predates all of this sends a v1 parcel: no `diamonds`,
895 // no `diamondTombs`. It must apply as it always did, and touch nothing.
896 const v1 = await page.evaluate(async (id) => {
897 const state = await window.DaimondCore.collectSync();
898 delete state.diamonds;
899 delete state.diamondTombs;
900 state.v = 1;
901 const blob = await window.DaimondIdentity.wrap(JSON.stringify(state));
902 await fetch('/api/sync', {
903 method: 'POST', credentials: 'same-origin',
904 headers: { 'content-type': 'application/json', 'x-daimond-api': '1' },
905 body: JSON.stringify({ base_version: window.DaimondSync.version(), device: 'old-device', blob: blob }),
906 });
907 let threw = '';
908 try { await window.DaimondCore.applySync(JSON.parse(await window.DaimondIdentity.unwrap(blob))); }
909 catch (e) { threw = String(e && e.message || e); }
910 const m = await import('/pkg/oxedyne_daimond.js');
911 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
912 return { threw, still: JSON.parse(await app.list_diamonds()).some(d => d.id === id) };
913 }, cid);
914 check('a v1 parcel (no diamonds section) applies without error',
915 v1.threw === '', v1.threw);
916 check('and leaves this device’s Diamonds exactly where they were', v1.still === true);
917
918 // ── (4f) Tags across devices: the change that used to vanish ───────
919 // A tag is content, but it is not WORK, so tagging deliberately leaves
920 // `updated` alone -- the rail is ordered by it, and filing a Diamond must not
921 // shuffle it to the top. The merge compared `updated`, so a tag-only change
922 // was invisible to it: it never travelled, and the moment the other device
923 // did anything stamped (a rename, a crystal edit) its untagged copy became
924 // strictly fresher and REPLACED the tagged one wholesale. A real user lost
925 // real tags to exactly that.
926 //
927 // Two devices are simulated on the one browser by swapping the store under
928 // it: `export_diamond` is a device's disk, `import_diamond` lays it back down
929 // verbatim (stamps included), and the merge that runs on each side is the
930 // app's own `applySync`, not a copy of it written here.
931 const TAG = 'keepsake';
932
933 /// What this device holds, as import-ready packs: its disk.
934 const diskNow = () => wasm(async (app) => {
935 const out = [];
936 for (const d of JSON.parse(await app.list_diamonds())) out.push(await app.export_diamond(d.id));
937 return out;
938 });
939
940 /// Make the browser BE the device that disk came from.
941 const becomeDevice = (disk) => wasm(async (app, disk) => {
942 for (const d of JSON.parse(await app.list_diamonds())) await app.delete_diamond(d.id);
943 for (const pack of disk) await app.import_diamond(pack);
944 return JSON.parse(await app.list_diamonds()).length;
945 }, disk);
946
947 /// What this device would push, and what a pull does with what arrives.
948 const parcelNow = () => page.evaluate(() => window.DaimondCore.collectSync());
949 const pullParcel = (p) => page.evaluate(async (p) => { await window.DaimondCore.applySync(p); }, p);
950
951 /// One Diamond's row as the rail reads it.
952 const rowOf = (id) => wasm(async (app, id) =>
953 JSON.parse(await app.list_diamonds()).find(d => d.id === id) || null, id);
954 const orderNow = () => wasm(async (app) =>
955 JSON.parse(await app.list_diamonds()).map(d => d.id).join(','));
956 const entryOf = (p, id) => (p.diamonds || []).find(d => d.id === id) || {};
957 const tagsOn = (row) => ((row && row.tags) || []).join(',');
958
959 // A clean two-device world: one Diamond to tag, one beside it so the rail has
960 // an order to disturb.
961 const two = await wasm(async (app, crystal) => {
962 for (const d of JSON.parse(await app.list_diamonds())) await app.delete_diamond(d.id);
963 const a = await app.create_diamond('Tag-Travel');
964 const b = await app.create_diamond('Tag-Neighbour');
965 await app.write_crystal_data(a, crystal);
966 return { a, b };
967 }, crystalOf('shared ground'));
968 const disk0 = await diskNow(); // what BOTH devices last agreed on
969 check('the tag fixture starts from one agreed copy on both devices',
970 !!(two.a && two.b) && disk0.length === 2, 'packs=' + disk0.length);
971
972 // (1) A tag-only change reaches the other device.
973 await becomeDevice(disk0);
974 const orderBefore = await orderNow();
975 const rowBefore = await rowOf(two.a);
976 await wasm((app, arg) => app.set_tags(arg.id, JSON.stringify([arg.tag])), { id: two.a, tag: TAG });
977 const rowTagged = await rowOf(two.a);
978 check('tagging leaves `updated` alone, so the rail keeps its order',
979 !!rowTagged && rowTagged.updated === rowBefore.updated && (await orderNow()) === orderBefore,
980 'updated ' + rowBefore.updated + ' -> ' + (rowTagged && rowTagged.updated));
981 const parcel1 = await parcelNow();
982 const disk1 = await diskNow(); // device 1: tagged, nothing else touched
983
984 await becomeDevice(disk0); // device 2: the copy it last saw
985 const beforePull2 = await rowOf(two.a);
986 await pullParcel(parcel1);
987 const gotTag = await rowOf(two.a);
988 check('a tag-only change reaches the other device',
989 tagsOn(gotTag) === TAG, 'tags=[' + tagsOn(gotTag) + ']');
990 check('and arriving does not reorder the rail it arrived on',
991 !!gotTag && gotTag.updated === beforePull2.updated && (await orderNow()) === orderBefore,
992 'updated ' + beforePull2.updated + ' -> ' + (gotTag && gotTag.updated));
993
994 // (2) The other device then works on the SAME Diamond. Its copy is fresher,
995 // so it wins wholesale -- and because it had already received the tags, the
996 // tags come back with the rename rather than being wiped by it.
997 await wasm((app, id) => app.rename_diamond(id, 'Renamed-On-Two'), two.a);
998 const parcel2 = await parcelNow();
999 await becomeDevice(disk1); // device 1 as it was: tagged, not renamed
1000 await pullParcel(parcel2);
1001 const afterWork = await rowOf(two.a);
1002 check('the other device working on that Diamond does not wipe the tags',
1003 !!afterWork && afterWork.name === 'Renamed-On-Two' && tagsOn(afterWork) === TAG,
1004 afterWork ? afterWork.name + ' [' + tagsOn(afterWork) + ']' : 'absent');
1005 check('an imported copy keeps the stamp it was sent with (an import is not working)',
1006 !!afterWork && afterWork.updated === (entryOf(parcel2, two.a).updated || 0),
1007 'here=' + (afterWork && afterWork.updated) + ' sent=' + entryOf(parcel2, two.a).updated);
1008
1009 // (3) Two stores that have ALREADY diverged -- the state the user is in.
1010 // One side tagged the Diamond under a build that wrote no second stamp, so
1011 // both copies are stamped identically and neither is fresher than the other.
1012 const diverged = await page.evaluate(({ disk, id, tag }) => disk.map((p) => {
1013 const pack = JSON.parse(p);
1014 if (pack.id !== id) return p;
1015 const meta = JSON.parse(pack.files['.daimond/meta.json']);
1016 meta.tags = [tag];
1017 delete meta.touched; // an old build: one stamp, and it did not move
1018 pack.files['.daimond/meta.json'] = JSON.stringify(meta);
1019 return JSON.stringify(pack);
1020 }), { disk: disk0, id: two.a, tag: TAG });
1021
1022 await becomeDevice(diverged); // device 1: tagged, stamps as they were
1023 const pD1 = await parcelNow();
1024 await becomeDevice(disk0); // device 2: untagged, the same stamps
1025 const beforeConv = await rowOf(two.a);
1026 await pullParcel(pD1);
1027 const conv2 = await rowOf(two.a);
1028 check('two stores that already diverged converge: the untagged side gains the tag',
1029 tagsOn(conv2) === TAG, 'tags=[' + tagsOn(conv2) + ']');
1030 check('and converging still does not reorder the rail',
1031 !!conv2 && conv2.updated === beforeConv.updated,
1032 'updated ' + beforeConv.updated + ' -> ' + (conv2 && conv2.updated));
1033 const pD2 = await parcelNow();
1034 await becomeDevice(diverged); // device 1 once more
1035 await pullParcel(pD2);
1036 const conv1 = await rowOf(two.a);
1037 check('and the side that had the tag keeps it when the union comes back',
1038 tagsOn(conv1) === TAG, 'tags=[' + tagsOn(conv1) + ']');
1039
1040 // (4) A parcel from a device that predates the second stamp still merges by
1041 // the only stamp it carries.
1042 const oldStyle = await page.evaluate(({ p, id }) => {
1043 const e = (p.diamonds || []).find(d => d.id === id);
1044 if (!e) return p;
1045 const pack = JSON.parse(e.data);
1046 const meta = JSON.parse(pack.files['.daimond/meta.json']);
1047 meta.name = 'Named-By-An-Old-Build';
1048 meta.updated = (e.updated || 0) + 60000;
1049 delete meta.touched;
1050 pack.files['.daimond/meta.json'] = JSON.stringify(meta);
1051 e.data = JSON.stringify(pack);
1052 e.updated = meta.updated;
1053 delete e.touched;
1054 return p;
1055 }, { p: await parcelNow(), id: two.a });
1056 await pullParcel(oldStyle);
1057 const oldWon = await rowOf(two.a);
1058 check('a parcel with no second stamp still merges on the one it has',
1059 !!oldWon && oldWon.name === 'Named-By-An-Old-Build', oldWon ? oldWon.name : 'absent');
1060
1061 // ── (4g) Links across devices: the same flaw, one file over ───────
1062 // A Diamond's links live in a sidecar inside its own directory, so they rode
1063 // the wholesale copy and nothing else -- and before `touched`, asserting or
1064 // removing a link stamped nothing at all, exactly as tagging did not. Two
1065 // stores could therefore hold different links at identical stamps for ever,
1066 // and the first thing either of them stamped replaced the other's links
1067 // wholesale. Tags were repaired by unioning them at equal stamps; this is
1068 // the same repair one file over, where a link's id is what says whether the
1069 // two copies mean the same link or two different ones.
1070 const EAST = 'link-east-1', WEST = 'link-west-1';
1071
1072 /// One stored sidecar line, as a hand or an older build would leave it.
1073 const linkLine = (id, from, to, rel) => JSON.stringify({
1074 id: id, ts: 1700000000000, from: from, to: to, rel: rel, note: '', by: 'user',
1075 }) + '\n';
1076
1077 /// Every link one Diamond's own sidecar holds, as `id/rel`, sorted. The id
1078 /// is in there because a union that re-created the links it took would look
1079 /// identical by relation alone, and would then duplicate on every round.
1080 const linksOf = (id) => wasm(async (app, id) =>
1081 JSON.parse(await app.all_links()).filter(l => l.owner === id)
1082 .map(l => l.id + '/' + l.rel).sort().join(','), id);
1083
1084 /// A disk with one Diamond's sidecar replaced and its stamps left exactly as
1085 /// they were -- which is what a link written by a build that stamped nothing
1086 /// looks like from the outside.
1087 const withSidecar = (disk, id, text) => page.evaluate(({ disk, id, text }) => disk.map((p) => {
1088 const pack = JSON.parse(p);
1089 if (pack.id !== id) return p;
1090 pack.files['.daimond/links.jsonl'] = text;
1091 return JSON.stringify(pack);
1092 }), { disk: disk, id: id, text: text });
1093
1094 const three = await wasm(async (app, crystal) => {
1095 for (const d of JSON.parse(await app.list_diamonds())) await app.delete_diamond(d.id);
1096 const a = await app.create_diamond('Link-Travel');
1097 const b = await app.create_diamond('Link-East');
1098 const c = await app.create_diamond('Link-West');
1099 await app.write_crystal_data(a, crystal);
1100 return { a: a, b: b, c: c };
1101 }, crystalOf('shared ground'));
1102 const diskL = await diskNow(); // what BOTH devices last agreed on
1103 const BOTH = [EAST + '/east', WEST + '/west'].sort().join(',');
1104 const dev1 = await withSidecar(diskL, three.a,
1105 linkLine(EAST, 'diamond:' + three.a, 'diamond:' + three.b, 'east'));
1106 const dev2 = await withSidecar(diskL, three.a,
1107 linkLine(WEST, 'diamond:' + three.a, 'diamond:' + three.c, 'west'));
1108
1109 await becomeDevice(dev1);
1110 const pL1 = await parcelNow();
1111 await becomeDevice(dev2);
1112 const beforeUnion = await rowOf(three.a);
1113 const linksBefore = await linksOf(three.a);
1114 check('the link fixture is two stores that disagree at one identical stamp',
1115 linksBefore === WEST + '/west'
1116 && entryOf(pL1, three.a).touched === beforeUnion.touched,
1117 'here=[' + linksBefore + '] stamps ' + entryOf(pL1, three.a).touched
1118 + ' / ' + beforeUnion.touched);
1119
1120 await pullParcel(pL1);
1121 const unioned2 = await linksOf(three.a);
1122 const rowUnion = await rowOf(three.a);
1123 check('two stores that already diverged converge: both links, both ids kept',
1124 unioned2 === BOTH, '[' + unioned2 + ']');
1125 check('and unioning links does not reorder the rail either',
1126 !!rowUnion && rowUnion.updated === beforeUnion.updated,
1127 'updated ' + beforeUnion.updated + ' -> ' + (rowUnion && rowUnion.updated));
1128 check('writing the union stamps this side, so it can travel back',
1129 !!rowUnion && rowUnion.touched > beforeUnion.touched,
1130 'touched ' + beforeUnion.touched + ' -> ' + (rowUnion && rowUnion.touched));
1131
1132 const pL2 = await parcelNow(); // device 2, unioned and stamped
1133 const diskBoth = await diskNow();
1134 await becomeDevice(dev1); // device 1 as it was: one link
1135 await pullParcel(pL2);
1136 const unioned1 = await linksOf(three.a);
1137 const rowBack = await rowOf(three.a);
1138 check('the union travels back, and the fresher side is taken wholesale',
1139 unioned1 === BOTH, '[' + unioned1 + ']');
1140 check('the import lays that stamp down verbatim, so the union cannot bounce',
1141 !!rowBack && rowBack.touched === entryOf(pL2, three.a).touched,
1142 'here=' + (rowBack && rowBack.touched) + ' sent=' + entryOf(pL2, three.a).touched);
1143
1144 // Round three: the two sides now hold the same links at the same stamp, and
1145 // a union with nothing to add must write nothing at all -- otherwise each
1146 // side would stamp itself fresher than the other for ever.
1147 const pL3 = await parcelNow();
1148 await becomeDevice(diskBoth);
1149 const quietBefore = await rowOf(three.a);
1150 await pullParcel(pL3);
1151 const quietAfter = await rowOf(three.a);
1152 check('once the two agree, the union writes nothing and moves no stamp',
1153 (await linksOf(three.a)) === BOTH
1154 && !!quietAfter && quietAfter.touched === quietBefore.touched,
1155 '[' + (await linksOf(three.a)) + '] touched ' + quietBefore.touched
1156 + ' -> ' + (quietAfter && quietAfter.touched));
1157
1158 // A link removed since the fix DOES stamp the Diamond, so the removal is
1159 // strictly fresher and is taken wholesale. The union only ever sees copies
1160 // that are equally fresh, so it cannot be what puts a deleted link back.
1161 await becomeDevice(diskBoth);
1162 await wasm((app, arg) => app.remove_link(arg.id, arg.link), { id: three.a, link: EAST });
1163 const rowDel = await rowOf(three.a);
1164 const pDel = await parcelNow();
1165 const diskDel = await diskNow();
1166 check('removing a link stamps the Diamond, so the removal can travel at all',
1167 (await linksOf(three.a)) === WEST + '/west'
1168 && !!rowDel && rowDel.touched > quietBefore.touched,
1169 '[' + (await linksOf(three.a)) + '] touched ' + quietBefore.touched
1170 + ' -> ' + (rowDel && rowDel.touched));
1171
1172 await becomeDevice(diskBoth); // the other device: still holds both
1173 const pStale = await parcelNow(); // and would hand the deleted link back
1174 await pullParcel(pDel);
1175 const pDel2 = await parcelNow(); // that device, having taken the deletion
1176 check('a link deleted on another device goes here too',
1177 (await linksOf(three.a)) === WEST + '/west', '[' + (await linksOf(three.a)) + ']');
1178
1179 await becomeDevice(diskDel); // the device that did the deleting
1180 const staleRow = await rowOf(three.a);
1181 await pullParcel(pStale);
1182 // The outcome AND the mechanism: the copy that still holds the link is
1183 // strictly older, so it never reaches the union at all. That is the whole of
1184 // why unioning links cannot undo a deletion made since the stamp existed.
1185 check('and a stale copy that still holds it does not put it back',
1186 (await linksOf(three.a)) === WEST + '/west'
1187 && entryOf(pStale, three.a).touched < staleRow.touched,
1188 '[' + (await linksOf(three.a)) + '] stale ' + entryOf(pStale, three.a).touched
1189 + ' < here ' + staleRow.touched);
1190 const beforeAgreed = await rowOf(three.a);
1191 await pullParcel(pDel2);
1192 const afterAgreed = await rowOf(three.a);
1193 check('nor does an equal-stamped copy that has already taken the deletion',
1194 (await linksOf(three.a)) === WEST + '/west'
1195 && !!afterAgreed && afterAgreed.touched === beforeAgreed.touched,
1196 '[' + (await linksOf(three.a)) + '] touched ' + beforeAgreed.touched
1197 + ' -> ' + (afterAgreed && afterAgreed.touched));
1198
1199 // Every merge above left the store changed, so the engine has a push
1200 // scheduled. Let it land: a push still in flight when the next section stubs
1201 // the gateway is only rescheduled, and that section would then measure a chip
1202 // that says "Syncing…" for a reason that has nothing to do with it.
1203 await pushLanded(page, 'the merges of (4f)/(4g)');
1204 await page.waitForTimeout(500);
1205
1206 // ── (6) A parcel the gateway refuses as too large is VISIBLE ───────
1207 // A 413 used to log to the console and stop, so sync simply stopped working
1208 // and nothing on the screen said so. The refusal is stubbed rather than
1209 // provoked: the real ceiling is 32 MB, and building that in the browser to
1210 // test a status chip would cost more than the behaviour it proves.
1211 const tooBig = await page.evaluate(async () => {
1212 const chip = () => {
1213 const c = document.getElementById('sync-chip');
1214 if (!c) return null;
1215 return {
1216 state: c.dataset.state || '',
1217 text: (c.querySelector('.stext') || {}).textContent || '',
1218 title: c.title || '',
1219 shown: c.style.display !== 'none',
1220 };
1221 };
1222 // A genuine local change, or the push is skipped as a no-op and nothing
1223 // reaches the (stubbed) gateway at all. Made BEFORE the stub goes on, so
1224 // nothing the change does can be answered by it.
1225 const changed = await window.__bump('too-big-1');
1226 const real = window.fetch;
1227 // Counted, so "the refusal was answered" is a measurement rather than an
1228 // assumption: the push below is driven until a parcel really leaves.
1229 let sent = 0;
1230 window.fetch = function (u, o) {
1231 const url = String((u && u.url) || u || '');
1232 if (url.indexOf('/api/sync') !== -1 && o && o.method === 'POST') {
1233 sent++;
1234 return Promise.resolve(new Response(
1235 JSON.stringify({ ok: false, error: 'Sync blob exceeds the size limit' }),
1236 { status: 413, headers: { 'content-type': 'application/json' } }));
1237 }
1238 return real.apply(this, arguments);
1239 };
1240 const refused = await window.__pushSent(() => sent, 'too-big-1');
1241 const stalled = chip();
1242 const api = window.DaimondSync.state ? window.DaimondSync.state() : null;
1243 window.fetch = real;
1244 // And a later successful push clears it — a stall that outlives its cause
1245 // is the same lie the other way round.
1246 //
1247 // Driven until the engine says the stall is gone, for the third time in
1248 // this file: a bare push() here found a round in flight, rescheduled, and
1249 // the chip was read still holding the 413 it had never been given a
1250 // chance to clear. Waiting on the STALL, not on a timer, so a stall that
1251 // genuinely never clears still fails.
1252 const cleared = await window.__bump('too-big-2');
1253 const tClear = Date.now();
1254 while (window.DaimondSync.state().stalled && Date.now() - tClear < 15000) {
1255 await window.DaimondSync.push();
1256 if (window.DaimondSync.state().stalled) await new Promise(r => setTimeout(r, 250));
1257 }
1258 const after = chip();
1259 return { changed, cleared, stalled, api, after, refused };
1260 });
1261 check('both local changes moved the parcel, so the 413 arm was reached at all',
1262 tooBig.changed.moved === true && tooBig.cleared.moved === true,
1263 [tooBig.changed.why, tooBig.cleared.why].filter(Boolean).join('; '));
1264 check('and a parcel really reached the refusal', tooBig.refused.sent === true,
1265 JSON.stringify(tooBig.refused));
1266 check('a 413 push shows a stalled state on the sync chip, held (not a flash)',
1267 !!(tooBig.stalled && tooBig.stalled.shown && tooBig.stalled.state === 'stalled'),
1268 JSON.stringify(tooBig.stalled));
1269 check('and says on hover that the parcel is too large, naming what makes it large',
1270 !!(tooBig.stalled && /too large/i.test(tooBig.stalled.title)
1271 && /(Diamond|file)/i.test(tooBig.stalled.title)),
1272 (tooBig.stalled && tooBig.stalled.title || '').slice(0, 120));
1273 check('the engine reports the stall through its own surface too',
1274 !!(tooBig.api && tooBig.api.stalled === true), JSON.stringify(tooBig.api));
1275 check('a later successful push clears the stall',
1276 !!(tooBig.after && tooBig.after.state === 'synced'), JSON.stringify(tooBig.after));
1277 const lastLine = await page.evaluate(() => {
1278 const c = document.getElementById('sync-chip');
1279 return { title: c ? c.title : '', at: (window.DaimondSync.state ? DaimondSync.state().lastSyncedAt : 0) };
1280 });
1281 check('a successful sync records when it happened, and the chip can say so',
1282 lastLine.at > 0 && /synced/i.test(lastLine.title),
1283 JSON.stringify(lastLine).slice(0, 120));
1284
1285 // ── (7) A parked tab converges on focus, without a reload ──────────
1286 // Sync fired on idle, on the tab going away, and on auth. A device left open
1287 // on the desk therefore never caught up until it was reloaded. Coming back to
1288 // a window is exactly the moment its owner expects to see the other device's
1289 // work.
1290 //
1291 // Measured with the wake channel SHUT. The gateway taps every other device of
1292 // the account when the mailbox moves, so with the channel open this device
1293 // converges on its own and the pull it makes is counted here as a focus pull:
1294 // the storm reads as noise and the trigger under test is never the thing that
1295 // answered. Section 13 turns the channel back on and tests it in its own
1296 // right, which is where that behaviour belongs.
1297 await page.evaluate(() => window.DaimondSync.wakeVia('off'));
1298 // And from a quiet engine: a push still armed when the focus fires holds
1299 // `inFlight`, and `focusPull` stands aside for a round already under way.
1300 // Longer than the push debounce, so anything armed has fired and finished.
1301 await page.waitForTimeout(4000);
1302 const focus = await page.evaluate(async () => {
1303 const mark = 'FOCUSMARK-' + Date.now();
1304 const state = await window.DaimondCore.collectSync();
1305 state.chats = (state.chats || []).concat([{
1306 id: 'focus-' + Date.now(), title: mark, updatedAt: Date.now(),
1307 messages: [{ role: 'user', content: mark, mid: 'fm-' + Date.now(), ts: Date.now() }],
1308 }]);
1309 const blob = await window.DaimondIdentity.wrap(JSON.stringify(state));
1310 const r = await fetch('/api/sync', {
1311 method: 'POST', credentials: 'same-origin',
1312 headers: { 'content-type': 'application/json', 'x-daimond-api': '1' },
1313 body: JSON.stringify({ base_version: window.DaimondSync.version(), device: 'other', blob: blob }),
1314 });
1315 const posted = r.status;
1316 // What the chat store holds, not what localStorage holds: the transcripts
1317 // are in IndexedDB, so the old read saw an empty string and "this device
1318 // did not already hold it" passed for a device that held nothing at all.
1319 const holds = () => JSON.stringify(window.DaimondCore.chatStore().stored()).indexOf(mark) !== -1;
1320 const before = holds();
1321 // Count the pulls, so a focus STORM is proved to coalesce into one. Every
1322 // pull is a bare `GET /api/sync`; the presence path (`?presence=1`) is a
1323 // different door and is not one of these, but two content pulls still are,
1324 // so the burst below is FOCUS EVENTS ONLY.
1325 //
1326 // A `visibilitychange` is deliberately NOT dispatched into the count. Since
1327 // devices began listening for dispatched errands (daimond.js
1328 // `peerCollectOnReturn`, wired to `visibilitychange`), returning to a visible
1329 // tab fires a SECOND, separate content pull — the peer-return recovery pull —
1330 // alongside the focus one. That pull is intended and bounded (one per return,
1331 // guarded by `_recovering`); it is simply not the focus-coalescing property
1332 // this check measures, and counting it here made a green throttle read as two
1333 // pulls. The visibility path shares the very same `scheduleFocusPull` throttle
1334 // as focus (js/sync.js), so five focus events exercise the coalescing in full:
1335 // a broken throttle pulls five times here, which is exactly what must go red.
1336 const real = window.fetch;
1337 let gets = 0;
1338 window.fetch = function (u, o) {
1339 const url = String((u && u.url) || u || '');
1340 if (url.indexOf('/api/sync') !== -1 && url.indexOf('presence') === -1
1341 && (!o || !o.method || o.method === 'GET')) gets++;
1342 return real.apply(this, arguments);
1343 };
1344 for (let i = 0; i < 5; i++) window.dispatchEvent(new Event('focus'));
1345 await new Promise(res => setTimeout(res, 900));
1346 const storm = gets;
1347 // And a focus arriving straight back is refused by the throttle. STRAIGHT
1348 // BACK means straight back: the throttle is three seconds rather than thirty
1349 // since 2026-08-28 (see FOCUS_PULL_MIN_MS in js/sync.js, and why it was
1350 // lowered), so this probe used to sit two and a half seconds inside a
1351 // thirty-second window and now sits one second inside a three-second one.
1352 // A probe calibrated to the old figure would be measuring the throttle
1353 // expiring rather than the throttle holding, and would pass or fail on how
1354 // busy the box was.
1355 window.dispatchEvent(new Event('focus'));
1356 await new Promise(res => setTimeout(res, 900));
1357 const again = gets;
1358 // Only now let the storm's own pull finish landing: what it FETCHED is the
1359 // question above, and what it MERGED is the question here.
1360 await new Promise(res => setTimeout(res, 2500));
1361 const after = holds();
1362 window.fetch = real;
1363 return { posted, before, after, storm, again };
1364 });
1365 await page.evaluate(() => window.DaimondSync.wakeVia('ws'));
1366 check('the other device’s push landed on the mailbox', focus.posted === 200, 'HTTP ' + focus.posted);
1367 check('this device did not already hold it', focus.before === false);
1368 check('focus pulls the other device’s work in, with no reload', focus.after === true);
1369 check('a focus storm coalesces into ONE pull', focus.storm === 1, 'pulls=' + focus.storm);
1370 check('a focus straight afterwards is throttled, not another pull',
1371 focus.again === focus.storm, 'pulls=' + focus.again);
1372
1373 // ── (8) Models and API keys travel with the work ───────────────────
1374 // A second device that holds the account holds the same identity, so a key
1375 // sealed under it opens on both. That is what makes carrying keys safe, and
1376 // it is the only reason this is allowed at all: what travels is `keyEnc`,
1377 // never a readable key.
1378 const M = await page.evaluate(async () => {
1379 const S = window.DaimondModels;
1380 const r = { api: typeof S.exportSync === 'function' && typeof S.applySync === 'function' };
1381 if (!r.api) return r;
1382 const now = Date.now();
1383 const PLAIN = 'LOCAL-KEY-' + '4242';
1384 const def0 = S.getDefault(); // put the app's own default back afterwards
1385
1386 S.addProvider('groq', { name: 'Groq', url: 'https://api.groq.com/openai/v1/chat/completions' });
1387 await S.setKey('groq', PLAIN);
1388
1389 const e1 = JSON.stringify(S.exportSync());
1390 const e2 = JSON.stringify(S.exportSync());
1391 r.deterministic = e1 === e2;
1392 r.carriesKeyEnc = !!(S.exportSync().providers.groq || {}).keyEnc;
1393 r.plaintextAbsent = e1.indexOf(PLAIN) === -1;
1394 r.noMintedRow = !Object.prototype.hasOwnProperty.call(S.exportSync().providers, 'credits');
1395
1396 // A provider only the other device has arrives; one only this device has
1397 // survives. (The union is what makes a second device usable at all.)
1398 await S.applySync({
1399 v: 2, def: { provider: '', model: '' }, defAt: 0,
1400 providers: { together: {
1401 name: 'Together AI', url: 'https://api.together.xyz/v1/chat/completions',
1402 models: ['m-b', 'm-a'], fetched: now, touched: now, keyEnc: 'REMOTE-SEALED',
1403 } },
1404 });
1405 let ex = S.exportSync();
1406 r.remoteArrived = !!ex.providers.together && ex.providers.together.keyEnc === 'REMOTE-SEALED';
1407 r.localSurvived = !!ex.providers.groq;
1408 r.modelsSorted = !!ex.providers.together
1409 && ex.providers.together.models.join(',') === 'm-a,m-b';
1410
1411 // An OLDER remote copy must not clobber a newer local one.
1412 const stamp = ex.providers.groq.touched;
1413 await S.applySync({ v: 2, providers: { groq: {
1414 name: 'Stale', url: 'https://stale.example/v1/chat/completions',
1415 models: [], fetched: 0, touched: stamp - 60000, keyEnc: 'STALE-SEALED',
1416 } } });
1417 let g = S.exportSync().providers.groq;
1418 r.olderIgnored = g.keyEnc !== 'STALE-SEALED' && g.url.indexOf('groq.com') !== -1;
1419
1420 // A fresher one wins, wholesale.
1421 await S.applySync({ v: 2, providers: { groq: {
1422 name: 'Groq', url: 'https://api.groq.com/openai/v1/chat/completions',
1423 models: ['fresh-1'], fetched: now + 60000, touched: stamp + 60000, keyEnc: 'FRESH-SEALED',
1424 } } });
1425 g = S.exportSync().providers.groq;
1426 r.newerWon = g.keyEnc === 'FRESH-SEALED' && g.models.join(',') === 'fresh-1';
1427
1428 // The live session keeps the key it is running on: a merge must not lock
1429 // a working device out mid-turn.
1430 r.sessionKeyKept = S.keyFor('groq') === PLAIN;
1431
1432 // The default follows the freshest side, and only to somewhere real.
1433 S.setDefault('groq', 'fresh-1');
1434 const defAt = S.exportSync().defAt;
1435 await S.applySync({ v: 2, def: { provider: 'nowhere', model: 'x' }, defAt: defAt + 60000, providers: {} });
1436 r.defGuarded = S.getDefault().provider === 'groq';
1437 await S.applySync({ v: 2, def: { provider: 'together', model: 'm-a' }, defAt: defAt + 120000, providers: {} });
1438 r.defFreshest = S.getDefault().provider === 'together' && S.getDefault().model === 'm-a';
1439 await S.applySync({ v: 2, def: { provider: 'groq', model: 'fresh-1' }, defAt: 1, providers: {} });
1440 r.defOlderIgnored = S.getDefault().provider === 'together';
1441
1442 // Enumeration order must not reach the wire: the push-skip comparison is a
1443 // string compare, so a store that enumerates differently would push for ever.
1444 S.addProvider('zzz-probe', { name: 'Z', url: 'https://z.example/v1/chat/completions' });
1445 S.addProvider('aaa-probe', { name: 'A', url: 'https://a.example/v1/chat/completions' });
1446 const ks = Object.keys(S.exportSync().providers);
1447 r.sortedKeys = JSON.stringify(ks) === JSON.stringify(ks.slice().sort());
1448
1449 // An old parcel carries no models section at all, and must still apply.
1450 let threw = '';
1451 try {
1452 await S.applySync(undefined);
1453 await S.applySync({});
1454 await S.applySync({ v: 1 });
1455 } catch (e) { threw = String((e && e.message) || e); }
1456 r.oldParcelNoop = threw === '' && !!S.exportSync().providers.groq;
1457
1458 ['groq', 'together', 'zzz-probe', 'aaa-probe'].forEach(function (id) { S.removeProvider(id); });
1459 S.setDefault(def0.provider, def0.model);
1460 return r;
1461 });
1462 check('models.js offers exportSync/applySync for the parcel', M.api === true);
1463 check('two exports of one store are byte-identical', M.deterministic === true);
1464 check('the export lists providers in sorted order (enumeration never reaches the wire)',
1465 M.sortedKeys === true);
1466 check('a model list travels sorted, for the same reason', M.modelsSorted === true);
1467 check('a sealed key travels', M.carriesKeyEnc === true);
1468 check('a readable key never does', M.plaintextAbsent === true);
1469 check('the minted credits row does not travel (it is minted per device)',
1470 M.noMintedRow === true);
1471 check('a provider only the other device has arrives, key and all', M.remoteArrived === true);
1472 check('a provider only this device has survives the merge', M.localSurvived === true);
1473 check('an older remote provider does not clobber a newer local one', M.olderIgnored === true);
1474 check('a fresher remote provider replaces the local one', M.newerWon === true);
1475 check('the running session keeps the key it holds', M.sessionKeyKept === true);
1476 check('the default follows the freshest side', M.defFreshest === true);
1477 check('an older default is ignored', M.defOlderIgnored === true);
1478 check('a default naming a provider nobody has is refused', M.defGuarded === true);
1479 check('a parcel with no models section applies as a no-op', M.oldParcelNoop === true);
1480
1481 // The plaintext-at-rest key exists only where there is no identity to seal
1482 // under — and sync only runs WITH one. It is still checked, because the store
1483 // on disk may predate the identity and the export must not carry it out.
1484 const atRest = await page.evaluate(() => {
1485 const S = window.DaimondModels;
1486 if (typeof S.exportSync !== 'function') return { absent: false, noField: false };
1487 const raw = localStorage.getItem('daimond-models-v2');
1488 localStorage.setItem('daimond-models-v2', JSON.stringify({
1489 v: 2, def: { provider: 'plainprov', model: 'm' },
1490 providers: { plainprov: {
1491 name: 'Plain', url: 'https://plain.example/v1/chat/completions',
1492 key: 'AT-REST-PLAIN-9876', keyEnc: '', models: ['m'], fetched: 1, touched: 1,
1493 } },
1494 }));
1495 S.init({});
1496 const ex = S.exportSync();
1497 const out = {
1498 absent: JSON.stringify(ex).indexOf('AT-REST-PLAIN-9876') === -1,
1499 noField: !Object.prototype.hasOwnProperty.call(ex.providers.plainprov || {}, 'key'),
1500 };
1501 if (raw === null) localStorage.removeItem('daimond-models-v2');
1502 else localStorage.setItem('daimond-models-v2', raw);
1503 S.init({});
1504 return out;
1505 });
1506 check('a plaintext-at-rest key is left behind by the export',
1507 atRest.absent === true && atRest.noField === true, JSON.stringify(atRest));
1508
1509 // ── (9) A provider deleted here stays deleted ──────────────────────
1510 // The provider merge is a UNION, so a row removed on this device and still
1511 // held on the other one was handed straight back on the next pull -- key and
1512 // all -- and removing it was something the user could not make stick. An
1513 // absence still means "that device never had it"; a TOMBSTONE means "it is
1514 // gone", and the stamps decide between a deletion and a re-add.
1515 const T = await page.evaluate(async () => {
1516 const S = window.DaimondModels;
1517 const r = {};
1518 const now = Date.now();
1519 S.addProvider('tombprov', { name: 'Tombed', url: 'https://tomb.example/v1/chat/completions' });
1520 const born = S.exportSync().providers.tombprov.touched;
1521 S.removeProvider('tombprov');
1522 let ex = S.exportSync();
1523 r.gone = !ex.providers.tombprov;
1524 r.tombed = !!(ex.tombs && ex.tombs.tombprov && ex.tombs.tombprov >= born);
1525 // The other device still has it, and offers it back.
1526 await S.applySync({ v: 2, providers: { tombprov: {
1527 name: 'Tombed', url: 'https://tomb.example/v1/chat/completions',
1528 models: [], fetched: 0, touched: born, keyEnc: 'RESURRECTED',
1529 } } });
1530 r.stayedGone = !S.exportSync().providers.tombprov;
1531 // A deletion made on the OTHER device reaches this one: the tombstone
1532 // arrives without the row, and the row here goes.
1533 S.addProvider('otherdev', { name: 'Other', url: 'https://other.example/v1/chat/completions' });
1534 await S.applySync({ v: 2, providers: {}, tombs: { otherdev: Date.now() + 1000 } });
1535 r.remoteDeleteHonoured = !S.exportSync().providers.otherdev;
1536 // A re-add AFTER the deletion wins by its stamp -- deleting a provider must
1537 // not make its id unusable for a week.
1538 S.addProvider('tombprov', { name: 'Back', url: 'https://back.example/v1/chat/completions' });
1539 r.readdSurvives = !!S.exportSync().providers.tombprov;
1540 await S.applySync({ v: 2, providers: {}, tombs: { tombprov: now - 1000 } });
1541 r.readdSurvivesMerge = !!S.exportSync().providers.tombprov;
1542 S.removeProvider('tombprov');
1543 return r;
1544 });
1545 check('removing a provider takes it out of the store', T.gone === true);
1546 check('and leaves a tombstone in the parcel', T.tombed === true);
1547 check('a deleted provider handed back by the other device does not come back',
1548 T.stayedGone === true);
1549 check('a provider deleted on the other device is deleted here',
1550 T.remoteDeleteHonoured === true);
1551 check('a provider re-added after its deletion survives', T.readdSurvives === true);
1552 check('and survives a merge carrying the older tombstone', T.readdSurvivesMerge === true);
1553
1554 // ── (10) The two standing refusals, on one chip, in one order ──────
1555 // 402 (not on the tier) and 413 (parcel too large) both outlive the round
1556 // that found them, and both are reported on the chip alone. So neither may be
1557 // painted over by an ordinary round -- a pull SUCCEEDING used to show
1558 // "Synced" on a device whose pushes were paused by a 402, and a pull FAILING
1559 // used to blank a refusal that was still perfectly true. And the chip has to
1560 // lead somewhere: "Sync off" names Pro, and Pro is bought in Credits.
1561 const gate = await page.evaluate(async () => {
1562 const chip = () => {
1563 const c = document.getElementById('sync-chip');
1564 if (!c) return null;
1565 return { state: c.dataset.state || '', title: c.title || '',
1566 text: (c.querySelector('.stext') || {}).textContent || '',
1567 shown: c.style.display !== 'none' };
1568 };
1569 // Every refusal below is only reached if a parcel is actually sent, so each
1570 // change records whether it moved the parcel and the file asserts on it.
1571 const bumps = [];
1572 const bump = async (tag) => {
1573 const r = await window.__bump(tag);
1574 bumps.push(tag + ': ' + (r.moved ? 'moved' : r.why));
1575 return r.moved;
1576 };
1577 const real = window.fetch;
1578 // Refusals ANSWERED, not merely offered. A push that finds a round in
1579 // flight reschedules and returns, so a single call could leave every
1580 // assertion below reporting on a stub nothing ever reached.
1581 let sent = 0;
1582 const stub = (post, get) => {
1583 window.fetch = function (u, o) {
1584 const url = String((u && u.url) || u || '');
1585 const isSync = url.indexOf('/api/sync') !== -1;
1586 const method = (o && o.method) || 'GET';
1587 const code = isSync ? (method === 'POST' ? post : get) : 0;
1588 if (code) {
1589 if (method === 'POST') sent++;
1590 return Promise.resolve(new Response(JSON.stringify({ ok: false, error: 'stub' }),
1591 { status: code, headers: { 'content-type': 'application/json' } }));
1592 }
1593 return real.apply(this, arguments);
1594 };
1595 };
1596 const out = {};
1597 // A 413 first: the parcel is too large, and the chip stalls.
1598 stub(413, 0);
1599 await bump('gate-1');
1600 out.sent413 = await window.__pushSent(() => sent, 'gate-1');
1601 out.stalled = chip();
1602 // Then a 402 on top of it. Not entitled outranks too large: an account
1603 // that may not sync at all cannot act on a parcel being oversized.
1604 stub(402, 0);
1605 await bump('gate-2');
1606 sent = 0;
1607 out.sent402 = await window.__pushSent(() => sent, 'gate-2');
1608 out.off = chip();
1609 // A pull that WORKS must not paint "Synced" over it.
1610 window.fetch = real;
1611 await window.DaimondSync.pull();
1612 out.afterGoodPull = chip();
1613 // Nor may a pull that fails simply hide it.
1614 stub(0, 500);
1615 await window.DaimondSync.pull();
1616 out.afterBadPull = chip();
1617 window.fetch = real;
1618 // Clicking it goes where the sentence leads.
1619 document.getElementById('sync-chip').click();
1620 await new Promise(r => setTimeout(r, 400));
1621 const cv = document.getElementById('admin-credits');
1622 out.creditsOpen = !!(cv && cv.style.display !== 'none' && cv.offsetParent !== null);
1623 out.creditsNote = (document.getElementById('credits-note') || {}).textContent || '';
1624 if (window.DaimondAdmin && DaimondAdmin.close) DaimondAdmin.close();
1625 // The tier comes back: the stall underneath it is still true and shows again.
1626 window.DaimondSync.recheck();
1627 await new Promise(r => setTimeout(r, 1200));
1628 out.afterRecheck = chip();
1629 // And a push that fits clears the lot, so nothing below runs under a stall.
1630 await bump('gate-3');
1631 await window.DaimondSync.push();
1632 await new Promise(r => setTimeout(r, 400));
1633 out.cleared = chip();
1634 out.api = window.DaimondSync.state();
1635 out.bumps = bumps;
1636 return out;
1637 });
1638 check('each refusal below was answered to a parcel that really left',
1639 allMoved(gate.bumps), (gate.bumps || []).join(' | '));
1640 check('and both refusals were reached by a parcel that was actually SENT',
1641 gate.sent413.sent === true && gate.sent402.sent === true,
1642 '413 ' + JSON.stringify(gate.sent413) + ', 402 ' + JSON.stringify(gate.sent402));
1643 check('a 413 stalls the chip', gate.stalled && gate.stalled.state === 'stalled',
1644 JSON.stringify(gate.stalled));
1645 check('a 402 on top of a stall shows "Sync off" — not entitled outranks too large',
1646 !!(gate.off && gate.off.state === 'off' && gate.off.shown), JSON.stringify(gate.off));
1647 check('and says on hover that it is Pro, and that the chip can be clicked',
1648 !!(gate.off && /Pro/.test(gate.off.title) && /Credits/i.test(gate.off.title)),
1649 (gate.off && gate.off.title || '').replace(/\n/g, ' | ').slice(0, 140));
1650 check('a pull that WORKS does not paint "Synced" over a device whose pushes are off',
1651 !!(gate.afterGoodPull && gate.afterGoodPull.state === 'off'),
1652 JSON.stringify(gate.afterGoodPull));
1653 check('a pull that FAILS does not hide the refusal either',
1654 !!(gate.afterBadPull && gate.afterBadPull.state === 'off' && gate.afterBadPull.shown),
1655 JSON.stringify(gate.afterBadPull));
1656 check('clicking the off chip opens the Pro offer in Credits',
1657 gate.creditsOpen === true && gate.creditsNote.length > 0,
1658 'open=' + gate.creditsOpen + ' note=' + JSON.stringify(gate.creditsNote.slice(0, 60)));
1659 check('when the tier comes back the stall underneath is still reported',
1660 !!(gate.afterRecheck && gate.afterRecheck.state === 'stalled'),
1661 JSON.stringify(gate.afterRecheck));
1662 check('and a push that fits clears both',
1663 !!(gate.cleared && gate.cleared.state === 'synced')
1664 && gate.api.stalled === false && gate.api.entitled === true,
1665 JSON.stringify(gate.cleared) + ' ' + JSON.stringify(gate.api));
1666
1667 // ── (11) Work done OUTSIDE a turn travels on its own ───────────────
1668 // Pushes fired on exactly two things: a turn ending, and the tab going away.
1669 // Most of what a person does to a Diamond is neither. A user renamed a
1670 // Diamond on one machine, left the tab open and focused, and the new name
1671 // never reached the other machine — because nothing ever scheduled the push.
1672 // The other device's focus pull was working perfectly; the mailbox simply
1673 // still held the old name.
1674 //
1675 // Measured from a QUIET engine. A push that finds one in flight reschedules,
1676 // so `pushLanded` leaves a timer armed, and that stray timer would carry the
1677 // rename and hide the whole bug — which is exactly what it did the first time
1678 // this was written.
1679
1680 /// Let anything already armed drain, and return once the version has stopped
1681 /// moving. A flat sleep is not enough: what has to be true is that the engine
1682 /// is quiet, not that some number of milliseconds passed.
1683 const quiesce = async (pg, ms = 20000) => {
1684 let last = -1, stable = Date.now();
1685 const t0 = Date.now();
1686 while (Date.now() - t0 < ms) {
1687 const v = await pg.evaluate(() => window.DaimondSync.state().version);
1688 if (v !== last) { last = v; stable = Date.now(); }
1689 else if (Date.now() - stable > 5000) return last;
1690 await pg.waitForTimeout(300);
1691 }
1692 return last;
1693 };
1694
1695 /// Wait for the engine to push on its OWN. Deliberately never calls push():
1696 /// the point of the whole section is that the change leaves without asking.
1697 const ownPush = async (pg, v0, ms = 25000) => {
1698 const t0 = Date.now();
1699 while (Date.now() - t0 < ms) {
1700 const v = await pg.evaluate(() => window.DaimondSync.state().version);
1701 if (v > v0) return { landed: true, took: Date.now() - t0 };
1702 await pg.waitForTimeout(250);
1703 }
1704 return { landed: false, took: Date.now() - t0 };
1705 };
1706
1707 /// Count POSTs to the mailbox, so "nothing was sent" is a measurement.
1708 const countPosts = (pg) => pg.evaluate(() => {
1709 window.__syncPosts = 0;
1710 const real = window.__syncRealFetch || window.fetch;
1711 window.__syncRealFetch = real;
1712 window.fetch = function (u, o) {
1713 const url = String((u && u.url) || u || '');
1714 if (url.indexOf('/api/sync') !== -1 && o && o.method === 'POST') window.__syncPosts++;
1715 return real.apply(this, arguments);
1716 };
1717 });
1718 const posts = (pg) => pg.evaluate(() => window.__syncPosts | 0);
1719 const unstub = (pg) => pg.evaluate(() => {
1720 if (window.__syncRealFetch) { window.fetch = window.__syncRealFetch; window.__syncRealFetch = null; }
1721 });
1722
1723 /// Rename a Diamond the way a person does: double-click its name in the rail,
1724 /// type into the dialog, press the button. No turn is taken and the tab is
1725 /// never hidden — which is the whole of the report.
1726 const renameDiamond = async (from, to) => {
1727 const found = await page.evaluate((nm) => {
1728 const box = [...document.querySelectorAll('#diamond-list .diamond-box')]
1729 .find(b => ((b.querySelector('.session-box-name') || {}).textContent || '').trim() === nm);
1730 if (!box) return false;
1731 box.querySelector('.session-box-name')
1732 .dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
1733 return true;
1734 }, from);
1735 if (!found) return 'not in the rail';
1736 await page.waitForSelector('.dlg-card', { timeout: 8000 });
1737 await page.evaluate((nm) => {
1738 const card = [...document.querySelectorAll('.dlg-card')].find(c => c.getClientRects().length);
1739 const inp = card.querySelector('input.dlg-input');
1740 inp.value = nm;
1741 inp.dispatchEvent(new Event('input', { bubbles: true }));
1742 card.querySelector('.dlg-ok').click();
1743 }, to);
1744 await page.waitForTimeout(600);
1745 return 'ok';
1746 };
1747
1748 /// What the MAILBOX holds, decrypted — what the other device would receive if
1749 /// it pulled this instant. The Diamond names, and what the trash says about
1750 /// each of them, because since the trash those are two different facts about
1751 /// the same Diamond and a delete moves only the second.
1752 const inMailbox = () => page.evaluate(async () => {
1753 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
1754 const j = await r.json();
1755 if (!j.present) return { names: [], trash: {} };
1756 try {
1757 const st = JSON.parse(await window.DaimondIdentity.unwrap(j.blob));
1758 return {
1759 names: (st.diamonds || []).map((d) => {
1760 try { return JSON.parse(JSON.parse(d.data).files['.daimond/meta.json']).name; }
1761 catch (e) { return '?'; }
1762 }),
1763 trash: (st.trash && st.trash.items) || {},
1764 };
1765 } catch (e) { return { names: ['<undecryptable>'], trash: {} }; }
1766 });
1767 const namesInMailbox = async () => (await inMailbox()).names;
1768
1769 await newDiamond('Quiet-Alpha');
1770 await pushLanded(page, 'Quiet-Alpha');
1771 const vQuiet = await quiesce(page);
1772 check('the engine is quiet before the measurement, so nothing stray can carry it',
1773 vQuiet > 0, 'version=' + vQuiet);
1774
1775 await countPosts(page);
1776 const renamed = await renameDiamond('Quiet-Alpha', 'Quiet-Renamed');
1777 const own = await ownPush(page, vQuiet);
1778 const mailNames = await namesInMailbox();
1779 check('a Diamond is renamed through the rail, with no turn and the tab visible',
1780 renamed === 'ok', renamed);
1781 check('and the rename pushes ON ITS OWN — no turn, no tab-hide, no explicit push',
1782 own.landed === true, 'version ' + vQuiet + ' -> '
1783 + (await page.evaluate(() => window.DaimondSync.state().version))
1784 + ' after ' + own.took + 'ms, posts=' + (await posts(page)));
1785 check('so the mailbox holds the NEW name, which is all the other device can read',
1786 mailNames.includes('Quiet-Renamed') && !mailNames.includes('Quiet-Alpha'),
1787 JSON.stringify(mailNames));
1788 check('and it took ONE parcel, not one per keystroke', (await posts(page)) === 1,
1789 'posts=' + (await posts(page)));
1790
1791 // The receiving half, end to end: a device that has never seen the rename
1792 // pulls it. (Pull-on-focus is proved in section 7; what is proved here is
1793 // that there is now something on the mailbox for it to find.)
1794 const second = await page.evaluate(async () => {
1795 const m = await import('/pkg/oxedyne_daimond.js');
1796 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
1797 for (const d of JSON.parse(await app.list_diamonds())) await app.delete_diamond(d.id);
1798 localStorage.removeItem('daimond-sync-version');
1799 await window.DaimondSync.pull();
1800 return JSON.parse(await app.list_diamonds()).map(d => d.name);
1801 });
1802 check('a second device pulls the rename in', second.includes('Quiet-Renamed'),
1803 JSON.stringify(second));
1804
1805 // A second real mutation path through the same funnel: deleting through the
1806 // rail, which writes the TRASH RECORD that has to travel with it.
1807 //
1808 // Not a tombstone, and not an absence. (4b) settled what a rail delete now
1809 // means -- the Diamond is trashed, its bytes still travel so the other device
1810 // can restore it, and only destroying it from the trash tombstones it -- and
1811 // this check went on asserting the contract that replaced. It failed on every
1812 // run, naming the Diamond it had just been told would still be there.
1813 await quiesce(page);
1814 await countPosts(page);
1815 const vDel = await page.evaluate(() => window.DaimondSync.state().version);
1816 const quietId = await wasm(async (app) =>
1817 (JSON.parse(await app.list_diamonds()).find(d => d.name === 'Quiet-Renamed') || {}).id || '');
1818 const dropped = await removeDiamond('Quiet-Renamed');
1819 const delPush = await ownPush(page, vDel);
1820 const afterDel = await inMailbox();
1821 const delRec = afterDel.trash[quietId] || null;
1822 check('deleting a Diamond in the rail also travels without a turn',
1823 dropped === 'ok' && delPush.landed === true,
1824 dropped + ', after ' + delPush.took + 'ms');
1825 // The other Diamonds are named too, so an empty or unreadable mailbox cannot
1826 // pass this by holding nothing.
1827 check('and the mailbox says it is in the trash, while still holding the rest',
1828 !!quietId && !!delRec && delRec.at > delRec.back && afterDel.names.includes('Link-Travel'),
1829 'id=' + quietId + ' rec=' + JSON.stringify(delRec) + ' ' + JSON.stringify(afterDel.names));
1830
1831 // A nudge is not a poll, and this is the failure mode the fix itself could
1832 // have: collectSync() persists the chats on its way past, so if THAT nudged,
1833 // every push would arm the next one for ever.
1834 //
1835 // Counted as parcels PACKED, not as requests sent. An unchanged parcel is
1836 // skipped before any request is made, so a POST counter watches a perfectly
1837 // quiet network while the app re-exports every Diamond every 2.5 seconds —
1838 // measured at six packs in fifteen seconds with the guard removed, and one
1839 // with it. Both numbers are checked, because both costs are real.
1840 await quiesce(page);
1841 // Refresh this device's roster stamp first: it is rewritten when it goes
1842 // stale (five minutes), and a run that crossed that boundary here would see
1843 // a genuinely changed parcel and read it as a spurious push.
1844 await page.evaluate(async () => { await window.DaimondCore.collectSync(); return true; });
1845 await countPosts(page);
1846 const idle = await page.evaluate(async () => {
1847 let packed = 0;
1848 const real = window.DaimondCore.collectSync;
1849 window.DaimondCore.collectSync = function () { packed++; return real.apply(this, arguments); };
1850 window.DaimondSync.nudge();
1851 await new Promise(r => setTimeout(r, 15000));
1852 window.DaimondCore.collectSync = real;
1853 return { packed: packed, posts: window.__syncPosts | 0 };
1854 });
1855 check('a nudge with nothing changed sends no parcel at all', idle.posts === 0,
1856 'posts=' + idle.posts);
1857 check('and packing it does not arm the next one — the debounce is not a poll',
1858 idle.packed <= 1, 'parcels packed in 15s = ' + idle.packed);
1859
1860 // A nudge must not walk through the standing refusals either. 402 stops
1861 // pushes until the tier is rechecked, and a mutation arriving afterwards
1862 // must not quietly restart them or repaint the chip.
1863 const refusal = await page.evaluate(async () => {
1864 const chip = () => {
1865 const c = document.getElementById('sync-chip');
1866 return c ? { state: c.dataset.state || '', shown: c.style.display !== 'none' } : null;
1867 };
1868 // One real change, made before the stub so nothing it does is answered by
1869 // it, and asserted on: without it the push below is skipped and the 402
1870 // arm is never reached at all.
1871 const changed = await window.__bump('nudge-402');
1872 const real = window.__syncRealFetch || window.fetch;
1873 window.__syncRealFetch = real;
1874 let sent = 0;
1875 window.fetch = function (u, o) {
1876 const url = String((u && u.url) || u || '');
1877 if (url.indexOf('/api/sync') !== -1 && o && o.method === 'POST') {
1878 sent++;
1879 return Promise.resolve(new Response(JSON.stringify({ ok: false, error: 'stub' }),
1880 { status: 402, headers: { 'content-type': 'application/json' } }));
1881 }
1882 return real.apply(this, arguments);
1883 };
1884 // Pushed, refused: the engine is now paused on a 402. Driven until one
1885 // really goes -- a push over a round already in flight is only
1886 // rescheduled, and the pause below would then be nobody's.
1887 const reached = await window.__pushSent(() => sent, 'nudge-402');
1888 const paused = { chip: chip(), entitled: window.DaimondSync.state().entitled, reached };
1889 // Now a mutation of the kind that nudges. It must change nothing.
1890 const before = sent;
1891 await window.DaimondCore.collectSync(); // persistChats() runs on the way past
1892 window.DaimondSync.nudge();
1893 await new Promise(r => setTimeout(r, 7000));
1894 const out = { paused, after: chip(), tried: sent - before, changed: changed,
1895 entitled: window.DaimondSync.state().entitled };
1896 window.fetch = real;
1897 window.__syncRealFetch = null;
1898 return out;
1899 });
1900 check('the change that provoked the 402 really left this device',
1901 refusal.changed.moved === true, refusal.changed.why);
1902 check('a 402 pauses pushes and says so on the chip',
1903 !!(refusal.paused.chip && refusal.paused.chip.state === 'off')
1904 && refusal.paused.entitled === false && refusal.paused.reached.sent === true,
1905 JSON.stringify(refusal.paused));
1906 check('a nudge afterwards does not restart them behind the refusal',
1907 refusal.tried === 0 && refusal.entitled === false, 'attempts=' + refusal.tried);
1908 check('nor repaint the chip over it',
1909 !!(refusal.after && refusal.after.state === 'off' && refusal.after.shown),
1910 JSON.stringify(refusal.after));
1911
1912 // Put the engine back, so nothing below runs paused.
1913 await unstub(page);
1914 await page.evaluate(() => window.DaimondSync.recheck());
1915 await page.waitForTimeout(1500);
1916 await pushLanded(page, 'the tier coming back');
1917 const back = await page.evaluate(() => window.DaimondSync.state());
1918 check('and the tier coming back lifts it, with the engine syncing again',
1919 back.entitled === true && back.stalled === false, JSON.stringify(back));
1920
1921 // (9) The device roster travels, so a user can be told whether their account
1922 // is on more than one device.
1923 //
1924 // Nothing else can tell them. Pairing hands the second device the SAME
1925 // keypair, so the gateway sees one user and cannot count devices, and the
1926 // parked bundle is deleted on redeem, so there is no server record of the
1927 // pairing either. The count therefore has to come out of the parcel, which
1928 // means it has to survive the real encrypted round trip -- and the roster must
1929 // stay INSIDE the sealed blob while it does. The second device is simulated
1930 // the way this file simulates one everywhere else: by swapping what this
1931 // browser holds.
1932 const DEV_B = 'bbbb2222cccc3333';
1933 const roster0 = await page.evaluate(() => ({
1934 self: localStorage.getItem('daimond-device-id'),
1935 reg: localStorage.getItem('daimond-devices') || '{}',
1936 }));
1937 check('this device is on its own roster before any of it travels',
1938 /^[0-9a-f]{16}$/.test(roster0.self || '') && !!JSON.parse(roster0.reg)[roster0.self],
1939 roster0.reg.slice(0, 120));
1940
1941 await pushLanded(page, 'the device roster');
1942 const sealed = await page.evaluate(async () => {
1943 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
1944 const j = await r.json();
1945 return j.blob || '';
1946 });
1947 check('the roster rides inside the sealed blob — the gateway is told no device name',
1948 !sealed.includes('device') && !sealed.includes('Chromium') && !sealed.includes(roster0.self),
1949 'blob ' + sealed.length + ' bytes');
1950
1951 // Device B: another install of the app, which has never seen this roster.
1952 const bSaw = await page.evaluate(async (b) => {
1953 localStorage.setItem('daimond-device-id', b);
1954 localStorage.setItem('daimond-devices', '{}');
1955 await window.DaimondSync.pull();
1956 return JSON.parse(localStorage.getItem('daimond-devices') || '{}');
1957 }, DEV_B);
1958 check('the second device pulls and learns of the first',
1959 !!bSaw[roster0.self], Object.keys(bSaw).join(','));
1960 await pushLanded(page, 'device B line');
1961
1962 // Device A again, knowing only itself, exactly as it was left.
1963 const aSaw = await page.evaluate(async (r) => {
1964 localStorage.setItem('daimond-device-id', r.self);
1965 localStorage.setItem('daimond-devices', r.reg);
1966 await window.DaimondSync.pull();
1967 return JSON.parse(localStorage.getItem('daimond-devices') || '{}');
1968 }, roster0);
1969 check('and the first device pulls and learns of the second',
1970 !!aSaw[DEV_B], Object.keys(aSaw).join(','));
1971 check('so both ends see BOTH devices — the account is visibly on two',
1972 !!aSaw[roster0.self] && !!aSaw[DEV_B] && Object.keys(aSaw).length === 2,
1973 JSON.stringify(aSaw).slice(0, 200));
1974 const lineB = aSaw[DEV_B] || {};
1975 check('each line carries a name and a last-seen, so the list can be read',
1976 !!lineB.name && lineB.seen > 1.7e12 && lineB.created > 1.7e12,
1977 JSON.stringify(lineB));
1978
1979 // A name the USER gives a device, the whole way round. The second device
1980 // names the FIRST one's line -- which is the case that cannot work if the
1981 // name rides on `seen`, because the first device refreshes its own line and
1982 // would win it straight back. And the name is the user's own words, so it
1983 // must reach the other device WITHOUT the gateway ever holding it in the
1984 // clear: sync.js sends the coarse derived description as the mailbox label
1985 // and nothing else, so the typed name may exist only inside the sealed blob.
1986 const rename = await page.evaluate(async (r) => {
1987 const wait = (n) => new Promise(x => setTimeout(x, n));
1988 const mine = JSON.parse(localStorage.getItem('daimond-devices') || '{}');
1989 const out = { aBefore: mine[r.self] };
1990 // Device B, naming device A's line through the drawer, as a user would.
1991 localStorage.setItem('daimond-device-id', r.b);
1992 DaimondAdmin.home();
1993 const row = [...document.querySelectorAll('#admin-home .device-row')]
1994 .find(x => ((x.querySelector('.device-id') || {}).textContent || '') === r.self.slice(-4));
1995 const btn = row && row.querySelector('.device-rename');
1996 if (!btn) { out.renamed = false; return out; }
1997 btn.click();
1998 await wait(80);
1999 const input = document.querySelector('.dlg .dlg-input');
2000 const ok = document.querySelector('.dlg .dlg-ok');
2001 if (!input || !ok) { out.renamed = false; return out; }
2002 input.value = 'Kitchen laptop';
2003 ok.click();
2004 await wait(200);
2005 DaimondAdmin.close();
2006 out.renamed = (JSON.parse(localStorage.getItem('daimond-devices') || '{}')[r.self] || {}).label
2007 === 'Kitchen laptop';
2008 return out;
2009 }, { self: roster0.self, b: DEV_B });
2010 check('the second device can name the first one\'s line', rename.renamed === true,
2011 JSON.stringify(rename.aBefore));
2012
2013 await pushLanded(page, 'the typed device name');
2014 const sealedNamed = await page.evaluate(async () => {
2015 const r = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
2016 const j = await r.json();
2017 return { blob: j.blob || '', label: j.device || j.label || '' };
2018 });
2019 check('the name the user typed is inside the sealed blob and nowhere else on the wire',
2020 !sealedNamed.blob.includes('Kitchen') && !sealedNamed.blob.includes('laptop')
2021 && !/kitchen/i.test(JSON.stringify(sealedNamed.label)),
2022 'blob ' + sealedNamed.blob.length + ' bytes, mailbox label ' + JSON.stringify(sealedNamed.label));
2023
2024 // Back to device A, with the roster it had before any of this: its own line
2025 // is the FRESHER one, and it still has to take the name.
2026 const aNamed = await page.evaluate(async (r) => {
2027 localStorage.setItem('daimond-device-id', r.self);
2028 localStorage.setItem('daimond-devices', r.reg);
2029 const before = JSON.parse(r.reg)[r.self];
2030 await window.DaimondSync.pull();
2031 const after = JSON.parse(localStorage.getItem('daimond-devices') || '{}')[r.self];
2032 DaimondAdmin.home();
2033 const rows = [...document.querySelectorAll('#admin-home .device-row')]
2034 .map(x => x.innerText.replace(/\s+/g, ' ').trim());
2035 DaimondAdmin.close();
2036 return { before: before, after: after, rows: rows };
2037 }, { self: roster0.self, reg: JSON.stringify(aSaw) });
2038 check('and the first device pulls the name for ITSELF, though its own line is the fresher one',
2039 !!aNamed.after && aNamed.after.label === 'Kitchen laptop'
2040 && aNamed.after.seen >= aNamed.before.seen,
2041 JSON.stringify(aNamed.after));
2042 check('so the drawer there now reads the name its owner gave it',
2043 aNamed.rows.some(x => /Kitchen laptop/.test(x) && /this device/i.test(x)),
2044 aNamed.rows.join(' | '));
2045 // The drawer is the surface the user actually reads.
2046 const shown = await page.evaluate(() => {
2047 DaimondAdmin.home();
2048 const rows = [...document.querySelectorAll('#admin-home .device-row')]
2049 .map(r => r.innerText.replace(/\s+/g, ' ').trim());
2050 DaimondAdmin.close();
2051 return rows;
2052 });
2053 check('and the Admin drawer lists them both, with this one marked',
2054 shown.length === 2 && shown.filter(r => /this device/i.test(r)).length === 1,
2055 shown.join(' | '));
2056
2057 // (5) The export bundle carries the salt (without it, no second device could decrypt).
2058 const bundle = await page.evaluate(() => {
2059 const b = window.DaimondIdentity.exportBundle();
2060 return b ? { hasSalt: !!b.salt, hasPriv: !!b.priv, hasPub: !!b.pub, v: b.v } : null;
2061 });
2062 check('identity export bundle carries salt + wrapped key + pubkey',
2063 !!bundle && bundle.hasSalt && bundle.hasPriv && bundle.hasPub && bundle.v === 1);
2064
2065 // ── (10) One bad section must not swallow the parcel ───────────────
2066 // A parcel is written by ANOTHER device: a version behind, a version ahead,
2067 // or halfway through a write when it was packed. Every section of the merge
2068 // is meant to be best effort, so that one of them failing costs only itself.
2069 // The TOP of applySync was not: the chats ran outside any guard, so a
2070 // transcript that was not a list threw out of the whole function and the
2071 // Diamonds, the files, the providers and the mailboxes below it were never
2072 // reached. The pull that called it logged one console.debug line -- hidden in
2073 // DevTools unless Verbose is on -- adopted the version, and went on to push
2074 // this device's state over the top of the parcel it had just failed to merge.
2075 const poison = await page.evaluate(async () => {
2076 const m = await import('/pkg/oxedyne_daimond.js');
2077 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2078 const id = await app.create_diamond('Poison-Local');
2079 const pack = JSON.parse(await app.export_diamond(id));
2080 const meta = JSON.parse(pack.files['.daimond/meta.json']);
2081 meta.name = 'Poison-Survivor';
2082 meta.touched = Date.now() + 60000; // the other device's copy is the fresher one
2083 pack.files['.daimond/meta.json'] = JSON.stringify(meta);
2084 const parcel = await window.DaimondCore.collectSync();
2085 parcel.diamonds = [{ id: id, updated: meta.updated || 0, touched: meta.touched,
2086 model: null, data: JSON.stringify(pack) }];
2087 // A chat THIS DEVICE ALREADY HOLDS, arriving with a transcript that is
2088 // not a list. A chat nobody here has seen is simply stored; one that is
2089 // held is MERGED, and the union walks the transcript -- so this is where
2090 // a parcel written by another build, or half written when it was packed,
2091 // actually reaches. Nothing is left behind by it: the throw happens
2092 // before the merged chats are written back.
2093 // From the store the chats actually live in. Read out of localStorage, this
2094 // was always an empty list: the parcel was never poisoned, and the three
2095 // checks below passed on a merge that had nothing to fail at.
2096 const store = window.DaimondCore.chatStore();
2097 const held = store.stored();
2098 const victim = held.length ? held[0].id : '';
2099 parcel.chats = (parcel.chats || []).map(c =>
2100 (c && c.id === victim) ? Object.assign({}, c, { messages: 'not-a-list' }) : c);
2101 let threw = '', report = null;
2102 try { report = await window.DaimondCore.applySync(parcel); }
2103 catch (e) { threw = String(e && e.message || e); }
2104 const row = JSON.parse(await app.list_diamonds()).find(d => d.id === id) || null;
2105 const kept = store.stored();
2106 return { threw, report, victim, name: row ? row.name : '(gone)',
2107 chatsIntact: kept.length === held.length
2108 && kept.every(c => Array.isArray(c.messages)) };
2109 });
2110 check('the poisoned parcel names a chat this device really holds', !!poison.victim, poison.victim);
2111 check('a malformed section does not throw out of applySync', poison.threw === '', poison.threw);
2112 check('and the Diamond in the same parcel still arrives',
2113 poison.name === 'Poison-Survivor', poison.name);
2114 check('and the merge SAYS which section it could not apply',
2115 !!(poison.report && Array.isArray(poison.report.failed) && poison.report.failed.indexOf('chats') !== -1),
2116 JSON.stringify(poison.report));
2117 check('and the section that failed left this device’s own chats as they were',
2118 poison.chatsIntact === true);
2119
2120 // ── (11) Giving up is said out loud ────────────────────────────────
2121 // The pull-merge-retry loop is bounded, and running out of attempts used to
2122 // be one console.debug line. Worse: the last thing on the chip was the
2123 // "Synced" the reconciling PULL put there, so a device whose work never left
2124 // looked exactly like a device that had just saved.
2125 const exhausted = await page.evaluate(async () => {
2126 const chip = () => {
2127 const c = document.getElementById('sync-chip');
2128 return c ? { state: c.dataset.state || '', text: (c.querySelector('.stext') || {}).textContent || '',
2129 title: c.title || '', shown: c.style.display !== 'none' } : null;
2130 };
2131 // Something of this device's own to lose, made before the stub: with an
2132 // unchanged parcel the push never leaves and there is no conflict to
2133 // exhaust.
2134 const changed = await window.__bump('storm-note');
2135 const real = window.fetch;
2136 let posts = 0;
2137 window.fetch = function (u, o) {
2138 const url = String((u && u.url) || u || '');
2139 if (url.indexOf('/api/sync') !== -1 && o && o.method === 'POST') {
2140 posts++;
2141 return Promise.resolve(new Response(
2142 JSON.stringify({ ok: false, conflict: true, version: 9999, device: 'the other one' }),
2143 { status: 409, headers: { 'content-type': 'application/json' } }));
2144 }
2145 return real.apply(this, arguments);
2146 };
2147 // Driven until a parcel really leaves. A single push() here measured a
2148 // gateway nobody had spoken to: the call found a round already in flight,
2149 // rescheduled, and returned exactly as it does when it sent -- posts=0,
2150 // the chip still reading "Syncing…" from the other round, and all four
2151 // checks below red for a reason that had nothing to do with conflicts.
2152 //
2153 // `posts` is zeroed before each attempt, so what the bound below measures
2154 // is ONE push's retries and not the sum of every round in the window.
2155 let attempts = 0;
2156 const t0 = Date.now();
2157 while (posts === 0 && Date.now() - t0 < 15000) {
2158 attempts++;
2159 if (attempts > 1) await window.__bump('storm-note-' + attempts);
2160 posts = 0;
2161 await window.DaimondSync.push();
2162 if (posts === 0) await new Promise(r => setTimeout(r, 300));
2163 }
2164 const out = { posts, attempts, changed, chip: chip(), api: window.DaimondSync.state() };
2165 window.fetch = real;
2166 return out;
2167 });
2168 check('the work the conflict is about really left this device',
2169 exhausted.changed.moved === true, exhausted.changed.why);
2170 check('a permanent conflict is retried, and bounded', exhausted.posts >= 2 && exhausted.posts <= 6,
2171 'posts=' + exhausted.posts + ' over ' + exhausted.attempts + ' push attempts');
2172 check('and running out of attempts is SAID, not swallowed',
2173 !!(exhausted.chip && exhausted.chip.state === 'stalled' && exhausted.chip.shown),
2174 JSON.stringify(exhausted.chip));
2175 check('with a reason that names what is happening — another device writing too',
2176 !!(exhausted.chip && /device/i.test(exhausted.chip.title)),
2177 (exhausted.chip && exhausted.chip.title || '').slice(0, 140));
2178 check('and the engine reports the stall through its own surface',
2179 !!(exhausted.api && exhausted.api.stalled === true), JSON.stringify(exhausted.api));
2180
2181 // Clear the stall before the second device runs below.
2182 await bumped(page, 'calm-note');
2183 await pushLanded(page, 'clearing the stall');
2184
2185 // ── (12) Two REAL devices converge, both ways ──────────────────────
2186 // Every check above simulates the second device inside this browser. That
2187 // cannot see the thing that actually broke in the field: two windows, both
2188 // open and both FOCUSED, on two machines. Neither raises a focus event, so
2189 // the focus pull never fires, and the app settling only ever schedules a
2190 // PUSH -- which is skipped outright when this device has nothing new to send.
2191 // A device that is not editing therefore never asked the gateway anything at
2192 // all, and the other device's work sat in the mailbox unread for as long as
2193 // the window stayed where it was.
2194 child = await open({ name: 'syncmate', signIn: false, connect: false });
2195 await child.page.waitForFunction(() => !!window.DaimondPairing, null, { timeout: 15000 }).catch(() => {});
2196 const code = await page.evaluate(() => DaimondPairing.create());
2197 await child.page.evaluate(c => DaimondPairing.redeem(c), code.code);
2198 await child.page.reload({ waitUntil: 'domcontentloaded' });
2199 await signInAs(child, 'sync');
2200 await child.page.waitForFunction(
2201 () => !!window.DaimondSync && window.DaimondGateway && DaimondGateway.state().authed,
2202 null, { timeout: 15000 }).catch(() => {});
2203 const mate = await child.page.evaluate(() => ({
2204 authed: DaimondGateway.state().authed,
2205 same: window.DaimondIdentity.publicKeyB64url(),
2206 }));
2207 const mine = await page.evaluate(() => window.DaimondIdentity.publicKeyB64url());
2208 check('a second REAL device holds the same account and an authed session',
2209 mate.authed === true && mate.same === mine, JSON.stringify(mate).slice(0, 80));
2210
2211 /// One Diamond's name, as each device's own store reads it.
2212 const nameOn = (pg, id) => pg.evaluate(async (id) => {
2213 const m = await import('/pkg/oxedyne_daimond.js');
2214 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2215 return ((JSON.parse(await app.list_diamonds()).find(d => d.id === id)) || {}).name || '(absent)';
2216 }, id);
2217
2218 /// The same name, once the store has SETTLED on it. Every assertion below
2219 /// that expects a particular name is asserting the end of a merge, and
2220 /// `pull()` resolving is not the same instant as the store having finished
2221 /// rewriting the directory -- two `list_diamonds()` calls microseconds apart
2222 /// were observed disagreeing, which is what (4c) settles rather than
2223 /// snapshots for the same reason.
2224 ///
2225 /// Bounded, and it returns whatever it last read: a name that never arrives
2226 /// fails the caller's check with the name it actually found, rather than
2227 /// hanging or passing.
2228 const nameSettles = async (pg, id, want, ms = 6000) => {
2229 const t0 = Date.now();
2230 let seen = '';
2231 do {
2232 seen = await nameOn(pg, id);
2233 if (seen === want) return seen;
2234 await pg.waitForTimeout(150);
2235 } while (Date.now() - t0 < ms);
2236 return seen;
2237 };
2238
2239 // A shared Diamond, on both devices, agreed.
2240 const shared = await page.evaluate(async () => {
2241 const m = await import('/pkg/oxedyne_daimond.js');
2242 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2243 return await app.create_diamond('Both-Devices');
2244 });
2245 await pushLanded(page, 'the shared Diamond');
2246 await child.page.evaluate(() => window.DaimondSync.pull());
2247 check('the second device pulls the shared Diamond down',
2248 (await nameSettles(child.page, shared, 'Both-Devices')) === 'Both-Devices',
2249 await nameOn(child.page, shared));
2250 // …and it pushes once, so it has something it believes it last sent. This is
2251 // the state every idle device is in.
2252 await child.page.evaluate(async () => {
2253 const m = await import('/pkg/oxedyne_daimond.js');
2254 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2255 await app.create_diamond('Made-On-The-Mate');
2256 });
2257 await pushLanded(child.page, 'the mate own Diamond');
2258 await page.evaluate(() => window.DaimondSync.pull());
2259 // Quiesce the mate, so it is in the state every idle device is in: whatever
2260 // it would send, it has already sent.
2261 await child.page.evaluate(() => window.DaimondSync.push());
2262 await child.page.waitForTimeout(600);
2263 await child.page.evaluate(() => window.DaimondSync.push());
2264
2265 // (12a) The idle device. This one renames; the other has nothing to send and
2266 // never leaves the window it is in. Only the app settling fires there.
2267 await page.evaluate(async (id) => {
2268 const m = await import('/pkg/oxedyne_daimond.js');
2269 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2270 await app.rename_diamond(id, 'Renamed-Over-There');
2271 }, shared);
2272 await pushLanded(page, 'the rename for the idle device');
2273 const idleLearn = await child.page.evaluate(async () => {
2274 const v0 = window.DaimondSync.state().version;
2275 // The app settling, twice, which is all a device that is not being typed
2276 // at ever gets. Twice because the catch-up is rate-limited, and the
2277 // quiescing pushes just above have only recently spent that budget.
2278 window.dispatchEvent(new Event('daimond:idle'));
2279 await new Promise(r => setTimeout(r, 4000));
2280 window.dispatchEvent(new Event('daimond:idle'));
2281 await new Promise(r => setTimeout(r, 5500));
2282 return { v0, v1: window.DaimondSync.state().version };
2283 });
2284 check('a device with nothing to send still learns what the other one did',
2285 (await nameSettles(child.page, shared, 'Renamed-Over-There')) === 'Renamed-Over-There',
2286 'version ' + idleLearn.v0 + ' -> ' + idleLearn.v1);
2287
2288 // (12b) Both devices editing at once, pushing on every change, as seq 50's
2289 // nudge makes them. Each works on its OWN Diamond, so nothing here is a
2290 // question of whose copy wins: a merge that reconciles at all must end with
2291 // both names on both devices.
2292 const mateOwn = await child.page.evaluate(async () => {
2293 const m = await import('/pkg/oxedyne_daimond.js');
2294 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2295 return (JSON.parse(await app.list_diamonds()).find(d => d.name === 'Made-On-The-Mate') || {}).id || '';
2296 });
2297 check('the mate device owns a Diamond of its own to work on', !!mateOwn, mateOwn);
2298 for (let round = 1; round <= 3; round++) {
2299 await page.evaluate(async (arg) => {
2300 const m = await import('/pkg/oxedyne_daimond.js');
2301 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2302 await app.rename_diamond(arg.id, 'Here-' + arg.round);
2303 }, { id: shared, round });
2304 await child.page.evaluate(async (arg) => {
2305 const m = await import('/pkg/oxedyne_daimond.js');
2306 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2307 await app.rename_diamond(arg.id, 'There-' + arg.round);
2308 }, { id: mateOwn, round });
2309 // Both at once: one of them is pushing from a base the other just moved.
2310 await Promise.all([
2311 page.evaluate(() => window.DaimondSync.push()),
2312 child.page.evaluate(() => window.DaimondSync.push()),
2313 ]);
2314 await page.waitForTimeout(400);
2315 }
2316 // Let each side have its ordinary settling trigger, three times, and no more:
2317 // no reload, no focus, nothing a user would have to think of.
2318 for (let i = 0; i < 3; i++) {
2319 await Promise.all([
2320 page.evaluate(() => window.dispatchEvent(new Event('daimond:idle'))),
2321 child.page.evaluate(() => window.dispatchEvent(new Event('daimond:idle'))),
2322 ]);
2323 await page.waitForTimeout(6000);
2324 }
2325 const hereName = await nameSettles(page, shared, 'Here-3');
2326 const thereName = await nameSettles(page, mateOwn, 'There-3');
2327 const mateHere = await nameSettles(child.page, shared, 'Here-3');
2328 const mateThere = await nameSettles(child.page, mateOwn, 'There-3');
2329 check('after a storm of simultaneous pushes, this device holds both sides’ work',
2330 hereName === 'Here-3' && thereName === 'There-3', hereName + ' / ' + thereName);
2331 check('and so does the other one — the conflict path converges, both ways',
2332 mateHere === 'Here-3' && mateThere === 'There-3', mateHere + ' / ' + mateThere);
2333 const chips = await Promise.all([
2334 page.evaluate(() => { const c = document.getElementById('sync-chip'); return c ? c.dataset.state : '(none)'; }),
2335 child.page.evaluate(() => { const c = document.getElementById('sync-chip'); return c ? c.dataset.state : '(none)'; }),
2336 ]);
2337 check('and neither device is left claiming to be synced while it is stalled',
2338 chips.every(c => c !== 'stalled'), chips.join(' / '));
2339
2340 // ── (13) The gateway taps an idle device ───────────────────────────
2341 // (12a) proved the idle device catches up when the app settles. But the app
2342 // settling is still something that happened HERE, and a window sitting
2343 // unfocused on a second desk settles once and then never again: no turn ends,
2344 // nothing is renamed, the user is not in it. That window used to converge
2345 // only when somebody came back to it, which is the complaint this section
2346 // exists for. The trigger now comes from the gateway.
2347 //
2348 // Everything below runs with the second device touched in exactly one way:
2349 // reading its state. No focus, no visibility change, no settling event, no
2350 // reload. Those are counted, and the count must stay at zero.
2351 // First, what it was like without one. With the channel shut this device has
2352 // exactly the triggers it had before the channel existed -- a focus that will
2353 // not come, a turn that will not end, a push with nothing to send -- and ten
2354 // seconds go by with the other device's work sitting unread in the mailbox.
2355 // This is the live complaint, kept as a test so the channel cannot quietly
2356 // stop being the thing that answers it.
2357 await child.page.evaluate(() => window.DaimondSync.wakeVia('off'));
2358 const blindV = await child.page.evaluate(() => window.DaimondSync.state().version);
2359 await page.evaluate(async (id) => {
2360 const m = await import('/pkg/oxedyne_daimond.js');
2361 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2362 await app.rename_diamond(id, 'Unheard-Over-There');
2363 }, shared);
2364 await pushLanded(page, 'the rename the shut channel must miss');
2365 const blind = await child.page.evaluate(async (v0) => {
2366 const t0 = Date.now();
2367 while (Date.now() - t0 < 10000 && window.DaimondSync.state().version <= v0) {
2368 await new Promise(r => setTimeout(r, 200));
2369 }
2370 return { v0, v1: window.DaimondSync.state().version };
2371 }, blindV);
2372 check('WITHOUT the channel, an idle unfocused device never hears — this is the bug',
2373 blind.v1 === blind.v0 && (await nameOn(child.page, shared)) !== 'Unheard-Over-There',
2374 'version stayed at ' + blind.v1);
2375
2376 // Now with it. Same device, same conditions, one thing different.
2377 await child.page.evaluate(() => window.DaimondSync.wakeVia('ws'));
2378 await child.page.waitForFunction(
2379 () => { const w = window.DaimondSync.wake(); return w.open && (w.mode === 'ws' || w.mode === 'poll'); },
2380 null, { timeout: 15000 }).catch(() => {});
2381 // A channel that has just opened says what the version is NOW, so a device
2382 // that missed something while it was away learns about it on connecting
2383 // rather than on the next push somebody else happens to make.
2384 await child.page.waitForFunction(
2385 v0 => window.DaimondSync.state().version > v0, blind.v1, { timeout: 15000 }).catch(() => {});
2386 check('opening the channel catches the device up on what it missed while it was shut',
2387 (await nameSettles(child.page, shared, 'Unheard-Over-There')) === 'Unheard-Over-There',
2388 'version ' + blind.v1 + ' -> ' + (await child.page.evaluate(() => window.DaimondSync.state().version)));
2389 const chan = await child.page.evaluate(() => {
2390 window.__wakeProbe = { focus: 0, vis: 0, idle: 0 };
2391 window.addEventListener('focus', () => window.__wakeProbe.focus++, true);
2392 document.addEventListener('visibilitychange', () => window.__wakeProbe.vis++, true);
2393 window.addEventListener('daimond:idle', () => window.__wakeProbe.idle++, true);
2394 return { wake: window.DaimondSync.wake(), version: window.DaimondSync.state().version };
2395 });
2396 check('the idle device holds a wake channel open to the gateway',
2397 chan.wake.open === true && (chan.wake.mode === 'ws' || chan.wake.mode === 'poll'),
2398 JSON.stringify(chan.wake));
2399 check('and the two devices name different channels, so neither is woken by itself',
2400 chan.wake.id !== (await page.evaluate(() => window.DaimondSync.wake().id)),
2401 chan.wake.id);
2402
2403 // The other device renames a Diamond and pushes. Nothing at all happens on
2404 // the idle one.
2405 const selfBefore = await page.evaluate(() => window.DaimondSync.wake().wakes);
2406 await page.evaluate(async (id) => {
2407 const m = await import('/pkg/oxedyne_daimond.js');
2408 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2409 await app.rename_diamond(id, 'Woken-Over-There');
2410 }, shared);
2411 const beganWake = Date.now();
2412 await pushLanded(page, 'the rename the wake channel carries');
2413 const woke = await child.page.evaluate(async (v0) => {
2414 const t0 = Date.now();
2415 while (Date.now() - t0 < 10000 && window.DaimondSync.state().version <= v0) {
2416 await new Promise(r => setTimeout(r, 100));
2417 }
2418 return {
2419 v1: window.DaimondSync.state().version,
2420 probe: window.__wakeProbe,
2421 wake: window.DaimondSync.wake(),
2422 };
2423 }, chan.version);
2424 const wakeMs = Date.now() - beganWake;
2425 check('an idle, unfocused device applies the other one’s work with no trigger of its own',
2426 (await nameSettles(child.page, shared, 'Woken-Over-There')) === 'Woken-Over-There',
2427 'version ' + chan.version + ' -> ' + woke.v1 + ' in ' + wakeMs + 'ms');
2428 check('and it converges within five seconds of the push, not on the next thing the user does',
2429 woke.v1 > chan.version && wakeMs < 5000, wakeMs + 'ms');
2430 check('and nothing on that device caused it — no focus, no visibility change, no settling',
2431 woke.probe.focus === 0 && woke.probe.vis === 0 && woke.probe.idle === 0,
2432 JSON.stringify(woke.probe));
2433 check('and the pull was the wake channel’s doing, by its own count',
2434 woke.wake.wakes > chan.wake.wakes && woke.wake.heard >= woke.v1,
2435 JSON.stringify(woke.wake));
2436
2437 // The device that pushed is not woken by its own push: it already knows the
2438 // version it just wrote, and a channel that told it would double every round.
2439 const selfWake = await page.evaluate(() => window.DaimondSync.wake());
2440 check('and the device that pushed was not woken by its own push',
2441 selfWake.wakes === selfBefore,
2442 'wakes ' + selfBefore + ' -> ' + selfWake.wakes);
2443
2444 // (13b) The channel survives the gateway going away and coming back. A
2445 // restart is the ordinary case -- a deploy -- and a device that did not
2446 // reconnect would go quiet until its owner touched it, which is exactly the
2447 // state this whole section exists to end.
2448 //
2449 // ── ONE SIGHTING, UNEXPLAINED, 2026-08-28 ─────────────────────────
2450 // `pushLanded` below came back `api handler error` -- a plain-text 500 to
2451 // its GET of /api/sync, which threw out of `res.json()` and took the rest
2452 // of the file with it: 163 passed, and sections 13c and 14 never ran. The
2453 // store had been created eleven minutes earlier by the run before it and
2454 // was being restarted for the first time in its life.
2455 //
2456 // WHAT WAS ESTABLISHED. It did not recur: the same code answered 177/177
2457 // twice afterwards against the same store, once warm. So it is not the
2458 // catch-up added to js/sync.js that day, and it is not this section.
2459 //
2460 // WHAT WAS NOT. Whether a cold o3db can 500 a read shortly after its first
2461 // restart. The restarted gateway is spawned here with `stdio: 'ignore'`, so
2462 // the one process that could have said why wrote nowhere -- which is the
2463 // first thing to change if this is seen again. It was not chased further,
2464 // deliberately: one sighting is a sighting, and a speculative guard against
2465 // a fault nobody has characterised is how a fix grows a second defect.
2466 const restarted = await restartGateway();
2467 check('the gateway comes back after a restart', restarted === true, String(restarted));
2468 if (restarted === true) {
2469 await child.page.waitForFunction(
2470 () => window.DaimondSync.wake().open === true, null, { timeout: 60000 }).catch(() => {});
2471 const back = await child.page.evaluate(() => window.DaimondSync.wake());
2472 check('and the idle device’s wake channel comes back with it, unprompted',
2473 back.open === true, JSON.stringify(back));
2474
2475 const v2 = await child.page.evaluate(() => window.DaimondSync.state().version);
2476 await page.evaluate(async (id) => {
2477 const m = await import('/pkg/oxedyne_daimond.js');
2478 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2479 await app.rename_diamond(id, 'Woken-After-Restart');
2480 }, shared);
2481 const beganAgain = Date.now();
2482 await pushLanded(page, 'the rename after the restart');
2483 await child.page.evaluate(async (v0) => {
2484 const t0 = Date.now();
2485 while (Date.now() - t0 < 15000 && window.DaimondSync.state().version <= v0) {
2486 await new Promise(r => setTimeout(r, 100));
2487 }
2488 }, v2);
2489 check('and it is woken again on the far side of the restart',
2490 (await nameSettles(child.page, shared, 'Woken-After-Restart')) === 'Woken-After-Restart',
2491 'took ' + (Date.now() - beganAgain) + 'ms');
2492 }
2493
2494 // (13c) The plain-HTTP fallback does the same job. A front door that will not
2495 // carry a WebSocket upgrade is not a reason for an idle device to go blind:
2496 // the same wake arrives as a parked request answered early, and a completed
2497 // response wakes a throttled background tab exactly as a frame does.
2498 //
2499 // Every parked request this device makes from here on, and when each was
2500 // answered, so the second half of this section can say whether the one the
2501 // push woke was WAITING for it. Attached before the channel is switched over:
2502 // a park lasts three quarters of a minute, and a listener added later would
2503 // miss the one already open and conclude there was none.
2504 const parks = [];
2505 const above = (p) => p ? (p.url.match(/[?&]above=(\d+)/) || [])[1] : undefined;
2506 const onPark = (r) => {
2507 if (r.url().includes('/api/sync?above=')) parks.push({ r, url: r.url(), began: Date.now(), ended: 0, body: null });
2508 };
2509 const onParkDone = (r) => {
2510 const p = parks.find(q => q.r === r && !q.ended);
2511 if (!p) return;
2512 p.ended = Date.now();
2513 p.body = r.response().then(res => res.text()).catch(() => '');
2514 };
2515 child.page.on('request', onPark);
2516 child.page.on('requestfinished', onParkDone);
2517
2518 const pollOn = await child.page.evaluate(() => window.DaimondSync.wakeVia('poll'));
2519 check('the channel can be put onto parked requests instead of a socket', pollOn === 'poll', pollOn);
2520 await child.page.waitForFunction(
2521 () => window.DaimondSync.wake().open === true, null, { timeout: 15000 }).catch(() => {});
2522 const pollChan = await child.page.evaluate(() => {
2523 window.__wakeProbe = { focus: 0, vis: 0, idle: 0 };
2524 return { wake: window.DaimondSync.wake(), version: window.DaimondSync.state().version };
2525 });
2526 check('and parking one is enough to hold the channel open', pollChan.wake.open === true,
2527 JSON.stringify(pollChan.wake));
2528 await page.evaluate(async (id) => {
2529 const m = await import('/pkg/oxedyne_daimond.js');
2530 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2531 await app.rename_diamond(id, 'Woken-Without-A-Socket');
2532 }, shared);
2533 const beganPoll = Date.now();
2534 await pushLanded(page, 'the rename the parked request carries');
2535 // What the push itself cost, which is most of what the budget below spends:
2536 // `pushLanded` re-pushes and re-reads the mailbox until this device's own
2537 // parcel is what it holds, and that loop is not the wake.
2538 const landedPoll = Date.now();
2539 const polled = await child.page.evaluate(async (v0) => {
2540 const t0 = Date.now();
2541 while (Date.now() - t0 < 10000 && window.DaimondSync.state().version <= v0) {
2542 await new Promise(r => setTimeout(r, 100));
2543 }
2544 return { v1: window.DaimondSync.state().version, probe: window.__wakeProbe, wake: window.DaimondSync.wake() };
2545 }, pollChan.version);
2546 const pollMs = Date.now() - beganPoll;
2547 check('a parked request wakes the idle device just as a socket does',
2548 (await nameSettles(child.page, shared, 'Woken-Without-A-Socket')) === 'Woken-Without-A-Socket'
2549 && polled.v1 > pollChan.version,
2550 'version ' + pollChan.version + ' -> ' + polled.v1 + ' in ' + pollMs + 'ms');
2551 check('and that route converges inside five seconds too', pollMs < 5000,
2552 pollMs + 'ms, of which the push took ' + (landedPoll - beganPoll) + 'ms');
2553 check('and still with nothing happening on the device itself',
2554 polled.probe.focus === 0 && polled.probe.vis === 0 && polled.probe.idle === 0,
2555 JSON.stringify(polled.probe));
2556 // The same question the socket route above is asked, and for the same reason:
2557 // WHAT pulled? push() catches up on its own every five seconds when it has
2558 // nothing to send (IDLE_PULL_MIN_MS, sync.js), which is inside the budget the
2559 // check above allows, so the version moving is no evidence at all that the
2560 // channel did it. The channel counts the pulls it causes, and only `wakeTo`
2561 // -- a version the channel itself heard -- increments that count.
2562 //
2563 // What is asked of `heard` is that the channel heard something ABOVE what the
2564 // device held, rather than that it heard as much as the device ended up with:
2565 // a pull answers with the mailbox as it stands, which may already have moved
2566 // past the version that was announced, and that is the mailbox being busy
2567 // rather than the channel being wrong.
2568 check('and that pull was the parked channel’s doing too, by its own count',
2569 polled.wake.wakes > pollChan.wake.wakes && polled.wake.heard > pollChan.version,
2570 'wakes ' + pollChan.wake.wakes + ' -> ' + polled.wake.wakes
2571 + ', heard ' + polled.wake.heard + ' while holding ' + pollChan.version
2572 + ', pulled to ' + polled.v1);
2573
2574 // And WAS it parked? The count above cannot say, and this is the half that was
2575 // never measured -- the gateway's own "waited past" line does not appear for
2576 // this account at all. A request that reaches the gateway AFTER the push is
2577 // answered on the spot, out of the version already in the store, and it
2578 // increments the very same counter: a channel whose park had lapsed and whose
2579 // next one turned up late is indistinguishable, by the count, from one that
2580 // waited. Without this, the section is satisfied by the idle catch-up in
2581 // push() and the parked wake is assumed rather than shown.
2582 //
2583 // So the request itself is watched, from the moment it leaves the browser.
2584 // What is waited for is one the gateway is demonstrably HOLDING: still
2585 // unanswered a full second after it was made, which no answer served out of
2586 // the store on arrival ever is, and with more of its own declared wait left
2587 // than the push below can take even at its slowest. Only then does the other
2588 // device push. A request in that state, answered after the push was made and
2589 // saying the version changed, came from the branch that was waiting for it and
2590 // from nowhere else.
2591 //
2592 // Both devices are let settle first: a round still on its way would end the
2593 // park before the push could, and prove nothing either way.
2594 await quiesce(child.page, 15000);
2595 await quiesce(page, 15000);
2596 // Longer than an answer made on arrival takes -- those come back in tens of
2597 // milliseconds on this loopback, and are seen doing so in the same run.
2598 const HELD_MS = 1000;
2599 const budget = (p) => ((p.url.match(/[?&]ms=(\d+)/) || [])[1] | 0);
2600 const parked = await (async () => {
2601 const t0 = Date.now();
2602 while (Date.now() - t0 < 90000) {
2603 // The channel holds one park at a time, so the newest unanswered one is
2604 // the live one; anything older is an orphan of a torn-down loop.
2605 const open = parks.filter(q => !q.ended);
2606 const p = open.length ? open[open.length - 1] : null;
2607 // `pushLanded` gives up after 25s, so a park with more than that left
2608 // cannot run its wait out from under the push.
2609 if (p && Date.now() - p.began >= HELD_MS && p.began + budget(p) - Date.now() > 25000) return p;
2610 await sleep(200);
2611 }
2612 return null;
2613 })();
2614 check('the gateway is HOLDING a request of the idle device’s own, unanswered',
2615 !!parked, parked
2616 ? parked.url.replace(/^.*\/api/, '/api') + ', open ' + (Date.now() - parked.began)
2617 + 'ms of its ' + budget(parked) + 'ms wait'
2618 : 'no request of the channel’s own was left open long enough to be woken, in 90s');
2619
2620 await page.evaluate(async (id) => {
2621 const m = await import('/pkg/oxedyne_daimond.js');
2622 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
2623 await app.rename_diamond(id, 'Woken-While-Parked');
2624 }, shared);
2625 const parkPush = Date.now();
2626 await pushLanded(page, 'the rename made while the other device had a request parked');
2627 const parkLanded = Date.now();
2628 for (let i = 0; i < 150 && parked && !parked.ended; i++) await sleep(100);
2629 child.page.off('request', onPark);
2630 child.page.off('requestfinished', onParkDone);
2631 let answer = null;
2632 try { answer = parked && parked.body ? JSON.parse(await parked.body) : null; }
2633 catch (e) { answer = null; }
2634 check('and the push wakes THAT request, rather than leaving it to run its wait out',
2635 !!answer && answer.waited === true && answer.changed === true
2636 && (answer.version | 0) > (above(parked) | 0)
2637 && parked.began < parkPush && parked.ended > parkPush,
2638 parked
2639 ? 'held on ' + above(parked) + ' for ' + (parked.ended - parked.began)
2640 + 'ms of its ' + budget(parked) + 'ms wait, answered ' + (parked.ended - parkPush)
2641 + 'ms after the push was made (which landed after ' + (parkLanded - parkPush)
2642 + 'ms): ' + JSON.stringify(answer)
2643 : 'nothing was parked to wake');
2644 const applied = await (async () => {
2645 const name = await nameSettles(child.page, shared, 'Woken-While-Parked');
2646 const after = await child.page.evaluate(() => ({
2647 version: window.DaimondSync.state().version,
2648 probe: window.__wakeProbe,
2649 wake: window.DaimondSync.wake(),
2650 }));
2651 return { name, ...after };
2652 })();
2653 check('and the device applies what that answer told it, still with nothing happening on it',
2654 !!parked && applied.name === 'Woken-While-Parked' && applied.version > (above(parked) | 0)
2655 && applied.probe.focus === 0 && applied.probe.vis === 0 && applied.probe.idle === 0
2656 && applied.wake.wakes > polled.wake.wakes,
2657 'version ' + (parked ? above(parked) : '?') + ' -> ' + applied.version + ', wakes '
2658 + polled.wake.wakes + ' -> ' + applied.wake.wakes + ', ' + JSON.stringify(applied.probe));
2659
2660 // The channel carries version integers and nothing else. What the gateway
2661 // parks on and answers with is read here directly, so a future change that
2662 // smuggled content down it would be caught rather than assumed against.
2663 //
2664 // The second half of this asks what a wait NOTHING MOVED UNDER answers, so
2665 // the premise has to be established rather than assumed. Both are needed and
2666 // neither was there: the mate had just pulled, and a pull schedules a push of
2667 // whatever this device adds over the pulled base -- so a parcel was still on
2668 // its way while the probe parked. And `above` was read from THIS device's
2669 // cursor, which the mate's round had already left behind, so the probe was
2670 // parking over a change that had happened rather than waiting for one that
2671 // had not. It reported `changed: true`, correctly, and the check called it an
2672 // invention.
2673 await quiesce(child.page, 15000);
2674 await quiesce(page, 15000);
2675 const bare = await page.evaluate(async () => {
2676 // The version the GATEWAY holds this instant, not this device's belief
2677 // about it.
2678 const r0 = await fetch('/api/sync', { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
2679 const v = ((await r0.json()).version) | 0;
2680 const r = await fetch('/api/sync?above=' + v + '&ms=1000&w=wkprobe',
2681 { credentials: 'same-origin', headers: { 'x-daimond-api': '1' } });
2682 return { status: r.status, json: await r.json(), above: v };
2683 });
2684 check('a wake answer carries a version and nothing else — no blob, no device, no account',
2685 bare.status === 200 && bare.json.waited === true
2686 && Object.keys(bare.json).sort().join(',') === 'changed,ok,version,waited',
2687 JSON.stringify(bare.json).slice(0, 160));
2688 check('and a wait that nothing moved under says so rather than inventing a change',
2689 bare.json.changed === false, 'parked above ' + bare.above + ' — ' + JSON.stringify(bare.json));
2690
2691 // The gateway is deliberately restarted above, so a wake socket that was open
2692 // across it reports a failed connection while it is down. That is the reconnect
2693 // working, not a fault, and it is the only WebSocket noise this run may make.
2694 const wakeNoise = /WebSocket connection to '[^']*\/api\/sync\/ws/;
2695 const cerrs = child.errs.filter(e => !/favicon|ERR_|Failed to load resource|401|402|409|426|502|Unauthorized/.test(e) && !wakeNoise.test(e));
2696 check('no unexpected console errors on the second device', cerrs.length === 0,
2697 cerrs.slice(0, 3).join(' | '));
2698
2699 const errs = s.errs.filter(e => !/favicon|ERR_|Failed to load resource|401|402|409|426|502|Unauthorized/.test(e) && !wakeNoise.test(e));
2700 check('no unexpected console errors', errs.length === 0, errs.slice(0, 3).join(' | '));
2701} catch (e) {
2702 check('verify_sync ran without throwing', false, String(e && e.message || e));
2703} finally {
2704 await child?.close?.().catch?.(() => {});
2705 await s.close?.().catch?.(() => {});
2706}
2707
2708console.log('\n' + (bad.length ? `FAIL: ${bad.length} failed, ${ok.length} passed` : `ok: all ${ok.length} passed`));
2709process.exit(bad.length ? 1 : 0);