Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/peer.test.mjs

120 KiB, 22 runs

created by r2519314175:1415, 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/* ============================================================
2 Test — the persistent desktop peer, STEP 1: prove the seam.
3 ------------------------------------------------------------
4 Drives the REAL www/js/identity.js, www/js/post.js and
5 www/js/peer.js in two independent simulated tabs of ONE account
6 (PHONE and LAPTOP), plus a THIRD tab of a DIFFERENT account
7 (STRANGER), through the whole errand seam of dev/PEER_DESIGN.md
8 step 7.1:
9
10 A (PHONE) seals a minimal errand envelope to its OWN account
11 and drops it in an in-memory post box (the `{to,addr,envelope}`
12 shape `send` posts). B (LAPTOP), the same account, collects it,
13 routes by the sealed `t` tag, runs the turn (a MOCK LLM), folds
14 the answer into the transcript as an append, and pushes the
15 parcel; it also posts a `done` report. A pulls the parcel and
16 the answer is merged in by the ordinary append-only union
17 (mergeMessages, daimond.js:997) -- no peer-specific merge.
18
19 And the property step 1 exists to prove:
20
21 the errand opens ONLY for the same account. STRANGER, a
22 different identity with a different sealing key, CANNOT open
23 the sealed envelope -- `DaimondPost.unseal` refuses it.
24
25 No gateway is involved: the post box and the parcel store are
26 in-memory stand-ins for `/api/post` and `/api/sync`, and the
27 seal, the open and the fold are the real client code.
28
29 Run: node www/js/peer.test.mjs
30 (Node 20+, whose WebCrypto implements X25519 and Ed25519 -- the
31 real engine the seal and the identity run on.)
32 ============================================================ */
33import { readFileSync } from 'node:fs';
34import { fileURLToPath } from 'node:url';
35import { dirname, join } from 'node:path';
36import { webcrypto } from 'node:crypto';
37
38const HERE = dirname(fileURLToPath(import.meta.url));
39const real = webcrypto;
40let failures = 0;
41function check(name, cond) {
42 if (cond) { console.log(' ok ' + name); }
43 else { console.log(' FAIL ' + name); failures++; }
44}
45const toHex = (bytes) => {
46 const b = new Uint8Array(bytes);
47 let s = '';
48 for (let i = 0; i < b.length; i++) s += ('0' + b[i].toString(16)).slice(-2);
49 return s;
50};
51const encU8 = (s) => new TextEncoder().encode(s);
52const b64Bytes = (u8) => Buffer.from(u8).toString('base64');
53const httpResp = (obj) => ({ status: 200, json: async () => obj });
54const eqBytes = (a, b) => {
55 const x = new Uint8Array(a), y = new Uint8Array(b);
56 if (x.length !== y.length) return false;
57 for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
58 return true;
59};
60
61// ── One simulated tab ──────────────────────────────────────
62//
63// A Map-backed localStorage, a no-op document/window, the encoders and base64,
64// and the real WebCrypto. Each context loads the four app scripts as the classic
65// IIFEs they are and attaches their globals onto its own `window`, so two
66// contexts are two independent devices with independent storage.
67function makeTab() {
68 const store = new Map();
69 const localStorage = {
70 getItem: (k) => (store.has(k) ? store.get(k) : null),
71 setItem: (k, v) => store.set(k, String(v)),
72 removeItem: (k) => store.delete(k),
73 };
74 const win = {};
75 win.addEventListener = () => {};
76 win.dispatchEvent = () => true;
77 win.matchMedia = () => ({ matches: false, addListener: () => {}, addEventListener: () => {} });
78 const noEl = {
79 addEventListener: () => {}, appendChild: () => {}, setAttribute: () => {},
80 querySelector: () => null, querySelectorAll: () => [], remove: () => {},
81 style: {}, classList: { add: () => {}, remove: () => {}, toggle: () => {} },
82 };
83 const document = {
84 readyState: 'complete',
85 addEventListener: () => {},
86 querySelector: () => null,
87 querySelectorAll: () => [],
88 getElementById: () => null,
89 createElement: () => Object.assign({}, noEl),
90 body: noEl,
91 };
92 const btoa = (s) => Buffer.from(s, 'binary').toString('base64');
93 const atob = (s) => Buffer.from(s, 'base64').toString('binary');
94 function EventShim(t) { this.type = t; }
95
96 function loadScript(rel, extra) {
97 let body = readFileSync(join(HERE, rel), 'utf8');
98 if (extra) body += extra;
99 // A REAL browser makes `window` the global object, so the app scripts refer
100 // to their siblings by a bare `DaimondIdentity` / `DaimondPost` after a
101 // `window.X &&` guard. `new Function` gives them no such global, so free
102 // identifiers are resolved against this tab's `window` with an enclosing
103 // `with(window)` -- the one construct that puts an object in the scope chain.
104 // Host built-ins the scripts also read bare (crypto, btoa, TextEncoder, ...)
105 // are NOT properties of `window`, so they fall through `with` to the named
106 // parameters below. The outer function is non-strict (no directive), which
107 // is what makes `with` legal; each app script keeps its own inner 'use strict'.
108 const fn = new Function(
109 'window', 'document', 'crypto', 'localStorage', 'btoa', 'atob',
110 'TextEncoder', 'TextDecoder', 'Event',
111 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
112 'console', 'globalThis',
113 'with (window) {\n' + body + '\n}');
114 fn(win, document, real, localStorage, btoa, atob,
115 TextEncoder, TextDecoder, EventShim,
116 setTimeout, clearTimeout, setInterval, clearInterval,
117 console, globalThis);
118 }
119 // The vendored bundle's top-level `var DaimondNoble` is wrapper-local here (a
120 // browser turns it into a window property), so publish it explicitly, exactly
121 // as curvefallback.test.mjs does.
122 loadScript('vendor/noble-curves.min.js', '\n;window.DaimondNoble = DaimondNoble;');
123 loadScript('curvefallback.js');
124 loadScript('identity.js');
125 loadScript('post.js');
126 loadScript('peer.js');
127 return win;
128}
129
130// ── The in-memory transports ───────────────────────────────
131
132/// The post box: `/api/post` reduced to its safety-relevant shape. A row is
133/// `{ seq, kind:'post', to, addr, envelope }`; a re-post of the same address
134/// collapses to one row (address is content, post.js). `collect(since)` returns
135/// rows above a watermark, which is all the peer path needs of it here.
136function makePostBox() {
137 let seq = 0;
138 const rows = [];
139 return {
140 post(body) {
141 if (rows.some((r) => r.addr === body.addr)) return { ok: true, addr: body.addr };
142 seq += 1;
143 rows.push({ seq, kind: 'post', to: body.to, addr: body.addr, envelope: body.envelope });
144 return { ok: true, addr: body.addr };
145 },
146 collect(since) { return rows.filter((r) => r.seq > (since | 0)); },
147 top() { return seq; },
148 };
149}
150
151/// The parcel store: `/api/sync` reduced to one versioned blob of chats, and the
152/// append-only union that opens it on the far side. The union is a faithful port
153/// of daimond.js `mergeMessages` (:997) and `mergeInto` (:1794) -- union by mid,
154/// fuller copy wins, time order -- because THAT is the merge an errand result
155/// rides home on, and step 1 is proving it rides home with no new rule.
156function makeParcelStore() {
157 let version = 0;
158 let chats = [];
159 return {
160 push(next) { version += 1; chats = JSON.parse(JSON.stringify(next)); return version; },
161 pull() { return { version, chats: JSON.parse(JSON.stringify(chats)) }; },
162 version() { return version; },
163 };
164}
165function unionMessages(a, b) {
166 const at = {}, out = [];
167 (a || []).concat(b || []).forEach((m) => {
168 const had = at[m.mid];
169 if (had === undefined) { at[m.mid] = out.length; out.push(m); return; }
170 if ((out[had].elided || 0) && !(m.elided || 0)) out[had] = m; // fuller wins
171 });
172 out.sort((x, y) => {
173 if ((x.ts || 0) !== (y.ts || 0)) return (x.ts || 0) - (y.ts || 0);
174 return String(x.mid).localeCompare(String(y.mid));
175 });
176 return out;
177}
178function mergeInto(base, incoming) {
179 const byId = {};
180 (base || []).forEach((c) => { if (c && c.id) byId[c.id] = c; });
181 (incoming || []).forEach((c) => {
182 if (!c || !c.id) return;
183 const st = byId[c.id];
184 if (!st) { byId[c.id] = c; return; }
185 const fresh = (c.updatedAt || 0) > (st.updatedAt || 0) ? c : st;
186 const merged = Object.assign({}, fresh);
187 merged.messages = unionMessages(st.messages, c.messages);
188 byId[c.id] = merged;
189 });
190 return Object.keys(byId).map((id) => byId[id]);
191}
192
193async function main() {
194 const PASS_A = 'correct horse battery staple frigate';
195 const PASS_S = 'a wholly different eight word passphrase indeed today';
196
197 const box = makePostBox();
198 const parcel = makeParcelStore();
199
200 // ── Set-up: two tabs of one account, one tab of another ────
201 console.log('Set-up — three tabs, two accounts');
202 const phone = makeTab();
203 const laptop = makeTab();
204 const stranger = makeTab();
205
206 await phone.DaimondIdentity.create('Phone', PASS_A);
207 check('phone account created and holds a sealing key',
208 !!phone.DaimondIdentity.sealingKeyRaw()
209 && phone.DaimondIdentity.sealingKeyRaw().length === 32);
210
211 // LAPTOP becomes the SAME account by adopting the bundle, then unlocking.
212 const bundle = phone.DaimondIdentity.exportBundle();
213 check('laptop adopts the account bundle', laptop.DaimondIdentity.importBundle(bundle));
214 const un = await laptop.DaimondIdentity.unlock(PASS_A);
215 check('laptop unlocks the shared account', !!un && un.ok === true && laptop.DaimondIdentity.isUnlocked());
216 check('laptop and phone hold the SAME sealing key (same account)',
217 eqBytes(phone.DaimondIdentity.sealingKeyRaw(), laptop.DaimondIdentity.sealingKeyRaw()));
218
219 // STRANGER is a different identity entirely.
220 await stranger.DaimondIdentity.create('Stranger', PASS_S);
221 check('stranger is a DIFFERENT account (different sealing key)',
222 !eqBytes(phone.DaimondIdentity.sealingKeyRaw(), stranger.DaimondIdentity.sealingKeyRaw()));
223
224 // ── The PER-DEVICE id: distinct even on paired devices ─────
225 //
226 // Pairing copies the account keypair whole, so publicKeyB64url() is IDENTICAL on
227 // phone and laptop -- it CANNOT be the device id. The peer keys holder/
228 // dispatchedBy/presence on deviceId(), which is minted per device and never
229 // travels in the bundle, so the two devices are distinguishable. On the pre-fix
230 // code (device id === publicKeyB64url) the distinctness check below fails.
231 console.log('\nDevice id — paired devices SHARE the account key but hold DISTINCT device ids');
232 check('paired phone and laptop share the account public key (the trap)',
233 phone.DaimondIdentity.publicKeyB64url() === laptop.DaimondIdentity.publicKeyB64url());
234 // Defensive access: on the pre-fix code deviceId() does not exist, so these read
235 // null and the checks FAIL cleanly (rather than crashing the run) -- which is the
236 // evidence that the distinctness the peer needs is absent before the fix.
237 const devId = (tab) => (tab.DaimondIdentity.deviceId ? tab.DaimondIdentity.deviceId() : null);
238 const phoneDev = devId(phone);
239 const laptopDev = devId(laptop);
240 check('phone and laptop mint DISTINCT device ids despite the shared account key',
241 !!phoneDev && !!laptopDev && phoneDev !== laptopDev);
242 check('a device id is NOT the account public key (would collide across paired devices)',
243 !!phoneDev && phoneDev !== phone.DaimondIdentity.publicKeyB64url());
244 check('deviceId() is stable per device (a second read returns the same id)',
245 !!phoneDev && devId(phone) === phoneDev);
246
247 // The two consequences the shared-id bug caused, proven on the two REAL device
248 // ids. Both checks pass now and FAIL if the device id collapses back to the
249 // account key (phoneDev === laptopDev).
250 const NOWD = 1700000000000;
251 const Pd = phone.DaimondPeer;
252 // (1) PRESENCE: a peer must see the OTHER device. With one shared id the only
253 // entry is self, freshestPeer self-excludes it, and auto-dispatch is dead.
254 const presenceBoth = {
255 [phoneDev]: { name: 'phone', lastSeen: NOWD - 500 },
256 [laptopDev]: { name: 'laptop', lastSeen: NOWD - 200 },
257 };
258 const seen = Pd.freshestPeer(presenceBoth, phoneDev, NOWD);
259 check('presence lists the OTHER device (freshestPeer returns the laptop, not self)',
260 !!seen && seen.deviceId === laptopDev);
261 check('a shared id would self-exclude (proof the bug killed auto-dispatch)',
262 Pd.freshestPeer({ [phoneDev]: { name: 'me', lastSeen: NOWD } }, phoneDev, NOWD) === null);
263 // (2) THE LEASE (the money risk): the foreign-holder test must FIRE between two
264 // distinct ids, and MUST NOT when holder === self -- the same-holder blind spot
265 // that let both paired devices run and bill the same turn.
266 const dispTurn = { why: 'dispatched', iturn: 'turn-money' };
267 const leaseByLaptop = { turnId: 'turn-money', holder: laptopDev, mode: 'running', expiry: NOWD + 60000, renewedAt: NOWD };
268 const leaseBySelf = { turnId: 'turn-money', holder: phoneDev, mode: 'running', expiry: NOWD + 60000, renewedAt: NOWD };
269 check('foreign-holder detection FIRES: a lease held by the laptop reads peer-held on the phone',
270 Pd.dispatchState(dispTurn, leaseByLaptop, phoneDev, NOWD) === 'peer-held');
271 check('same-holder is the money bug: a lease whose holder EQUALS self is NOT peer-held',
272 Pd.dispatchState(dispTurn, leaseBySelf, phoneDev, NOWD) === 'reclaimable');
273 // And the CAS itself: two DISTINCT device ids racing one turn from one base -> one
274 // wins, one stands down. With a shared holder id the merge could not tell them
275 // apart and both would keep their claim.
276 {
277 const L = phone.DaimondLease;
278 L.forget();
279 const cas = makeCas({});
280 const snap1 = await cas.read();
281 const snap2 = await cas.read();
282 const r1 = await L.takeFrom(snap1, 'turn-money', { holder: phoneDev, eid: 'e1' }, cas, () => NOWD);
283 const r2 = await L.takeFrom(snap2, 'turn-money', { holder: laptopDev, eid: 'e2' }, cas, () => NOWD + 1);
284 check('two DISTINCT device ids race one turn: exactly one holds it (no double-bill)',
285 (r1.won ? 1 : 0) + (r2.won ? 1 : 0) === 1);
286 check('the committed lease names exactly one of the two distinct device ids',
287 [phoneDev, laptopDev].includes(cas.peekLeases()['turn-money'].holder));
288 L.forget();
289 }
290
291 // ── The phone dispatches ───────────────────────────────────
292 console.log('\nDispatch — phone seals an errand to itself and posts it');
293
294 // Persist-first: the prompt is pushed to the parcel BEFORE the errand, so a
295 // peer can never claim an errand whose prompt it cannot yet read (§4.1).
296 const chat0 = {
297 id: 'chat-1', name: 'Arithmetic',
298 messages: [{ mid: 'm-user-1', role: 'user', content: 'What is 2+2?', ts: 1000 }],
299 updatedAt: 1000,
300 };
301 const vPrompt = parcel.push([chat0]);
302 check('prompt parcel pushed before the errand', vPrompt === 1);
303
304 const model = { provider: 'openrouter', model: 'test/model', url: 'https://openrouter.ai/api/v1/chat/completions' };
305 const errand = phone.DaimondPeer.makeErrand({
306 turnId: 'turn-1', chatId: 'chat-1', prompt: 'What is 2+2?',
307 model, parcelVersion: vPrompt, dispatchedBy: 'phone-device',
308 });
309 const sealed = await phone.DaimondPeer.sealForSelf(errand);
310 check('errand sealed to a {to, addr, envelope} post body',
311 !!sealed.to && !!sealed.addr && !!sealed.envelope);
312 const phonePubHex = toHex(await phone.DaimondIdentity.publicKeyRaw());
313 const phonePubB64url = phone.DaimondIdentity.publicKeyB64url();
314 // The delivery address is the BASE64URL account form the gateway binds an account
315 // to -- NOT the hex of the raw key. A hex `to` matched no account and every post
316 // 404'd ("No account holds that key"), which is the whole reason a dispatch never
317 // arrived. These two checks (b64url form, and NOT the old hex) fail on that code.
318 check('errand `to` is the account\'s b64url public address (the form the gateway binds)',
319 sealed.to === phonePubB64url);
320 check('errand `to` is NOT the hex of the raw key (the 404 bug)',
321 sealed.to !== phonePubHex && phonePubB64url !== phonePubHex);
322 box.post(sealed);
323 check('post box holds exactly one row after the post', box.top() === 1);
324
325 // ── Same-account-ONLY: the negative that matters ───────────
326 console.log('\nSeal — the errand opens for the account and for NOBODY else');
327 // The stranger must be UNLOCKED and holding its own private sealing key, or a
328 // refusal here would prove nothing -- a locked device fails to open everything.
329 check('stranger is unlocked with its own key (so the refusal is meaningful)',
330 stranger.DaimondIdentity.isUnlocked());
331 let strangerOpened = false, strangerWhy = '';
332 try { await stranger.DaimondPeer.openEnvelope(sealed.envelope); strangerOpened = true; }
333 catch (e) { strangerOpened = false; strangerWhy = String(e && e.message || e); }
334 check('a DIFFERENT account CANNOT open the sealed errand', strangerOpened === false);
335 check('the refusal is the crypto "not for you", not an incidental error',
336 /not sealed/i.test(strangerWhy));
337
338 // ── The laptop collects, claims-nothing (step 1), runs, pushes ─
339 console.log('\nPeer — laptop collects, runs the turn, folds and pushes');
340 let ranErrand = null, pushedVersion = 0;
341 const rows = box.collect(0);
342 const tally = await laptop.DaimondPeer.routeRows(rows, {
343 // A row the laptop cannot open should never happen in this run; if it does,
344 // name why, so a regression reads as a sentence rather than a silent zero.
345 onOther: (row, e) => { console.log(' note: a row did not open —', e && e.message); },
346 onErrand: async (err) => {
347 ranErrand = err;
348 check('laptop opened the errand to the same fields the phone sealed',
349 err.turnId === 'turn-1' && err.chatId === 'chat-1'
350 && err.prompt === 'What is 2+2?' && err.model && err.model.provider === 'openrouter'
351 && err.parcelVersion === vPrompt);
352
353 // Reconstruct: pull the parcel to >= the errand's version, find the chat.
354 const snap = parcel.pull();
355 check('laptop pulled the parcel to at least the errand version',
356 snap.version >= err.parcelVersion);
357 const chat = snap.chats.find((c) => c.id === err.chatId);
358 check('laptop reconstructed the chat carrying the prompt',
359 !!chat && chat.messages.length === 1 && chat.messages[0].role === 'user');
360
361 // Run the turn — a MOCK LLM stands in for runTurn's provider call.
362 const answer = mockRunTurn(err.prompt);
363 laptop.DaimondPeer.foldAssistant(chat, {
364 mid: 'm-asst-1', turnId: err.turnId, text: answer, model: err.model, ts: 2000,
365 });
366 // Push the parcel under the ordinary path; the assistant message is a
367 // pure append.
368 pushedVersion = parcel.push(snap.chats);
369
370 // Post the report — the nudge, not the answer.
371 const report = laptop.DaimondPeer.makeReport({
372 eid: err.eid, turnId: err.turnId, chatId: err.chatId,
373 status: 'done', parcelVersion: pushedVersion,
374 });
375 const sealedRep = await laptop.DaimondPeer.sealForSelf(report);
376 box.post(sealedRep);
377 },
378 });
379 check('laptop routed exactly one errand', tally.errands === 1);
380 check('the turn ran and produced an answer', !!ranErrand && pushedVersion === 2);
381
382 // ── The phone returns and collects ─────────────────────────
383 console.log('\nReturn — phone pulls the parcel and the answer is merged in');
384
385 // The phone's local view is still the prompt-only chat it dispatched.
386 const phoneLocal = [JSON.parse(JSON.stringify(chat0))];
387 const snap = parcel.pull();
388 const merged = mergeInto(phoneLocal, snap.chats);
389 const mchat = merged.find((c) => c.id === 'chat-1');
390 check('merged chat carries BOTH the prompt and the answer', !!mchat && mchat.messages.length === 2);
391 const asst = mchat.messages.find((m) => m.role === 'assistant');
392 check('the assistant answer is the peer\'s, folded under the turn id',
393 !!asst && asst.content === mockRunTurn('What is 2+2?') && asst.iturn === 'turn-1');
394 check('the user prompt survived the merge unchanged',
395 mchat.messages.some((m) => m.role === 'user' && m.content === 'What is 2+2?'));
396
397 // Idempotency: pulling and merging AGAIN duplicates nothing (union by mid).
398 const merged2 = mergeInto(merged, parcel.pull().chats);
399 const mchat2 = merged2.find((c) => c.id === 'chat-1');
400 check('a second pull-and-merge duplicates nothing', mchat2.messages.length === 2);
401
402 // The phone collects the report from the box.
403 let sawReport = null;
404 await phone.DaimondPeer.routeRows(box.collect(0), {
405 onReport: async (rep) => { sawReport = rep; },
406 });
407 check('phone collected the done report for its turn',
408 !!sawReport && sawReport.status === 'done' && sawReport.turnId === 'turn-1'
409 && sawReport.parcelVersion === 2);
410
411 // The phone can of course also open its OWN errand (its self-slot); it just
412 // must not act on its own dispatch. Proven here so the same-account property
413 // is symmetric: both devices of the account open it, nobody else does.
414 let phoneOpenedOwn = false;
415 try { const e = await phone.DaimondPeer.openEnvelope(sealed.envelope); phoneOpenedOwn = e.turnId === 'turn-1'; }
416 catch (e) { phoneOpenedOwn = false; }
417 check('phone opens its OWN errand too (the account\'s self-slot)', phoneOpenedOwn === true);
418
419 // ══════════════════════════════════════════════════════════
420 // STEP 2 — signing, the raw poster, and the collector.
421 // ══════════════════════════════════════════════════════════
422
423 // ── Signing: the account's own opens AND verifies; a forgery does not ──
424 console.log('\nSigning — the account\'s own verifies, a correspondent\'s forgery is refused');
425
426 const ownOpened = await phone.DaimondPeer.openEnvelope(sealed.envelope);
427 check('the opened own errand carries the account\'s author and a signature',
428 !!ownOpened && ownOpened.author === phonePubHex && typeof ownOpened.sig === 'string' && ownOpened.sig.length > 0);
429 check('phone opens its own errand and it VERIFIES',
430 await phone.DaimondPeer.verifyEnvelope(ownOpened));
431
432 // A tampered envelope: change a signed field after the fact -> verify fails.
433 const tampered = Object.assign({}, ownOpened, { prompt: 'spend all the money' });
434 check('a tampered errand FAILS verification', (await phone.DaimondPeer.verifyEnvelope(tampered)) === false);
435
436 // THE FORGERY THAT THE SEAL ALONE DOES NOT STOP. The stranger knows the
437 // account's PUBLIC sealing key (it is on the card), so it seals a forged errand
438 // TO the account -- which the account can OPEN. Only the signature stops it: the
439 // stranger cannot sign as the account.
440 const forgedBase = stranger.DaimondPeer.makeErrand({
441 turnId: 'forged-1', chatId: 'chat-1', prompt: 'transfer the credits',
442 });
443 const forgedSigned = await stranger.DaimondPeer.signEnvelope(forgedBase); // signed by STRANGER
444 const forgedSealed = await stranger.DaimondPost.seal(
445 [phone.DaimondIdentity.sealingKeyRaw()], encU8(JSON.stringify(forgedSigned))); // sealed TO phone
446 const forgedEnv = b64Bytes(forgedSealed);
447
448 let phoneOpenedForgery = false, forgeryWhy = '';
449 try { await phone.DaimondPeer.openEnvelope(forgedEnv); phoneOpenedForgery = true; }
450 catch (e) { phoneOpenedForgery = false; forgeryWhy = String(e && e.message || e); }
451 check('phone can OPEN the forgery (it was sealed to the account\'s key)',
452 (await phone.DaimondPeer.peek(forgedEnv)) !== null);
453 check('phone REFUSES the forgery (it was not signed by the account)', phoneOpenedForgery === false);
454 check('the refusal is the signature check, not something incidental',
455 /not signed by this account/i.test(forgeryWhy));
456 // And through the collector door: absorb verifies and DROPS, never routes.
457 const forgedPeek = await phone.DaimondPeer.peek(forgedEnv);
458 const absorbed = await phone.DaimondPeer.absorb(forgedPeek, { addr: 'x' });
459 check('the collector ABSORB drops the forgery (verified:false, routed:false)',
460 absorbed.verified === false && absorbed.routed === false);
461
462 // ── AAD domain separation, and the DPY1 legacy read path ────
463 console.log('\nSeal — the purpose (AAD) domain-separates the account key, and a legacy DPY1 still opens');
464 const withMagic = (tag, body) => { const o = new Uint8Array(tag.length + body.length); o.set(tag, 0); o.set(body, tag.length); return o; };
465 const DPY1 = new Uint8Array([0x44, 0x50, 0x59, 0x31]);
466 const DPY2 = new Uint8Array([0x44, 0x50, 0x59, 0x32]);
467
468 // A body sealed under one purpose opens only under that same purpose.
469 const aad1 = await phone.DaimondIdentity.wrapBytesAad(encU8('coordination'), 'daimond/test/one');
470 const round = await phone.DaimondIdentity.unwrapBytesAad(aad1, 'daimond/test/one');
471 check('wrapBytesAad round-trips under the SAME purpose', new TextDecoder().decode(round) === 'coordination');
472 let crossAad = false, noAad = false;
473 try { await phone.DaimondIdentity.unwrapBytesAad(aad1, 'daimond/test/two'); } catch (e) { crossAad = true; }
474 try { await phone.DaimondIdentity.unwrapBytes(aad1); } catch (e) { noAad = true; }
475 check('a DIFFERENT purpose CANNOT open it (crypto-layer domain separation)', crossAad === true);
476 check('a no-AAD open of an AAD body also fails (parcel/voice vs peer separation)', noAad === true);
477
478 // A DPY2 envelope sealed under the WRONG purpose is refused by the peer open path.
479 const legacyErr = await phone.DaimondPeer.signEnvelope(phone.DaimondPeer.makeErrand({ turnId: 'legacy-1', chatId: 'c', prompt: 'hi' }));
480 const legacyPlain = encU8(JSON.stringify(legacyErr));
481 const dpy2WrongAad = withMagic(DPY2, await phone.DaimondIdentity.wrapBytesAad(legacyPlain, 'not/the/peer/purpose'));
482 let dpy2WrongFailed = false;
483 try { await phone.DaimondPeer.openEnvelope(b64Bytes(dpy2WrongAad)); } catch (e) { dpy2WrongFailed = true; }
484 check('a DPY2 body under the WRONG purpose is refused by openEnvelope', dpy2WrongFailed === true);
485
486 // A legacy DPY1 envelope (same key, NO AAD) still opens and verifies — the rollout read path.
487 const dpy1Env = withMagic(DPY1, await phone.DaimondIdentity.wrapBytes(legacyPlain));
488 const dpy1Opened = await phone.DaimondPeer.openEnvelope(b64Bytes(dpy1Env));
489 check('a legacy DPY1 envelope (no AAD) still opens and verifies (rollout read path)',
490 !!dpy1Opened && dpy1Opened.turnId === 'legacy-1');
491 // And a paired sibling opens the same DPY1, since the key is the account's.
492 const dpy1Sibling = await laptop.DaimondPeer.openEnvelope(b64Bytes(dpy1Env));
493 check('a paired sibling opens the legacy DPY1 too (shared account key)',
494 !!dpy1Sibling && dpy1Sibling.turnId === 'legacy-1');
495
496 // ── The raw poster builds the correct body ─────────────────
497 console.log('\nPoster — DaimondPost.post(body) builds {to,addr,envelope} with the own address');
498 let lastPostBody = null;
499 const relay2 = (() => {
500 let seq = 0; const rows = [];
501 return {
502 put(b) { if (rows.some((r) => r.addr === b.addr)) return; seq++; rows.push({ seq, kind: 'post', to: b.to, addr: b.addr, envelope: b.envelope }); },
503 since(s) { return rows.filter((r) => r.seq > (s | 0)); },
504 };
505 })();
506 function wireGateway(win) {
507 win.DaimondGateway = {
508 clientApi: () => 1,
509 gwFetch: async (path, opts) => {
510 if (opts.method === 'POST') {
511 lastPostBody = JSON.parse(opts.body);
512 relay2.put(lastPostBody);
513 return httpResp({ ok: true });
514 }
515 const m = /[?&]since=(\d+)/.exec(String(path));
516 return httpResp({ ok: true, rows: relay2.since(m ? (m[1] | 0) : 0), more: false });
517 },
518 };
519 }
520 wireGateway(phone);
521
522 const errandBody = await phone.DaimondPeer.sealForSelf(
523 phone.DaimondPeer.makeErrand({ turnId: 'turn-2', chatId: 'chat-1', prompt: 'again?' }));
524 const putRes = await phone.DaimondPost.post(errandBody);
525 check('the raw put reports ok', putRes.ok === true && putRes.status === 200);
526 check('the posted body is exactly {to, addr, envelope}',
527 !!lastPostBody && Object.keys(lastPostBody).sort().join(',') === 'addr,envelope,to');
528 check('the posted `to` is the account\'s OWN b64url public address', lastPostBody.to === phonePubB64url);
529 check('the posted addr and envelope are the sealed artefact\'s',
530 lastPostBody.addr === errandBody.addr && lastPostBody.envelope === errandBody.envelope);
531 check('a put with a missing field is refused before any call',
532 (await phone.DaimondPost.post({ to: 'x', addr: 'y' })).ok === false);
533
534 // ── The collector routes errand/report BEFORE the message read ─
535 console.log('\nCollector — real collect() routes errand & report, passes a non-peer row through');
536 const routedErrands = [], routedReports = [];
537 phone.DaimondPeer.onErrand(async (e) => { routedErrands.push(e); });
538 phone.DaimondPeer.onReport(async (r) => { routedReports.push(r); });
539
540 // A report, posted through the raw put.
541 const reportBody = await phone.DaimondPeer.sealForSelf(
542 phone.DaimondPeer.makeReport({ eid: 'e2', turnId: 'turn-2', chatId: 'chat-1', status: 'done', parcelVersion: 2 }));
543 await phone.DaimondPost.post(reportBody);
544
545 // A NON-peer sealed row: plain JSON with no peer tag, sealed to self. It must
546 // peek to null and reach the message path (which, with no wasm bridge in this
547 // harness, records a "bad" message -- proving the row was NOT swallowed by the
548 // peer route).
549 const nonPeerSealed = await phone.DaimondPost.seal(
550 [phone.DaimondIdentity.sealingKeyRaw()], encU8(JSON.stringify({ kind: 'post', hello: 'not a peer envelope' })));
551 relay2.put({ to: phonePubHex, addr: 'nonpeer-' + Date.now(), envelope: b64Bytes(nonPeerSealed) });
552
553 check('a non-peer sealed row peeks to null (falls through to messages)',
554 (await phone.DaimondPeer.peek(b64Bytes(nonPeerSealed))) === null);
555
556 const col = await phone.DaimondPost.collect();
557 check('collect() succeeded', col.ok === true);
558 check('the errand was routed to the peer runner', routedErrands.some((e) => e.turnId === 'turn-2'));
559 check('the report was routed to the peer runner', routedReports.some((r) => r.turnId === 'turn-2'));
560 // The crisp proof that the peer route did not touch the message list: collect's
561 // own tally counts NO message stored from the errand and report rows, and the
562 // non-peer row reached the message path (recorded unreadable, no bridge here).
563 check('NO message was stored from the errand/report rows (got === 0)', col.got === 0);
564 check('the non-peer row passed THROUGH to the message path (unreadable === 1)', col.unreadable === 1);
565
566 // ══════════════════════════════════════════════════════════
567 // STEP 3 — the lease (money-critical). All against DaimondLease,
568 // the REAL take-if-vacant merge and lifecycle, over a CAS stub
569 // that models the gateway's compare-and-set exactly.
570 // ══════════════════════════════════════════════════════════
571 console.log('\nLease — two devices race one turn; exactly one wins');
572 const L = phone.DaimondLease; // the real implementation under test
573
574 await runLeaseAcceptance(L, check);
575
576 // ══════════════════════════════════════════════════════════
577 // STEP 4 — the dispatcher: the STRICT ORDER and the full errand.
578 // buildDispatch is pure; the test drives its order end to end.
579 // ══════════════════════════════════════════════════════════
580 console.log('\nDispatcher — buildDispatch fixes the order and the whole errand');
581 const T0 = 1700000000000; // a realistic epoch-ms
582 const dchat = { id: 'chat-9', provider: 'openrouter', model: 'test/m', holds: ['/a', '/b'] };
583 const plan = phone.DaimondPeer.buildDispatch(dchat, {
584 turnId: 'turn-9', prompt: 'do the thing', pause: { paused: ['x'] },
585 scope: dchat.holds, // daimond.js resolves scope (scopeChatTo / holds) and passes it
586 dispatchedBy: 'devPHONE', now: T0,
587 });
588 check('the order is push-prompt -> mark-dispatched -> post-errand',
589 plan.order.join(',') === 'push-prompt,mark-dispatched,post-errand');
590 check('the mark is the dispatched reason on the turn',
591 plan.mark.why === 'dispatched' && plan.mark.iturn === 'turn-9' && plan.mark.interrupted === true);
592 check('the deadline defaults to ~15 minutes out',
593 plan.fields.deadline === T0 + phone.DaimondPeer.DISPATCH_DEADLINE_MS);
594
595 // Drive the order end to end: push the prompt parcel FIRST (capturing the
596 // version), then post the errand carrying it -- exactly what daimond.js's thin
597 // wiring does. The sequence is recorded and must equal plan.order.
598 let ver = 41;
599 const fakeSync = { push: async () => { ver += 1; }, version: () => ver };
600 const seq = [];
601 await fakeSync.push(); seq.push('push-prompt');
602 const pv = fakeSync.version();
603 seq.push('mark-dispatched'); // daimond.js marks the local turn here
604 const errand9 = plan.errand(pv);
605 const body9 = await phone.DaimondPeer.sealForSelf(errand9);
606 const before9 = relay2.since(0).length;
607 await phone.DaimondPost.post(body9); seq.push('post-errand');
608
609 check('the executed sequence matches the planned order', seq.join(',') === plan.order.join(','));
610 check('the errand carries the version the prompt push committed at', errand9.parcelVersion === pv && pv === 42);
611 check('the errand was posted only AFTER the prompt push (never before)',
612 seq.indexOf('post-errand') > seq.indexOf('push-prompt'));
613 check('the post box grew by exactly the one errand', relay2.since(0).length === before9 + 1);
614
615 // The full envelope survives seal+sign+open, cross-device (laptop opens it).
616 const posted9 = relay2.since(before9).find((r) => r.addr === body9.addr);
617 const opened9 = await laptop.DaimondPeer.openEnvelope(posted9.envelope);
618 check('the dispatched errand opens on the peer with the whole envelope intact',
619 opened9.turnId === 'turn-9' && opened9.chatId === 'chat-9'
620 && opened9.prompt === 'do the thing'
621 && opened9.model.provider === 'openrouter' && opened9.model.model === 'test/m'
622 && Array.isArray(opened9.scope) && opened9.scope.join(',') === '/a,/b'
623 && opened9.pause && opened9.pause.paused.join(',') === 'x'
624 && opened9.parcelVersion === pv
625 && opened9.deadline === T0 + phone.DaimondPeer.DISPATCH_DEADLINE_MS
626 && opened9.dispatchedBy === 'devPHONE');
627
628 // ── The why:'dispatched' handling: dispatchState against the lease ──
629 console.log('\nDispatched turn — dispatchState classifies it against the lease');
630 const P = phone.DaimondPeer;
631 const dTurn = { why: 'dispatched', iturn: 'turn-9' };
632 const liveForeign = { turnId: 'turn-9', holder: 'devLAPTOP', mode: 'running', expiry: T0 + 60000, renewedAt: T0 };
633 const liveOwn = { turnId: 'turn-9', holder: 'devPHONE', mode: 'running', expiry: T0 + 60000, renewedAt: T0 };
634 const expired = { turnId: 'turn-9', holder: 'devLAPTOP', mode: 'running', expiry: T0 - 1, renewedAt: T0 };
635 check('a dispatched turn under a live FOREIGN lease is peer-held',
636 P.dispatchState(dTurn, liveForeign, 'devPHONE', T0) === 'peer-held');
637 check('a dispatched turn under our OWN lease is reclaimable',
638 P.dispatchState(dTurn, liveOwn, 'devPHONE', T0) === 'reclaimable');
639 check('a dispatched turn under an EXPIRED lease is reclaimable',
640 P.dispatchState(dTurn, expired, 'devPHONE', T0) === 'reclaimable');
641 check('a dispatched turn with NO lease is reclaimable',
642 P.dispatchState(dTurn, null, 'devPHONE', T0) === 'reclaimable');
643 check('an ordinary interrupted turn is not-dispatched',
644 P.dispatchState({ why: 'offline' }, liveForeign, 'devPHONE', T0) === 'not-dispatched');
645
646 // ══════════════════════════════════════════════════════════
647 // STEP 5 — the runner: take -> run -> push -> report -> release,
648 // the syncCas adapter, the revoke->abort path, ack-after-commit.
649 // ══════════════════════════════════════════════════════════
650 console.log('\nRunner — the errand runs end to end, and aborts on revoke');
651 await runRunnerAcceptance(phone.DaimondPeer, phone.DaimondLease, check);
652
653 // ══════════════════════════════════════════════════════════
654 // STEP 5b — THE RENEW HEARTBEAT is bounded: it stops on every
655 // exit and cannot outlive its turn. This is the fix for the
656 // permanent 409 push-loop -- a lease that renewed for ever
657 // rewrote the parcel every 30s, so it was never a fixed point.
658 // ══════════════════════════════════════════════════════════
659 console.log('\nHeartbeat — the renew ticker is owned by runErrand and can never renew for ever');
660 await runHeartbeatContainment(phone.DaimondPeer, phone.DaimondLease, check);
661
662 // ══════════════════════════════════════════════════════════
663 // STEP 5c — THE >LEASE_TTL_MS DOUBLE-RUN, closed. A busy turn
664 // cannot propagate a 30s renew (the push is suppressed over a
665 // live turn), so a TTL-capped lease read EXPIRED on other
666 // devices after 90s while the turn ran on -- and the phone's
667 // recovery re-ran and re-billed it. The lease is now claimed to
668 // the errand DEADLINE, so it stays live for the whole turn with
669 // no renew, and exactly one device ever runs and bills it.
670 // ══════════════════════════════════════════════════════════
671 console.log('\nDeadline lease — a >TTL turn cannot be double-run (claim expires at the deadline, no renew)');
672 await runDeadlineExpiryMoneySafety(phone.DaimondPeer, phone.DaimondLease, check);
673
674 // ══════════════════════════════════════════════════════════
675 // STEP 6 — the UI state machine (pure). daimond.js only renders
676 // what uiState decides; here every §5 state is asserted.
677 // ══════════════════════════════════════════════════════════
678 console.log('\nUI state — uiState classifies a dispatched turn through its life');
679 const U = phone.DaimondPeer;
680 const S = 1700000000000;
681 const dt = { why: 'dispatched', iturn: 'turn-u', deadline: S + U.DISPATCH_DEADLINE_MS };
682 const claimed = { turnId: 'turn-u', holder: 'devLAP', mode: 'claimed', expiry: S + 60000, renewedAt: S };
683 const running = { turnId: 'turn-u', holder: 'devLAP', mode: 'running', expiry: S + 60000, renewedAt: S };
684 const uExpired = { turnId: 'turn-u', holder: 'devLAP', mode: 'running', expiry: S - 1, renewedAt: S };
685 const released = { turnId: 'turn-u', holder: 'devLAP', mode: 'released', expiry: 0, renewedAt: S };
686 const repDone = { t: 'report', turnId: 'turn-u', status: 'done' };
687 const repFail = { t: 'report', turnId: 'turn-u', status: 'refused-spend' };
688
689 check('dispatched, no lease yet -> "dispatched"',
690 U.uiState(dt, null, null, 'devPHONE', S) === 'dispatched');
691 check('dispatched, deadline passed, no lease -> "no-peer-awake"',
692 U.uiState(dt, null, null, 'devPHONE', S + U.DISPATCH_DEADLINE_MS + 1) === 'no-peer-awake');
693 check('a live claimed lease -> "claimed"',
694 U.uiState(dt, claimed, null, 'devPHONE', S) === 'claimed');
695 check('a live running lease -> "running"',
696 U.uiState(dt, running, null, 'devPHONE', S) === 'running');
697 check('a done report -> "done" (outlives the lease)',
698 U.uiState(dt, released, repDone, 'devPHONE', S) === 'done');
699 check('a failure report -> "failed"',
700 U.uiState(dt, running, repFail, 'devPHONE', S) === 'failed');
701 check('a lease taken then EXPIRED with no report -> "failed" (peer stopped)',
702 U.uiState(dt, uExpired, null, 'devPHONE', S) === 'failed');
703 check('an ordinary (non-dispatched) turn -> "not-dispatched"',
704 U.uiState({ why: 'offline' }, running, null, 'devPHONE', S) === 'not-dispatched');
705 // The thin lease-record lookup the guards/renderer use.
706 phone.DaimondLease.forget();
707 check('DaimondLease.record is null for an unknown turn',
708 phone.DaimondLease.record('nope') === null);
709
710 // ══════════════════════════════════════════════════════════
711 // STEP 7 — presence beat (freshest-scalar) + smart auto-dispatch.
712 // ══════════════════════════════════════════════════════════
713 console.log('\nPresence + auto-dispatch — awake peers, and when to hand off');
714 await runPresenceAcceptance(phone.DaimondPeer, phone.DaimondPresence, check);
715
716 // ══════════════════════════════════════════════════════════
717 // MONEY-SAFETY REGRESSIONS — the four defects live two-context QA
718 // found (dev/PEER_DESIGN.md §2, §3.3, §4). Each check fails on the
719 // pre-fix code and passes on the fix.
720 // ══════════════════════════════════════════════════════════
721 await runMoneySafety(phone, laptop, check);
722
723 // ══════════════════════════════════════════════════════════
724 // ORPHAN RECOVERY — the shipped "sent to your other devices, then
725 // nothing" failure. A phone dispatches a turn no peer runs and comes
726 // back to nothing. The fix rescues it on return, THROUGH the same lease,
727 // so a peer that also claims never causes a second run or a second
728 // charge. These checks fail on the pre-fix code and pass on the fix.
729 // ══════════════════════════════════════════════════════════
730 await runRecoveryAcceptance(phone.DaimondPeer, phone.DaimondLease, check);
731
732 // ══════════════════════════════════════════════════════════
733 // THE NOMINATED RUNNER — a claim guard that defers to one device
734 // when it is FRESHLY awake, without ever stranding a turn: an
735 // offline or stale nominee is no barrier, and the stand-down is
736 // re-decided against live presence rather than being permanent.
737 // ══════════════════════════════════════════════════════════
738 await runNominationAcceptance(phone.DaimondPeer, phone.DaimondLease, check);
739
740 // ══════════════════════════════════════════════════════════
741 // THE FALLBACK LIVENESS GLUE — the HOLD (post.js takeRow) that
742 // keeps a stood-down errand on the relay, the scheduled re-collect
743 // that drives it, and the RE-ARM on a transient collect failure so
744 // the driver is restored rather than dropped. Drives the REAL
745 // post.js takeRow and peer.js runErrand; the daimond.js scheduler is
746 // modelled faithfully (null-first, await, re-arm on !ok) on a hand
747 // -driven timer, since daimond.js does not load under node.
748 // ══════════════════════════════════════════════════════════
749 await runFallbackLivenessAcceptance(laptop, check);
750
751 // ══════════════════════════════════════════════════════════
752 // REMOTE CONSENT FOR A HANDED-OFF TURN — the two envelopes, the
753 // exact-act binding, the forged/replayed-grant defence, PARK ->
754 // terminal at MAX_PARKS with the lease freed not stranded, the
755 // GLOBAL two-device parkCount bound, policy composition, and
756 // attended-only routing. (dev/HANDOFF_CONSENT_DESIGN.md.)
757 // ══════════════════════════════════════════════════════════
758 await runRemoteConsentAcceptance(phone, laptop, stranger, check);
759
760 console.log(failures === 0 ? '\nALL PASS' : ('\n' + failures + ' FAILURE(S)'));
761 if (failures) process.exitCode = 1;
762}
763
764// The REALISTIC leases CAS: unlike makeLeaseSync (a clean compare-and-set), this
765// models sync.js's push() + daimond.js's peerSyncShim faithfully -- on a 409 it
766// PULLS the winner's lease in, MERGES it (take-if-vacant drops our own claim) and
767// RETRIES, and it reports success by the VERSION ADVANCING. That advance is NOT
768// proof our claim landed: through this path a losing racer's version moves too. A
769// take that trusts `ok` alone lets BOTH racers believe they won -- the double
770// charge. `leaseTakeFrom` must re-read and confirm the section still names it.
771function makeRealisticSync(L, gw, NOW) {
772 let localVersion = gw.version();
773 let view = {};
774 return {
775 version: () => localVersion,
776 leases: () => JSON.parse(JSON.stringify(view)),
777 commit: async (base, proposed) => {
778 if (localVersion !== base) return { ok: false, version: localVersion, leases: JSON.parse(JSON.stringify(view)) };
779 view = JSON.parse(JSON.stringify(proposed)); // install
780 for (let a = 0; a < 4; a++) {
781 const r = gw.push(localVersion, view);
782 if (r.status === 200) { localVersion = r.version; break; } // clean commit
783 localVersion = r.version; // 409: pull + merge + retry
784 view = L.merge(view, r.leases, NOW);
785 }
786 if (localVersion > base) return { ok: true, version: localVersion };
787 return { ok: false, version: localVersion, leases: JSON.parse(JSON.stringify(view)) };
788 },
789 };
790}
791
792async function runRecoveryAcceptance(P, L, check) {
793 const NOW = 1700000000000;
794
795 // ── ADVERSARIAL, the money crux: a phone RECOVERS-LOCAL while a PEER also
796 // claims, from the SAME base version, through the REALISTIC pull-merge-retry
797 // commit. Exactly one may hold the lease -- else exactly one double-charge. ──
798 {
799 console.log('\nRecovery — adversarial: recover-local vs a peer claim, realistic sync');
800 L.forget();
801 let gwV = 5, gwL = {};
802 const gw = {
803 version: () => gwV,
804 leases: () => JSON.parse(JSON.stringify(gwL)),
805 push: (base, next) => {
806 if (base !== gwV) return { status: 409, version: gwV, leases: JSON.parse(JSON.stringify(gwL)) };
807 gwV += 1; gwL = JSON.parse(JSON.stringify(next));
808 return { status: 200, version: gwV };
809 },
810 };
811 const phoneCas = P.syncCas(makeRealisticSync(L, gw, NOW));
812 const peerCas = P.syncCas(makeRealisticSync(L, gw, NOW));
813 const snapPhone = await phoneCas.read(); // both read the SAME base 5
814 const snapPeer = await peerCas.read();
815 // The phone's local recovery take (allowSelf on the runner; here the take is
816 // what matters) and the peer's take race from that one base version.
817 const rPhone = await L.takeFrom(snapPhone, 'turn-adv', { holder: 'PHONE', eid: 'e', deadline: 0 }, phoneCas, () => NOW);
818 const rPeer = await L.takeFrom(snapPeer, 'turn-adv', { holder: 'PEER', eid: 'e', deadline: 0 }, peerCas, () => NOW + 1);
819 check('recover-vs-peer: EXACTLY ONE take wins (no double-charge) through the realistic commit',
820 (rPhone.won ? 1 : 0) + (rPeer.won ? 1 : 0) === 1);
821 check('recover-vs-peer: the loser stood down and was told the true holder',
822 rPeer.won === false && rPeer.holder === 'PHONE');
823 check('recover-vs-peer: the gateway names exactly one holder',
824 !!gw.leases()['turn-adv'] && gw.leases()['turn-adv'].holder === 'PHONE');
825 }
826
827 // ── An ORPHAN is rescued locally, exactly once, and acked so no peer re-runs. ──
828 {
829 console.log('\nRecovery — an orphaned dispatched turn runs locally on return');
830 L.forget();
831 const sync = makeLeaseSync({});
832 let ran = 0, acked = 0, pushed = 0, reported = 0;
833 const errand = P.makeErrand({ turnId: 't-orphan', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
834 const res = await P.runErrand(errand, {
835 selfId: 'PHONE', cas: P.syncCas(sync), allowSelf: true,
836 finished: async () => false,
837 reconstruct: async () => ({ chat: {}, app: {} }),
838 runTurn: async () => { ran++; },
839 abort: () => {}, pushResult: async () => { pushed++; return 1; },
840 post: async () => { reported++; }, ack: async () => { acked++; }, now: () => NOW,
841 });
842 check('orphan recovery RUNS the turn locally exactly once', res.ran === true && res.done === true && ran === 1);
843 check('orphan recovery ACKS the relay errand (so no peer re-collects and re-runs)', acked === 1);
844 check('orphan recovery pushes the answer and posts a done report', pushed === 1 && reported === 1);
845 check('orphan recovery released the lease when done', sync.leases()['t-orphan'].mode === 'released');
846 }
847
848 // ── Recovery STANDS DOWN when a peer holds a LIVE lease -- no take-over, no
849 // double run -- both by the pure decision and by the runner's own take. ──
850 {
851 console.log('\nRecovery — stands down when a peer is genuinely on the turn');
852 L.forget();
853 const now = NOW;
854 const live = { 't-held': { turnId: 't-held', eid: 'e', holder: 'PEER', mode: 'running', expiry: now + L.LEASE_TTL_MS, renewedAt: now } };
855 check('recoverDecision: FALSE under a live foreign lease (leave it to the peer)',
856 P.recoverDecision({ why: 'dispatched', iturn: 't-held' }, live['t-held'], false, 'PHONE', now) === false);
857 check('recoverDecision: TRUE when vacant and unfinished (rescue the orphan)',
858 P.recoverDecision({ why: 'dispatched', iturn: 'x' }, null, false, 'PHONE', now) === true);
859 check('recoverDecision: FALSE when the turn is already finished (a peer answered)',
860 P.recoverDecision({ why: 'dispatched', iturn: 'x' }, null, true, 'PHONE', now) === false);
861 check('recoverDecision: FALSE for a turn that was never dispatched',
862 P.recoverDecision({ why: 'offline', iturn: 'x' }, null, false, 'PHONE', now) === false);
863 // And the runner itself stands down on the take, even asked to recover.
864 const sync = makeLeaseSync(live);
865 let ran = 0, touched = false;
866 const errand = P.makeErrand({ turnId: 't-held', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
867 const res = await P.runErrand(errand, {
868 selfId: 'PHONE', cas: P.syncCas(sync), allowSelf: true,
869 finished: async () => false,
870 reconstruct: async () => { touched = true; return {}; },
871 runTurn: async () => { ran++; },
872 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => now,
873 });
874 check('recovery runner STANDS DOWN on a live foreign lease (never runs)', res.ran === false && ran === 0 && touched === false);
875 }
876
877 // ── A turn a peer ALREADY finished is not re-run by recovery (D1b belt-and-braces
878 // for the release-then-recollect window, where the lease reads vacant). ──
879 {
880 console.log('\nRecovery — never re-runs a turn a peer already completed');
881 L.forget();
882 const sync = makeLeaseSync({}); // lease vacant (peer released after done)
883 let ran = 0;
884 const errand = P.makeErrand({ turnId: 't-fin', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
885 const res = await P.runErrand(errand, {
886 selfId: 'PHONE', cas: P.syncCas(sync), allowSelf: true,
887 finished: async () => true, // a done report / merged answer exists
888 reconstruct: async () => { ran = -99; return {}; },
889 runTurn: async () => { ran++; },
890 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => NOW,
891 });
892 check('recovery on an ALREADY-FINISHED turn stands down (no second charge)',
893 res.ran === false && res.why === 'already-done' && ran === 0);
894 }
895
896 // ── The AUTOMATIC collect path still refuses this device's OWN errand (D1a),
897 // so an incidental re-collect on return never runs; only deliberate recovery
898 // (allowSelf) does. ──
899 {
900 console.log('\nRecovery — the automatic path still refuses a self-dispatch (D1a preserved)');
901 L.forget();
902 const sync = makeLeaseSync({});
903 let ran = 0;
904 const errand = P.makeErrand({ turnId: 't-self', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
905 const res = await P.runErrand(errand, { // allowSelf omitted -> false
906 selfId: 'PHONE', cas: P.syncCas(sync),
907 finished: async () => false,
908 reconstruct: async () => { ran = -99; return {}; },
909 runTurn: async () => { ran++; },
910 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => NOW,
911 });
912 check('automatic path refuses this device\'s OWN errand (D1a intact)',
913 res.ran === false && res.why === 'self-dispatched' && ran === 0);
914 }
915}
916
917// The NOMINATED runner. The account may name ONE always-on device; a non-nominee
918// stands down for it, but ONLY while it is genuinely, freshly awake -- an offline
919// or stale nominee is no barrier, and the stand-down is re-decided against live
920// presence so a turn is never stranded. The lease is still the single-runner
921// arbiter, so the nomination only moves WHO attempts the claim.
922async function runNominationAcceptance(P, L, check) {
923 const NOW = 1700000000000;
924 const W = P.DISPATCH_FRESH_MS; // the freshness the guard reuses
925 const NOMINEE = 'aaaa0000bbbb1111'; // 16-hex, the roster's id shape
926 const OTHER = 'cccc2222dddd3333';
927 const freshNom = { [NOMINEE]: { name: 'desktop', lastSeen: NOW - 1000 } };
928 const staleNom = { [NOMINEE]: { name: 'desktop', lastSeen: NOW - (W + 60000) } };
929
930 console.log('\nNomination — the always-on-runner claim guard');
931
932 // ── The pure decision, the crux of the guard. ──
933 check('(a) the NOMINEE never stands down for its own nomination -> it claims',
934 P.nominationStandDown(NOMINEE, NOMINEE, freshNom, NOW, W) === false);
935 check('(b) a non-nominee STANDS DOWN for a freshly-awake nominee',
936 P.nominationStandDown(NOMINEE, OTHER, freshNom, NOW, W) === true);
937 check('(c) a non-nominee CLAIMS when the nominee is offline (absent from presence)',
938 P.nominationStandDown(NOMINEE, OTHER, {}, NOW, W) === false);
939 check('(d) NO nomination -> first-come unchanged (never stands down)',
940 P.nominationStandDown('', OTHER, freshNom, NOW, W) === false);
941 // (e) STALE-PRESENCE stall defence: a lagging map showing a slept nominee as
942 // awake must NOT make a fallback stand down for a device that is gone.
943 check('(e) a non-nominee does NOT stand down for a STALE nominee',
944 P.nominationStandDown(NOMINEE, OTHER, staleNom, NOW, W) === false);
945 check('(e) freshness edge: at the window stands down, one ms past claims',
946 P.nominationStandDown(NOMINEE, OTHER, { [NOMINEE]: { name: 'd', lastSeen: NOW - W } }, NOW, W) === true
947 && P.nominationStandDown(NOMINEE, OTHER, { [NOMINEE]: { name: 'd', lastSeen: NOW - W - 1 } }, NOW, W) === false);
948 // (f) NOT PERMANENT: the SAME beat that read fresh at T reads stale at T+W+1, so
949 // a fallback that stood down re-decides and claims -- the turn is never stranded.
950 {
951 const nom = { [NOMINEE]: { name: 'desktop', lastSeen: NOW } };
952 check('(f) stands down at T while the nominee is fresh',
953 P.nominationStandDown(NOMINEE, OTHER, nom, NOW, W) === true);
954 check('(f) NOT permanent: re-decided past the window, the fallback CLAIMS',
955 P.nominationStandDown(NOMINEE, OTHER, nom, NOW + W + 1, W) === false);
956 }
957
958 // ── End to end through runErrand: the real CLAIM decision, not a smoke test. ──
959 // A non-nominee with a freshly-awake nominee stands down BEFORE the lease take:
960 // it never reconstructs, never runs, never touches the lease, and answers
961 // why:'nominee' -- the signal takeRow reads to HOLD the errand on the relay.
962 {
963 L.forget();
964 const sync = makeLeaseSync({});
965 let ran = 0, touched = false;
966 const errand = P.makeErrand({ turnId: 't-nom-b', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
967 const res = await P.runErrand(errand, {
968 selfId: OTHER, cas: P.syncCas(sync),
969 nominatedId: NOMINEE, presence: freshNom, freshWindowMs: W,
970 finished: async () => false,
971 reconstruct: async () => { touched = true; return {}; },
972 runTurn: async () => { ran++; },
973 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => NOW,
974 });
975 check('runErrand: a non-nominee STANDS DOWN for a fresh nominee (no run, lease untouched)',
976 res.ran === false && res.why === 'nominee' && ran === 0 && touched === false && !sync.leases()['t-nom-b']);
977 }
978 // The NOMINEE runs its own errand: never stands down, so it takes the lease.
979 {
980 L.forget();
981 const sync = makeLeaseSync({});
982 let ran = 0;
983 const errand = P.makeErrand({ turnId: 't-nom-a', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
984 const res = await P.runErrand(errand, {
985 selfId: NOMINEE, cas: P.syncCas(sync),
986 nominatedId: NOMINEE, presence: freshNom, freshWindowMs: W,
987 finished: async () => false,
988 reconstruct: async () => ({ chat: {}, app: {} }),
989 runTurn: async () => { ran++; },
990 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => NOW,
991 });
992 check('runErrand: the NOMINEE claims and runs its errand exactly once',
993 res.ran === true && ran === 1 && sync.leases()['t-nom-a'].holder === NOMINEE);
994 }
995 // A non-nominee with the nominee OFFLINE claims and runs (fallback = any awake).
996 {
997 L.forget();
998 const sync = makeLeaseSync({});
999 let ran = 0;
1000 const errand = P.makeErrand({ turnId: 't-nom-c', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
1001 const res = await P.runErrand(errand, {
1002 selfId: OTHER, cas: P.syncCas(sync),
1003 nominatedId: NOMINEE, presence: {}, freshWindowMs: W,
1004 finished: async () => false,
1005 reconstruct: async () => ({ chat: {}, app: {} }),
1006 runTurn: async () => { ran++; },
1007 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => NOW,
1008 });
1009 check('runErrand: a non-nominee CLAIMS when the nominee is offline (fallback runs)',
1010 res.ran === true && ran === 1 && sync.leases()['t-nom-c'].holder === OTHER);
1011 }
1012 // A non-nominee with the nominee STALE claims and runs -- the (e) stall defence,
1013 // proven through the runner and not only the pure decision.
1014 {
1015 L.forget();
1016 const sync = makeLeaseSync({});
1017 let ran = 0;
1018 const errand = P.makeErrand({ turnId: 't-nom-s', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
1019 const res = await P.runErrand(errand, {
1020 selfId: OTHER, cas: P.syncCas(sync),
1021 nominatedId: NOMINEE, presence: staleNom, freshWindowMs: W,
1022 finished: async () => false,
1023 reconstruct: async () => ({ chat: {}, app: {} }),
1024 runTurn: async () => { ran++; },
1025 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => NOW,
1026 });
1027 check('runErrand: a non-nominee CLAIMS when the nominee is STALE (no stall)',
1028 res.ran === true && ran === 1 && sync.leases()['t-nom-s'].holder === OTHER);
1029 }
1030 // No nomination -> unchanged first-come: a non-nominee claims and runs.
1031 {
1032 L.forget();
1033 const sync = makeLeaseSync({});
1034 let ran = 0;
1035 const errand = P.makeErrand({ turnId: 't-nom-d', chatId: 'c', prompt: 'p', eid: 'e', deadline: 0, dispatchedBy: 'PHONE' });
1036 const res = await P.runErrand(errand, {
1037 selfId: OTHER, cas: P.syncCas(sync),
1038 nominatedId: '', presence: freshNom, freshWindowMs: W,
1039 finished: async () => false,
1040 reconstruct: async () => ({ chat: {}, app: {} }),
1041 runTurn: async () => { ran++; },
1042 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {}, now: () => NOW,
1043 });
1044 check('runErrand: NO nomination -> first-come unchanged (non-nominee claims)',
1045 res.ran === true && ran === 1 && sync.leases()['t-nom-d'].holder === OTHER);
1046 }
1047}
1048
1049// The LIVENESS GLUE that keeps a nominee stand-down from stranding the turn. Three
1050// real pieces meet here and were only asserted by construction before:
1051// - post.js takeRow returns HOLD on a `why:'nominee'` stand-down, so the errand
1052// stays on the relay (not acked) for the nominee -- driven through the REAL
1053// DaimondPost.take door and the REAL DaimondPeer.absorb/runErrand;
1054// - a scheduled re-collect (daimond.js scheduleNomineeFallback) re-decides against
1055// LIVE presence, so once the nominee's beat ages out the fallback CLAIMS;
1056// - that scheduler RE-ARMS on a transient collect failure, so an offline blip
1057// restores the driver instead of dropping the only prompt re-collect.
1058// daimond.js does not load under node (a large, DOM-bound IIFE), so the scheduler is
1059// reproduced here line-for-line -- null the handle first, await the collect, re-arm
1060// on !ok -- on a hand-driven timer; the HOLD, the routing and the claim are all real.
1061async function runFallbackLivenessAcceptance(tab, check) {
1062 console.log('\nFallback liveness — HOLD -> scheduled re-collect -> claim, and re-arm on a failed tick');
1063 const P = tab.DaimondPeer, L = tab.DaimondLease, Post = tab.DaimondPost;
1064 const W = P.DISPATCH_FRESH_MS;
1065 const NOMINEE = 'aaaa0000bbbb1111'; // the always-on runner (asleep after one beat)
1066 const selfId = tab.DaimondIdentity.deviceId(); // this tab is the awake FALLBACK
1067
1068 // One scenario, built fresh so it owns its box, lease and clock. `mode` flips the
1069 // collect driver between a real run and a transient failure (the offline blip).
1070 async function scenario() {
1071 const box = makePostBox();
1072 const sync = makeLeaseSync({});
1073 L.forget();
1074 let clock = 1700000000000;
1075 const nomineeSeen = clock; // the nominee's one and only beat
1076 const presence = { [NOMINEE]: { name: 'desktop', lastSeen: nomineeSeen } };
1077 let ran = 0;
1078
1079 // The gateway's OWN view of the nominee's last beat: the source of truth the
1080 // collect path refreshes against, distinct from the local `presence` snapshot,
1081 // which can lag. `gatewayFresh()` models the nominee having beaten just now (awake);
1082 // `refreshMode==='fail'` models a refresh that fails or hangs (offline, hung gateway),
1083 // which the shipped code bounds and falls through to the local snapshot from.
1084 let gatewayLastSeen = nomineeSeen;
1085 let refreshMode = 'ok';
1086 async function refreshPresence() {
1087 if (refreshMode === 'fail') throw new Error('refresh failed');
1088 presence[NOMINEE] = { name: 'desktop', lastSeen: gatewayLastSeen };
1089 }
1090
1091 // Stub for presenceTick: the shipped onErrand beats presence the instant a turn
1092 // ends (Fix A), so a just-run device asserts liveness for the next turn rather than
1093 // waiting on its throttled 45s timer. Counted so the test proves the beat fires.
1094 let beats = 0;
1095 function beat() { beats++; }
1096
1097 // The REAL runner deps, carrying the nomination and the live presence/clock.
1098 function deps() {
1099 return {
1100 selfId, cas: P.syncCas(sync),
1101 nominatedId: NOMINEE, presence: presence, freshWindowMs: W, now: () => clock,
1102 finished: async () => false,
1103 reconstruct: async () => ({ chat: {}, app: {} }),
1104 runTurn: async () => { ran++; },
1105 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => {},
1106 };
1107 }
1108
1109 // The daimond.js glue, reproduced: the onErrand handler runs the errand and, on
1110 // a nominee stand-down, arms the fallback; the scheduler nulls its handle first,
1111 // awaits the collect, and re-arms on failure. A hand-driven timer stands in for
1112 // setTimeout so ticks are deterministic.
1113 const timer = makeFakeTimer();
1114 let pending = null, arms = 0, mode = 'ok';
1115 function scheduleNomineeFallback() {
1116 if (pending) return; // the guard: never two live timers
1117 arms++;
1118 pending = timer.set(async () => {
1119 pending = null; // null FIRST, so a route's re-arm takes the slot
1120 let res = null;
1121 try { res = await driveCollect(); } catch (e) { res = null; }
1122 if (!res || !res.ok) scheduleNomineeFallback(); // re-arm on a failed tick
1123 }, W + 5000);
1124 }
1125 // A collect that runs each un-acked relay row through the REAL takeRow (Post.take),
1126 // so the HOLD, the routing and the claim are the shipping code. `mode==='fail'`
1127 // models a transient GET failure that routes nothing -- the offline blip.
1128 async function driveCollect() {
1129 if (mode === 'fail') return { ok: false, why: 'status_0' };
1130 let hold = false;
1131 for (const row of box.collect(0)) {
1132 const r = await Post.take(row);
1133 if (r && r.hold) hold = true;
1134 }
1135 return { ok: true, hold: hold };
1136 }
1137
1138 P.onErrand(async (errand) => {
1139 // Mirror of the shipped onErrand handler (daimond.js): refresh live presence
1140 // before the stand-down reads the snapshot, fail-open. Shipped bounds it with
1141 // Promise.race(4s); a timeout resolves the same way a caught failure does --
1142 // proceed on the local snapshot -- which `refreshMode==='fail'` models here.
1143 try { await refreshPresence(); } catch (e) { /* local snapshot stands */ }
1144 const res = await P.runErrand(errand, deps());
1145 if (res && res.ran) { try { beat(); } catch (e) {} } // mirror of shipped Fix A: beat on a completed run
1146 if (res && res.why === 'nominee') scheduleNomineeFallback();
1147 return res;
1148 });
1149
1150 // Seal an errand to this account and drop it on the relay.
1151 const errand = P.makeErrand({ turnId: 'turn-live', chatId: 'c', prompt: 'p', model: {}, deadline: 0, dispatchedBy: 'phone-device' });
1152 const sealed = await P.sealForSelf(errand);
1153 box.post(sealed);
1154
1155 return {
1156 box, sync, timer,
1157 ranCount: () => ran,
1158 armCount: () => arms,
1159 pendingLive: () => timer.live() > 0,
1160 setMode: (m) => { mode = m; },
1161 age: (ms) => { clock = nomineeSeen + ms; }, // move the clock relative to the beat
1162 collect: () => driveCollect(), // the wake that first delivers the errand
1163 fire: async () => { // fire every live timer callback, in order
1164 const live = timer.handles.filter((h) => h.live);
1165 live.forEach((h) => { h.live = false; });
1166 for (const h of live) await h.fn();
1167 },
1168 gatewayFresh: () => { gatewayLastSeen = clock; }, // the nominee actually beat just now
1169 setRefreshMode: (m) => { refreshMode = m; }, // 'ok' | 'fail'
1170 beatCount: () => beats, // presence beats fired on a completed run
1171 };
1172 }
1173
1174 // ── (g) HOLD -> scheduled re-collect -> claim once the nominee ages out. ──
1175 {
1176 const s = await scenario();
1177 // The wake: the fallback collects, stands down for the fresh nominee, and the
1178 // errand is HELD on the relay (real takeRow) with the fallback armed.
1179 const first = await s.collect();
1180 check('(g) the fallback HOLDs the errand for a fresh nominee (real takeRow)',
1181 first.ok === true && first.hold === true);
1182 check('(g) standing down ran nothing and armed the re-collect',
1183 s.ranCount() === 0 && s.armCount() === 1 && s.pendingLive() === true
1184 && !s.sync.leases()['turn-live']);
1185 // The nominee sleeps: its one beat ages out of the freshness window.
1186 s.age(W + 1);
1187 await s.fire();
1188 check('(g) the scheduled re-collect CLAIMS once the nominee is stale (turn runs)',
1189 s.ranCount() === 1 && s.sync.leases()['turn-live'].holder === selfId);
1190 check('(g) the driver stops after the claim (no re-arm, no double run)',
1191 s.pendingLive() === false && s.ranCount() === 1);
1192 }
1193
1194 // ── (h) a transient collect failure RE-ARMS rather than dropping the driver, and
1195 // a later good tick still drives the claim. This is the gap the fix closes. ──
1196 {
1197 const s = await scenario();
1198 await s.collect(); // wake: HOLD + arm (nominee fresh)
1199 check('(h) armed after the wake', s.armCount() === 1 && s.pendingLive() === true);
1200 s.age(W + 1); // the nominee has now slept out the window
1201 s.setMode('fail'); // the next tick hits an offline blip
1202 await s.fire();
1203 check('(h) a FAILED tick re-arms the driver rather than dropping it',
1204 s.ranCount() === 0 && s.armCount() === 2 && s.pendingLive() === true);
1205 s.setMode('ok'); // the blip clears
1206 await s.fire();
1207 check('(h) the re-armed driver drives the claim on the next good tick',
1208 s.ranCount() === 1 && s.sync.leases()['turn-live'].holder === selfId);
1209 check('(h) exactly one live timer throughout (no double-arm)',
1210 s.timer.live() === 0 && s.pendingLive() === false);
1211 }
1212
1213 // ── (i) THE FIX: an awaited presence refresh on the collect path flips a stale LOCAL
1214 // read of an awake nominee back to a stand-down. Without the refresh line in the
1215 // reproduced onErrand glue this reddens -- the stale snapshot claims the nominee's
1216 // turn, which is the shipped iOS bug. ──
1217 {
1218 const s = await scenario();
1219 s.age(W + 1); // the LOCAL snapshot of the nominee is now stale...
1220 s.gatewayFresh(); // ...but the nominee actually beat just now (gateway is fresh)
1221 const r = await s.collect(); // wake: onErrand refreshes, then the stand-down sees it awake
1222 check('(i) refresh flips a stale local read: stands down for the awake nominee (HOLD, no claim)',
1223 r.ok === true && r.hold === true && s.ranCount() === 0
1224 && !s.sync.leases()['turn-live']);
1225 }
1226
1227 // ── (j) a refresh that fails, or the bounded-await timeout, falls through to the local
1228 // snapshot and CLAIMS -- liveness, never a strand. The shipped Promise.race(4s)
1229 // timeout proceeds on the local snapshot exactly as a caught failure does. ──
1230 {
1231 const s = await scenario();
1232 s.age(W + 1); // local snapshot stale
1233 s.gatewayFresh(); // the nominee is actually awake...
1234 s.setRefreshMode('fail'); // ...but the refresh fails (offline / hung gateway / timeout)
1235 await s.collect();
1236 check('(j) a failed or timed-out refresh falls through to local and CLAIMS (liveness)',
1237 s.ranCount() === 1 && s.sync.leases()['turn-live'].holder === selfId);
1238 }
1239
1240 // ── (k) THE WRITER-SIDE FIX: a device that runs a turn beats presence on completion,
1241 // so the next turn's stand-down sees it fresh. Without the beat line in the glue this
1242 // reddens (beatCount stays 0), which is the between-turns staleness that let gilgamesh
1243 // grab turn 2 while argonaut was backgrounded. ──
1244 {
1245 const s = await scenario();
1246 s.age(W + 1); // the nominee is genuinely stale, so this device claims and runs
1247 await s.collect();
1248 check('(k) running a turn beats presence on completion (liveness asserted, not left to the throttled timer)',
1249 s.ranCount() === 1 && s.beatCount() === 1);
1250 }
1251}
1252
1253async function runPresenceAcceptance(P, PR, check) {
1254 const T = 1700000000000;
1255 const fresh = { argonaut: { name: 'argonaut', lastSeen: T - 1000 } };
1256 const stale = { argonaut: { name: 'argonaut', lastSeen: T - 200000 } };
1257
1258 // ── Presence provider: freshest-scalar merge, freshness, self-exclusion. ──
1259 PR.forget();
1260 PR.beat('phone', 'phone-name', T);
1261 PR.adopt({ argonaut: { name: 'argonaut', lastSeen: T - 1000 } });
1262 check('presence snapshot carries this device and the adopted peer',
1263 !!PR.snapshot().phone && !!PR.snapshot().argonaut);
1264 check('awake() excludes self and lists the fresh peer',
1265 PR.awake('phone', T).length === 1 && PR.awake('phone', T)[0].deviceId === 'argonaut');
1266 // Freshest-scalar: an OLDER beat does not overwrite a newer one.
1267 PR.adopt({ argonaut: { name: 'argonaut', lastSeen: T - 5000 } });
1268 check('a stale incoming beat does NOT overwrite a fresher one (freshest-scalar)',
1269 PR.snapshot().argonaut.lastSeen === T - 1000);
1270 // A NEWER beat does win.
1271 PR.adopt({ argonaut: { name: 'argonaut', lastSeen: T - 100 } });
1272 check('a fresher incoming beat wins', PR.snapshot().argonaut.lastSeen === T - 100);
1273 check('a peer past the freshness window is not awake', PR.awake('phone', T + 200000).length === 0);
1274 check('the peer name is carried for the UI', PR.name('argonaut') === 'argonaut');
1275
1276 // ── freshestPeer: the one shared "who is awake" answer. ──
1277 check('freshestPeer finds the fresh non-self peer',
1278 P.freshestPeer(fresh, 'phone', T).deviceId === 'argonaut');
1279 check('freshestPeer is null when the only beat is stale',
1280 P.freshestPeer(stale, 'phone', T) === null);
1281 check('freshestPeer excludes this device',
1282 P.freshestPeer({ phone: { name: 'me', lastSeen: T } }, 'phone', T) === null);
1283
1284 // ── autoDispatchDecision: the policy. ──
1285 const quickChat = { id: 'c', provider: 'openrouter', model: 'm' };
1286 const workerChat = { id: 'c2', workerModel: 'w' };
1287 check('fresh peer + LONG turn (tools) -> dispatch',
1288 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', toolsEnabled: true }, T); return d.dispatch === true && d.reason === 'long-turn' && d.peer.name === 'argonaut'; })());
1289 check('fresh peer + QUICK turn -> run local',
1290 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone' }, T); return d.dispatch === false && d.reason === 'quick-local'; })());
1291 check('STALE peer -> run local (never dispatch into the void)',
1292 (() => { const d = P.autoDispatchDecision(quickChat, stale, { selfId: 'phone', toolsEnabled: true }, T); return d.dispatch === false && d.reason === 'no-fresh-peer'; })());
1293 check('no presence -> run local',
1294 P.autoDispatchDecision(quickChat, {}, { selfId: 'phone', toolsEnabled: true }, T).dispatch === false);
1295 check('TOGGLE on -> dispatch even a quick turn',
1296 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', toggle: true }, T); return d.dispatch === true && d.reason === 'toggle-on'; })());
1297 check('global default on (chat unset) -> dispatch a quick turn',
1298 P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', globalDefault: true }, T).dispatch === true);
1299 check('per-chat toggle OFF overrides a global default ON',
1300 P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', toggle: false, globalDefault: true }, T).dispatch === false);
1301 check('backgrounding with a turn in flight -> dispatch',
1302 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', backgrounding: true, turnInFlight: true }, T); return d.dispatch === true && d.reason === 'backgrounding-in-flight'; })());
1303 check('a worker chat is agentic -> dispatch',
1304 P.autoDispatchDecision(workerChat, fresh, { selfId: 'phone' }, T).reason === 'long-turn');
1305
1306 // ── The MOBILE policy: a phone hands EVERY turn to an awake peer, quick or not,
1307 // because the phone is not where a turn should run when a persistent peer exists;
1308 // the answer syncs back. Desktop keeps the old behaviour (it IS the instance). ──
1309 check('MOBILE + fresh peer + a plain QUICK turn -> dispatch (mobile-peer), naming the peer',
1310 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: true }, T); return d.dispatch === true && d.reason === 'mobile-peer' && d.peer && d.peer.name === 'argonaut'; })());
1311 check('DESKTOP (no isPhone) + fresh peer + a QUICK turn -> run local, NOT mobile-peer',
1312 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: false }, T); return d.dispatch === false && d.reason === 'quick-local'; })());
1313 check('MOBILE with NO fresh peer -> run local (never dispatch into the void)',
1314 (() => { const d = P.autoDispatchDecision(quickChat, stale, { selfId: 'phone', isPhone: true }, T); return d.dispatch === false && d.reason === 'no-fresh-peer'; })());
1315
1316 // ── The per-chat OPT-OUT pins a chat to THIS device, and it must beat the mobile
1317 // default and the global default alike -- so it is decided before either. ──
1318 check('OPT-OUT (toggle false) keeps a chat local even on MOBILE with a peer awake',
1319 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: true, toggle: false }, T); return d.dispatch === false && d.reason === 'chat-local'; })());
1320 check('OPT-OUT beats a global default ON as well (opt-out is decided first)',
1321 P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: true, toggle: false, globalDefault: true }, T).dispatch === false);
1322 check('OPT-IN (toggle true) is still an override on DESKTOP (toggle-on, not quick-local)',
1323 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: false, toggle: true }, T); return d.dispatch === true && d.reason === 'toggle-on'; })());
1324
1325 // ── LAPTOP ELIGIBILITY (proposal #10). A wide desktop view is `isPhone: false`.
1326 // Before this it could only hand off an agentic turn; the step-away posture, which
1327 // daimond.js's `maybeAutoDispatch` passes as `globalDefault: handoffWhenAway()`,
1328 // makes a laptop route its ordinary quick turns to an awake peer too, so a chat
1329 // started on it survives it being closed. Off by silence, so nothing changes for a
1330 // laptop that never turned it on. ──
1331 check('LAPTOP (isPhone false) + step-away posture ON + fresh peer -> dispatch, naming the peer',
1332 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: false, globalDefault: true }, T); return d.dispatch === true && d.reason === 'toggle-on' && d.peer && d.peer.name === 'argonaut'; })());
1333 check('LAPTOP + posture OFF + a QUICK turn -> run local (no regression when unset)',
1334 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: false }, T); return d.dispatch === false && d.reason === 'quick-local'; })());
1335
1336 // ── THE STEP-AWAY HAND-OFF (proposal #10). Exactly the opts daimond.js's
1337 // `handoffInFlightOnStepAway` passes from the `pagehide` handler for a turn still
1338 // running when the laptop is closed: backgrounding, a turn in flight. The posture
1339 // gate is applied in the app before this call; the pure decision picks the
1340 // freshest peer, and the lease is the single-runner arbiter at run time (untouched
1341 // here). ──
1342 check('STEP-AWAY: laptop closing with a turn in flight -> hand to the freshest peer',
1343 (() => { const d = P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: false, backgrounding: true, turnInFlight: true }, T); return d.dispatch === true && d.reason === 'backgrounding-in-flight' && d.peer && d.peer.name === 'argonaut'; })());
1344 check('STEP-AWAY with NO fresh peer -> run local (never hand into the void)',
1345 (() => { const d = P.autoDispatchDecision(quickChat, stale, { selfId: 'phone', isPhone: false, backgrounding: true, turnInFlight: true }, T); return d.dispatch === false && d.reason === 'no-fresh-peer'; })());
1346 check('STEP-AWAY still honours a per-chat OPT-OUT (a pinned chat is decided local first)',
1347 P.autoDispatchDecision(quickChat, fresh, { selfId: 'phone', isPhone: false, backgrounding: true, turnInFlight: true, toggle: false }, T).dispatch === false);
1348}
1349
1350// ════════════════════════════════════════════════════════════════
1351// The four money-safety defects, each with a mutation-proving check.
1352// ════════════════════════════════════════════════════════════════
1353async function runMoneySafety(phone, laptop, check) {
1354 const Pp = phone.DaimondPeer, Pl = laptop.DaimondPeer;
1355
1356 // ── D1 — the DISPATCHER must not re-run its own errand after release ──
1357 //
1358 // The sequential double-bill: phone dispatches -> laptop claims, runs, pushes,
1359 // RELEASES the lease -> phone returns and re-collects its OWN errand -> without
1360 // the guards it re-takes the released lease (which reads vacant) and runs the
1361 // already-completed turn a second time: two completions, two charges. The lease
1362 // CAS is shared (one parcel); each device runs through its own peer module.
1363 console.log('\nD1 — the dispatcher must NOT re-run its own errand after the peer releases');
1364 {
1365 phone.DaimondLease.forget(); laptop.DaimondLease.forget();
1366 const sync = makeLeaseSync({});
1367 let runCount = 0;
1368 const errand = Pp.makeErrand({
1369 turnId: 'turn-d1', chatId: 'chat-d1', prompt: 'add up', eid: 'e-d1',
1370 deadline: 9e15, dispatchedBy: 'phoneDev',
1371 });
1372 const deps = (selfId, extra) => Object.assign({
1373 selfId, cas: Pp.syncCas(sync), now: () => 5000,
1374 reconstruct: async () => ({ chat: { id: 'chat-d1', messages: [{ role: 'user', content: 'add up', mid: 'turn-d1', ts: 1 }] } }),
1375 runTurn: async () => { runCount += 1; },
1376 abort: () => {}, pushResult: async () => 7, post: async () => {}, ack: async () => {},
1377 }, extra || {});
1378
1379 // The peer (NOT the dispatcher) runs the turn and releases the lease.
1380 const lap = await Pl.runErrand(errand, deps('laptopDev'));
1381 check('D1: the peer runs the dispatched turn exactly once', lap.ran === true && runCount === 1);
1382 check('D1: the peer releases the lease when done (reads vacant afterwards)',
1383 sync.leases()['turn-d1'].mode === 'released');
1384
1385 // (a) The phone returns and re-collects its OWN errand: it MUST stand down.
1386 const ph = await Pp.runErrand(errand, deps('phoneDev'));
1387 check('D1(a): the dispatcher stands down on its OWN errand (dispatchedBy === self)',
1388 ph.ran === false && ph.why === 'self-dispatched');
1389 check('D1(a): still exactly ONE run/charge after the dispatcher re-collects', runCount === 1);
1390
1391 // (b) A THIRD device -- not the dispatcher -- collecting the SAME errand after
1392 // the release. The lease is 'released' (reads vacant), so without the finished
1393 // check it would re-take and re-run. A done report / merged answer means the
1394 // turn is FINISHED, not vacant-for-rerun: stand down before the take.
1395 const third = await Pl.runErrand(errand, deps('lap2Dev', { finished: async () => true }));
1396 check('D1(b): a released (vacant-reading) lease with a done answer is treated as FINISHED',
1397 third.ran === false && third.why === 'already-done');
1398 check('D1(b): still exactly ONE run/charge after a third device collects post-release', runCount === 1);
1399 }
1400
1401 // ── D2 — a plain chat is not "agentic" merely because it mirrors its model ──
1402 //
1403 // daimond.js seeds workerModel/workerProvider to the chat's OWN model for every
1404 // active chat (newChat, startChat), so a bare `c.workerModel` truthiness test
1405 // dispatched EVERY quick turn. The signal must be a GENUINE worker pair.
1406 console.log('\nD2 — a plain chat whose worker pair mirrors its own model stays local');
1407 {
1408 const T = 1700000000000;
1409 const fresh = { argonaut: { name: 'argonaut', lastSeen: T - 1000 } };
1410 const mirrored = { id: 'c', provider: 'openrouter', model: 'm', workerModel: 'm', workerProvider: 'openrouter' };
1411 const dM = Pp.autoDispatchDecision(mirrored, fresh, { selfId: 'phone' }, T);
1412 check('D2: a plain foreground chat with a MIRRORED worker pair stays local',
1413 dM.dispatch === false && dM.reason === 'quick-local');
1414 const genuine = { id: 'c2', provider: 'openrouter', model: 'm', workerModel: 'big/model', workerProvider: 'openrouter' };
1415 const dG = Pp.autoDispatchDecision(genuine, fresh, { selfId: 'phone' }, T);
1416 check('D2: a genuine worker chat (worker model differs) still dispatches',
1417 dG.dispatch === true && dG.reason === 'long-turn');
1418 // A worker PROVIDER that differs is genuine too, even with the same model name.
1419 const diffProv = { id: 'c3', provider: 'openrouter', model: 'm', workerModel: 'm', workerProvider: 'anthropic' };
1420 check('D2: a differing worker PROVIDER is agentic',
1421 Pp.autoDispatchDecision(diffProv, fresh, { selfId: 'phone' }, T).dispatch === true);
1422 }
1423
1424 // ── D3 — the peer runs against the transcript without re-appending the prompt ──
1425 //
1426 // The dispatcher persist-first pushed the prompt into the synced transcript
1427 // before the errand (§4.1). runErrand must tell runTurn so, or the model is fed
1428 // the prompt twice (seeded history + the re-sent turn) and it sits twice in the
1429 // messages array. The mock is a FAITHFUL stand-in for the real runTurn + agent
1430 // seam: the agent is seeded from the existing user/assistant history (ensureApp),
1431 // then run_turn SENDS `prompt`. Told the prompt is already present, the peer must
1432 // seed the agent WITHOUT it and not append a duplicate record.
1433 console.log('\nD3 — the peer does not feed the model the prompt twice');
1434 {
1435 phone.DaimondLease.forget();
1436 const sync = makeLeaseSync({});
1437 const errand = Pp.makeErrand({ turnId: 'turn-d3', chatId: 'chat-d3', prompt: 'the question', eid: 'e-d3', deadline: 9e15 });
1438 const ctxChat = { id: 'chat-d3', messages: [{ role: 'user', content: 'the question', mid: 'turn-d3', ts: 1 }] };
1439 let request = null;
1440 const res = await Pp.runErrand(errand, {
1441 selfId: 'peerZ', cas: Pp.syncCas(sync), now: () => 2000,
1442 reconstruct: async () => ({ chat: ctxChat }),
1443 runTurn: async (ctx, prompt, opts) => {
1444 const already = !!(opts && opts.promptInTranscript);
1445 const anchor = opts && opts.turnId;
1446 const seeded = (ctx.chat.messages || [])
1447 .filter((m) => (m.role === 'user' || m.role === 'assistant') && m.content)
1448 .filter((m) => !(already && anchor && String(m.mid) === String(anchor)))
1449 .map((m) => m.content);
1450 request = seeded.concat([prompt]); // run_turn always sends `prompt`
1451 if (!already) ctx.chat.messages.push({ role: 'user', content: prompt, mid: 'dup', ts: 9 });
1452 Pp.foldAssistant(ctx.chat, { mid: 'a-d3', turnId: 'turn-d3', text: 'answer', ts: 10 });
1453 },
1454 abort: () => {}, pushResult: async () => 3, post: async () => {}, ack: async () => {},
1455 });
1456 check('D3: the errand ran', res.ran === true && res.done === true);
1457 check('D3: the dispatched request carries the prompt exactly ONCE',
1458 request && request.filter((c) => c === 'the question').length === 1);
1459 check('D3: the prompt is not duplicated in the messages array',
1460 ctxChat.messages.filter((m) => m.role === 'user' && m.content === 'the question').length === 1);
1461 }
1462
1463 // ── D4 — a sync-pulled lease update notifies the UI (footer advances) ──
1464 //
1465 // A lease learned through a SYNC pull moves the local view but touches no message
1466 // record, so the dispatched footer would sit on "Sent to your other devices" and
1467 // never advance to running / show "[Take back]". `leaseAdopt` fires a registered
1468 // change listener whenever the merge actually moved.
1469 console.log('\nD4 — a sync-pulled lease update re-renders the dispatched footer');
1470 {
1471 const L = phone.DaimondLease;
1472 L.forget();
1473 check('D4: DaimondLease.onChange is published', typeof L.onChange === 'function');
1474 let fired = 0;
1475 if (L.onChange) L.onChange(() => { fired += 1; });
1476 const T = 1700000000000;
1477 const rec = { 'turn-d4': { turnId: 'turn-d4', holder: 'devLAP', mode: 'running', expiry: T + 60000, renewedAt: T } };
1478 const moved = L.adopt(rec, () => T);
1479 check('D4: adopting a newly-seen lease MOVES the view', moved === true);
1480 check('D4: a moved lease fires the change listener (the footer re-renders)', fired === 1);
1481 const before = fired;
1482 L.adopt({ 'turn-d4': { turnId: 'turn-d4', holder: 'devLAP', mode: 'running', expiry: T + 60000, renewedAt: T } }, () => T);
1483 check('D4: an unchanged pull does not fire the listener (no needless redraw)', fired === before);
1484 L.forget();
1485 }
1486}
1487
1488// A sync-like object modelling the gateway's leases CAS for the syncCas adapter:
1489// `version()`, `leases()`, and `commit(base, leases)` that accepts only when
1490// `base` is current (then bumps), else answers the current blob -- the 409.
1491function makeLeaseSync(initial) {
1492 let version = 5;
1493 let leases = JSON.parse(JSON.stringify(initial || {}));
1494 return {
1495 version: () => version,
1496 leases: () => JSON.parse(JSON.stringify(leases)),
1497 commit: (base, next) => {
1498 if (base !== version) return { ok: false, version, leases: JSON.parse(JSON.stringify(leases)) };
1499 version += 1; leases = JSON.parse(JSON.stringify(next));
1500 return { ok: true, version };
1501 },
1502 };
1503}
1504
1505// A leases CAS that COUNTS the pushes a renew commits and lets renews land (busy
1506// false), so a heartbeat that outlives its turn is visible as an ever-climbing
1507// version -- the permanent parcel churn the fix bounds. Faithful to makeLeaseSync's
1508// 409-on-stale-base, plus a `pushes` tally and a `bytes` digest of the leases.
1509function makeCountingSync(initial) {
1510 let version = 5;
1511 let leases = JSON.parse(JSON.stringify(initial || {}));
1512 let pushes = 0;
1513 return {
1514 version: () => version,
1515 leases: () => JSON.parse(JSON.stringify(leases)),
1516 pushes: () => pushes,
1517 bytes: () => JSON.stringify(leases),
1518 commit: (base, next) => {
1519 if (base !== version) return { ok: false, version, leases: JSON.parse(JSON.stringify(leases)) };
1520 version += 1; leases = JSON.parse(JSON.stringify(next)); pushes += 1;
1521 return { ok: true, version };
1522 },
1523 };
1524}
1525
1526// An injectable timer the test drives by hand: `set` records a live handle and its
1527// callback; `clear` marks it dead. The test invokes the captured callback itself,
1528// so ticks are deterministic and no wall-clock 30s is waited.
1529function makeFakeTimer() {
1530 const handles = [];
1531 return {
1532 handles,
1533 set: (fn, ms) => { const h = { fn, ms, live: true }; handles.push(h); return h; },
1534 clear: (h) => { if (h) h.live = false; },
1535 live: () => handles.filter((h) => h.live).length,
1536 };
1537}
1538
1539async function runHeartbeatContainment(P, L, check) {
1540 const TID = 'turn-hb';
1541
1542 // ── (A) HAPPY PATH: the ticker is created, then CLEARED, so nothing renews after
1543 // the turn. A ticker left live is the loop; a ticker cleared is a fixed point. ──
1544 {
1545 L.forget();
1546 const sync = makeCountingSync({});
1547 const timer = makeFakeTimer();
1548 const errand = P.makeErrand({ turnId: TID, chatId: 'c', prompt: 'p', eid: 'e', deadline: 9e15 });
1549 const res = await P.runErrand(errand, {
1550 selfId: 'peerA', cas: P.syncCas(sync), now: () => 2000,
1551 setTimer: timer.set, clearTimer: timer.clear,
1552 reconstruct: async () => ({ chat: { id: 'c', messages: [] } }),
1553 runTurn: async (ctx, prompt, opts) => { await opts.onProgress(); P.foldAssistant(ctx.chat, { mid: 'a', turnId: TID, text: 'ok', ts: 3 }); },
1554 abort: () => {}, pushResult: async () => 9, post: async () => {}, ack: async () => {},
1555 });
1556 check('heartbeat: the happy path creates a renew ticker', timer.handles.length === 1);
1557 check('heartbeat: and the ticker is CLEARED when the turn ends (no lingering renew)',
1558 timer.live() === 0 && res.done === true);
1559 // Fire the (dead) ticker callback anyway: a released lease must NEVER be re-renewed.
1560 const pushesAfter = sync.pushes();
1561 await timer.handles[0].fn();
1562 check('heartbeat: firing the ticker after release renews nothing (released lease is not resurrected)',
1563 sync.pushes() === pushesAfter && sync.leases()[TID].mode === 'released');
1564 }
1565
1566 // ── (B) THE TICKER IS READ-ONLY: a running turn -- even one whose promise NEVER
1567 // settles (the "couldn't finish" errand) -- causes NO parcel write from the
1568 // ticker, so the parcel is a fixed point for the whole turn (the churn source is
1569 // gone, not merely bounded). The lifetime CAP still stops the ticker and aborts a
1570 // hung turn, so no timer fires for ever. Reverting the cap leaves the timer live. ──
1571 {
1572 L.forget();
1573 const sync = makeCountingSync({});
1574 const timer = makeFakeTimer();
1575 let clock = 1000, aborted = 0;
1576 const errand = P.makeErrand({ turnId: TID, chatId: 'c', prompt: 'p', eid: 'e', deadline: 9e15 });
1577 const running = P.runErrand(errand, {
1578 selfId: 'peerHang', cas: P.syncCas(sync), now: () => clock,
1579 setTimer: timer.set, clearTimer: timer.clear,
1580 maxLeaseLifeMs: 100, // tiny cap so the test drives past it in a few ticks
1581 reconstruct: async () => ({ chat: { id: 'c', messages: [] } }),
1582 runTurn: () => new Promise(() => {}), // never settles
1583 abort: () => { aborted += 1; },
1584 pushResult: async () => 9, post: async () => {}, ack: async () => {},
1585 });
1586 await new Promise((r) => setTimeout(r, 0)); // let take/mark-running/ticker start
1587 check('heartbeat: a hung turn holds the lease and started a liveness ticker',
1588 timer.handles.length === 1 && !!sync.leases()[TID]);
1589 // Only the take (claim) and the one claimed->running transition wrote; the ticker
1590 // must add nothing more, however many times it fires.
1591 const pushesAtRunStart = sync.pushes();
1592 const tick = timer.handles[0].fn;
1593 for (let i = 0; i < 8; i++) { clock += 40; await tick(); } // each tick +40ms; cap 100ms
1594 check('heartbeat: the liveness ticker is READ-ONLY -- driving it pushes NOTHING (no renew churn)',
1595 sync.pushes() === pushesAtRunStart);
1596 check('heartbeat: a hung turn does NOT fire for ever -- the cap STOPS the ticker',
1597 timer.live() === 0);
1598 check('heartbeat: the cap aborts the hung run (best-effort hard stop)', aborted >= 1);
1599 for (let i = 0; i < 6; i++) { clock += 40; await tick(); } // well past the cap
1600 check('heartbeat: past the cap the parcel is still untouched (fixed point during the turn)',
1601 sync.pushes() === pushesAtRunStart);
1602 void running; // intentionally never awaited: the turn hung
1603 }
1604
1605 // ── (C) CRASH: the error path leaves the lease to EXPIRE, and the fix guarantees it
1606 // expires WITHOUT a further renew -- the ticker is cleared by the finally. ──
1607 {
1608 L.forget();
1609 const sync = makeCountingSync({});
1610 const timer = makeFakeTimer();
1611 const errand = P.makeErrand({ turnId: TID, chatId: 'c', prompt: 'p', eid: 'e', deadline: 9e15 });
1612 const res = await P.runErrand(errand, {
1613 selfId: 'peerCrash', cas: P.syncCas(sync), now: () => 2000,
1614 setTimer: timer.set, clearTimer: timer.clear,
1615 reconstruct: async () => ({ chat: { id: 'c', messages: [] } }),
1616 runTurn: async () => { throw new Error('kaboom'); },
1617 abort: () => {}, pushResult: async () => 9, post: async () => {}, ack: async () => {},
1618 });
1619 check('heartbeat: a crash is reported as an error and the lease is left unreleased (to expire)',
1620 res.error === true && sync.leases()[TID].mode !== 'released');
1621 check('heartbeat: the crash CLEARS the ticker, so the lingering lease expires without a renew',
1622 timer.live() === 0);
1623 const pushesAfter = sync.pushes();
1624 await timer.handles[0].fn(); // the dead ticker fires once more
1625 check('heartbeat: a fired-after-crash ticker renews nothing', sync.pushes() === pushesAfter);
1626 }
1627}
1628
1629async function runDeadlineExpiryMoneySafety(P, L, check) {
1630 const NOW = 1_700_000_000_000;
1631 const DEADLINE = NOW + 15 * 60 * 1000; // the dispatch deadline (buildDispatch default)
1632
1633 // ── The claim expiry IS the deadline, and the record carries it. ──
1634 {
1635 L.forget();
1636 const cas = makeCas({});
1637 const took = await L.take('turn-ttl', { holder: 'DESK', eid: 'e', deadline: DEADLINE }, cas, () => NOW);
1638 check('deadline: a claim with a deadline expires AT the deadline (not now + TTL)',
1639 took.won === true && cas.peekLeases()['turn-ttl'].expiry === DEADLINE);
1640 check('deadline: the record carries `deadline` so every merge/clamp honours the same bound',
1641 cas.peekLeases()['turn-ttl'].deadline === DEADLINE);
1642 }
1643
1644 // ── A recovery errand (no deadline) still gets a single TTL, unchanged. ──
1645 {
1646 L.forget();
1647 const cas = makeCas({});
1648 await L.take('turn-rec', { holder: 'PHONE', eid: 'e', deadline: 0 }, cas, () => NOW);
1649 check('deadline: a no-deadline (recovery) claim falls back to now + TTL',
1650 cas.peekLeases()['turn-rec'].expiry === NOW + L.LEASE_TTL_MS);
1651 }
1652
1653 // ── THE MONEY CRUX: a busy peer holds a turn for >90s; the phone returns and MUST
1654 // stand down, because the deadline-bounded lease still reads LIVE. Reverting the
1655 // claim expiry to now+TTL re-opens the double-charge (proven by the mutation run).
1656 {
1657 L.forget();
1658 const cas = makeCas({}); // the one shared parcel both devices read
1659 const desk = await L.take('turn-long', { holder: 'DESK', eid: 'e', deadline: DEADLINE }, cas, () => NOW);
1660 check('>TTL: the peer holds the long turn (claimed to its deadline)', desk.won === true);
1661 const later = NOW + 100_000; // past LEASE_TTL_MS (90s), well before the deadline
1662 // The phone returns, pulls the parcel, and evaluates recovery against server truth.
1663 L.forget();
1664 L.adopt(cas.peekLeases(), () => later);
1665 const rec = L.record('turn-long');
1666 check('>TTL: the peer lease still reads LIVE after 90s (deadline-bounded, no renew needed)',
1667 L.live(rec, later) === true);
1668 check('>TTL: recoverDecision stands the phone DOWN (a live foreign lease holds it)',
1669 P.recoverDecision({ why: 'dispatched', iturn: 'turn-long' }, rec, false, 'PHONE', later) === false);
1670 // The take itself: the phone tries to reclaim through the CAS and MUST lose.
1671 const phone = await L.take('turn-long', { holder: 'PHONE', eid: 'e2', deadline: 0 }, cas, () => later);
1672 check('>TTL: the phone take STANDS DOWN while the peer still runs (no double-run/charge)',
1673 phone.won === false && phone.holder === 'DESK');
1674 check('>TTL: EXACTLY ONE holder remains -- the peer (no double-charge)',
1675 (desk.won ? 1 : 0) + (phone.won ? 1 : 0) === 1 && cas.peekLeases()['turn-long'].holder === 'DESK');
1676 }
1677
1678 // ── The dead-peer bound: a lease is reclaimable at its DEADLINE, not before, and
1679 // not forever -- the accepted recovery-latency tradeoff, honoured via the carried
1680 // deadline (clampExpiry does not shrink it below the deadline on adopt). ──
1681 {
1682 L.forget();
1683 const cas = makeCas({});
1684 await L.take('turn-dead', { holder: 'DESK', eid: 'e', deadline: DEADLINE }, cas, () => NOW);
1685 // Before the deadline the lease is NOT reclaimable, even after adopting it fresh.
1686 L.forget();
1687 L.adopt(cas.peekLeases(), () => NOW + 100_000);
1688 const early = await L.take('turn-dead', { holder: 'PHONE', eid: 'e2', deadline: 0 }, cas, () => NOW + 100_000);
1689 check('dead-peer: before the deadline the lease is NOT reclaimable (held for the turn)',
1690 early.won === false && cas.peekLeases()['turn-dead'].holder === 'DESK');
1691 // Past the deadline (the dead peer never renews -- there is no renew) it expires.
1692 const reclaim = await L.take('turn-dead', { holder: 'PHONE', eid: 'e3', deadline: 0 }, cas, () => DEADLINE + 1);
1693 check('dead-peer: at the deadline a dead holder\'s lease IS reclaimable (bounded, not forever)',
1694 reclaim.won === true && cas.peekLeases()['turn-dead'].holder === 'PHONE');
1695 }
1696}
1697
1698async function runRunnerAcceptance(P, L, check) {
1699 const TID = 'turn-run';
1700 const errand = P.makeErrand({ turnId: TID, chatId: 'chat-r', prompt: 'compute', eid: 'e-run', deadline: 9e15 });
1701
1702 // ── syncCas arbitration: two takes from ONE base, exactly one wins. ──
1703 {
1704 L.forget();
1705 const sync = makeLeaseSync({});
1706 const cas = P.syncCas(sync);
1707 const snapA = await cas.read(); // both based on the SAME version through the adapter
1708 const snapB = await cas.read();
1709 const aRes = await L.takeFrom(snapA, TID, { holder: 'A', eid: 'ea' }, cas, () => 1000);
1710 const bRes = await L.takeFrom(snapB, TID, { holder: 'B', eid: 'eb' }, cas, () => 1001);
1711 check('syncCas: exactly one take wins through the adapter', (aRes.won ? 1 : 0) + (bRes.won ? 1 : 0) === 1);
1712 check('syncCas: the loser stood down (commit 409 -> adopt -> retry)', aRes.won === true && bRes.won === false);
1713 check('syncCas: the committed sync names the winner', sync.leases()[TID].holder === 'A');
1714 }
1715
1716 // ── Happy path: take -> reconstruct -> run -> push -> report -> complete -> ack -> release. ──
1717 {
1718 L.forget();
1719 const sync = makeLeaseSync({});
1720 let pushed = 0, report = null, acked = 0;
1721 const ctxChat = { id: 'chat-r', messages: [{ role: 'user', content: 'compute', mid: 'u1', ts: 1 }] };
1722 const res = await P.runErrand(errand, {
1723 selfId: 'peerA', cas: P.syncCas(sync), now: () => 2000,
1724 reconstruct: async () => ({ chat: ctxChat }),
1725 runTurn: async (ctx, prompt, opts) => {
1726 await opts.onProgress(); // a journal event -> lease renew
1727 P.foldAssistant(ctx.chat, { mid: 'a1', turnId: TID, text: 'the answer is 42', ts: 3 });
1728 },
1729 abort: () => {},
1730 pushResult: async () => { pushed += 1; return 9; },
1731 post: async (rep) => { report = rep; },
1732 ack: async () => { acked += 1; },
1733 });
1734 check('the runner completes the errand', res.ran === true && res.done === true);
1735 check('the runner order is take,reconstruct,run,push,report,complete,ack,release',
1736 res.trace.join(',') === 'take,reconstruct,run,push,report,complete,ack,release');
1737 check('the answer was folded into the transcript',
1738 ctxChat.messages.some((m) => m.role === 'assistant' && m.content === 'the answer is 42'));
1739 check('the transcript was pushed exactly once', pushed === 1);
1740 check('a done report was posted carrying the pushed version',
1741 !!report && report.t === 'report' && report.status === 'done' && report.parcelVersion === 9);
1742 check('the errand was acked exactly once, AFTER the push',
1743 acked === 1 && res.trace.indexOf('ack') > res.trace.indexOf('push'));
1744 check('the lease ends released', sync.leases()[TID].mode === 'released');
1745 }
1746
1747 // ── Stand down: a peer already holds the lease, so the runner does not run. ──
1748 {
1749 L.forget();
1750 const sync = makeLeaseSync({ [TID]: { turnId: TID, eid: 'other', holder: 'peerB', mode: 'running', expiry: 9e15, renewedAt: 1 } });
1751 let touched = false, acked = 0;
1752 const res = await P.runErrand(errand, {
1753 selfId: 'peerA', cas: P.syncCas(sync), now: () => 2000,
1754 reconstruct: async () => { touched = true; return { chat: {} }; },
1755 runTurn: async () => { touched = true; },
1756 abort: () => {}, pushResult: async () => 1, post: async () => {}, ack: async () => { acked += 1; },
1757 });
1758 check('the runner STANDS DOWN when a peer holds the lease', res.ran === false);
1759 check('a stood-down runner never reconstructs or runs', touched === false && res.trace.join(',') === 'take');
1760 check('a stood-down runner never acks', acked === 0);
1761 }
1762
1763 // ── Revoke -> hard abort: the phone takes the turn back mid-run. ──
1764 {
1765 L.forget();
1766 const sync = makeLeaseSync({});
1767 const cas = P.syncCas(sync);
1768 const now = () => 1000;
1769 let abortFired = false, pushed = 0, acked = 0;
1770 const res = await P.runErrand(errand, {
1771 selfId: 'peerA', cas, now,
1772 reconstruct: async () => ({ chat: { id: 'chat-r', messages: [] } }),
1773 runTurn: async (ctx, prompt, opts) => {
1774 for (let i = 0; i < 6; i++) {
1775 if (i === 2) { await L.revoke(TID, cas, now); } // the phone's take-back
1776 await opts.onProgress();
1777 if (abortFired) throw new Error('aborted by signal');
1778 }
1779 },
1780 abort: () => { abortFired = true; },
1781 pushResult: async () => { pushed += 1; return 9; },
1782 post: async () => {}, ack: async () => { acked += 1; },
1783 });
1784 check('a revoked lease HARD-ABORTS the turn', res.aborted === true && abortFired === true);
1785 check('an aborted run never pushes or acks (no double-bill commit)', pushed === 0 && acked === 0);
1786 check('the abort is recorded and the run never completed',
1787 res.trace.indexOf('abort') >= 0 && res.trace.indexOf('push') < 0);
1788 }
1789
1790 // ── Crash before commit: no ack, so the errand is NOT dropped (step-2 gap closed). ──
1791 {
1792 L.forget();
1793 const sync = makeLeaseSync({});
1794 let acked = 0, pushed = 0;
1795 const res = await P.runErrand(errand, {
1796 selfId: 'peerA', cas: P.syncCas(sync), now: () => 2000,
1797 reconstruct: async () => ({ chat: { id: 'chat-r', messages: [] } }),
1798 runTurn: async () => { throw new Error('kaboom'); },
1799 abort: () => {}, pushResult: async () => { pushed += 1; return 9; },
1800 post: async () => {}, ack: async () => { acked += 1; },
1801 });
1802 check('a crash before commit is reported as an error', res.error === true);
1803 check('a crashed runner never acks (the errand survives on the relay to re-collect)',
1804 acked === 0 && pushed === 0);
1805 check('a crashed runner leaves the lease unreleased, to EXPIRE for the phone',
1806 !!sync.leases()[TID] && sync.leases()[TID].mode !== 'released');
1807 }
1808}
1809
1810// A compare-and-set that models /api/sync: one versioned {version, leases} blob.
1811// `read()` hands back a COPY; `write(base, leases)` accepts ONLY when `base` is the
1812// current version (then bumps it), else answers the current blob -- the 409. Copies
1813// throughout, so a caller cannot mutate the server's state by holding a reference.
1814function makeCas(initialLeases) {
1815 let version = 5; // an arbitrary non-zero base
1816 let leases = JSON.parse(JSON.stringify(initialLeases || {}));
1817 return {
1818 read: async () => ({ version, leases: JSON.parse(JSON.stringify(leases)) }),
1819 write: async (base, next) => {
1820 if (base !== version) return { ok: false, version, leases: JSON.parse(JSON.stringify(leases)) };
1821 version += 1;
1822 leases = JSON.parse(JSON.stringify(next));
1823 return { ok: true, version };
1824 },
1825 peekVersion: () => version,
1826 peekLeases: () => JSON.parse(JSON.stringify(leases)),
1827 };
1828}
1829
1830async function runLeaseAcceptance(L, check) {
1831 const TID = 'turn-race';
1832
1833 // ── The core race: A and B both read v5, then both attempt a TAKE. ──
1834 // B is given a strictly LATER clock than A, so a freshest-scalar merge would
1835 // hand B the turn -- which is precisely the double claim the mutation test
1836 // below forces. The correct rule ignores the clock across holders.
1837 {
1838 L.forget();
1839 const cas = makeCas({});
1840 const snapA = await cas.read(); // both based on the SAME version 5
1841 const snapB = await cas.read();
1842 const nowA = () => 1000;
1843 const nowB = () => 1001; // B's clock reads later
1844 const aRes = await L.takeFrom(snapA, TID, { holder: 'A', eid: 'eA' }, cas, nowA);
1845 const bRes = await L.takeFrom(snapB, TID, { holder: 'B', eid: 'eB' }, cas, nowB);
1846
1847 check('device A wins the take (committed at the lower version)', aRes.won === true);
1848 check('device B STANDS DOWN (a live foreign lease beat its claim)', bRes.won === false);
1849 check('B was told who holds it', bRes.holder === 'A');
1850 check('EXACTLY ONE device won -- no double run', (aRes.won ? 1 : 0) + (bRes.won ? 1 : 0) === 1);
1851 check('the committed parcel names A as the holder', cas.peekLeases()[TID].holder === 'A');
1852 check('the version advanced exactly once (one claim committed)', cas.peekVersion() === 6);
1853 }
1854
1855 // ── take-if-vacant keeps the winner when a fresh device adopts the parcel ──
1856 {
1857 const fresh = { forget: true }; // simulate a third device's local view via merge()
1858 // A third device, having pulled the parcel that names A, must keep A even if
1859 // it holds a stale local claim of its own for the same turn.
1860 const localClaimC = { [TID]: { turnId: TID, eid: 'eC', holder: 'C', mode: 'claimed', expiry: 9e15, renewedAt: 2000 } };
1861 const parcelWithA = { [TID]: { turnId: TID, eid: 'eA', holder: 'A', mode: 'running', expiry: 9e15, renewedAt: 1000 } };
1862 const merged = L.merge(localClaimC, parcelWithA, 3000);
1863 check('adopting a parcel that names A drops a local claim by C (incoming wins)',
1864 merged[TID].holder === 'A');
1865 }
1866
1867 // ── An expired lease is vacant and reclaimable. ──
1868 {
1869 L.forget();
1870 const now = () => 100000;
1871 const dead = { [TID]: { turnId: TID, eid: 'eDead', holder: 'Dead', mode: 'running', expiry: 100000 - 1, renewedAt: 1 } };
1872 const cas = makeCas(dead);
1873 const res = await L.take(TID, { holder: 'Reclaimer', eid: 'eR' }, cas, now);
1874 check('an EXPIRED lease is reclaimable (a new device takes it)', res.won === true);
1875 check('the reclaimer is now the holder', cas.peekLeases()[TID].holder === 'Reclaimer');
1876 }
1877
1878 // ── A renew keeps a live lease held against a would-be reclaimer. ──
1879 {
1880 L.forget();
1881 let clock = 100000;
1882 const now = () => clock;
1883 const cas = makeCas({});
1884 const held = await L.take(TID, { holder: 'Holder', eid: 'eH' }, cas, now);
1885 check('the holder takes the lease', held.won === true);
1886 // Time advances past the ORIGINAL expiry but the holder renews first.
1887 clock = 100000 + 40000; // > RENEW cadence, < TTL
1888 const rnw = await L.renew(TID, 'Holder', cas, now);
1889 check('the holder renews its live lease', rnw.ok === true);
1890 // A reclaimer tries at a moment past the ORIGINAL expiry but before the
1891 // RENEWED one -- it must stand down, because the renew kept the lease live.
1892 clock = 100000 + 95000; // past original 90s TTL, within the renewed one
1893 const late = await L.take(TID, { holder: 'LateComer', eid: 'eL' }, cas, now);
1894 check('a renew keeps the lease held (a late reclaimer stands down)', late.won === false);
1895 check('the late reclaimer was told the holder still holds it', late.holder === 'Holder');
1896
1897 // And once the holder RELEASES, the turn is reclaimable again.
1898 const rel = await L.release(TID, 'Holder', cas, now);
1899 check('the holder can release the lease', rel.ok === true);
1900 clock = 100000 + 96000;
1901 const after = await L.take(TID, { holder: 'NextUp', eid: 'eN' }, cas, now);
1902 check('after release the turn is reclaimable', after.won === true);
1903 }
1904
1905 // ── HARDENING a: a far-future expiry (fast-clock holder) is CLAMPED. ──
1906 {
1907 L.forget();
1908 const now = 1000000000000; // a realistic epoch-ms, past 2^31
1909 const TTL = L.LEASE_TTL_MS;
1910 const farFuture = { [TID]: { turnId: TID, eid: 'eFast', holder: 'Fast',
1911 mode: 'running', expiry: now + 10 * TTL, renewedAt: now } };
1912 const merged = L.merge({}, farFuture, now);
1913 check('an adopted far-future expiry is CLAMPED to now + TTL',
1914 merged[TID].expiry === now + TTL);
1915 // And so it is reclaimable at the NORMAL ttl, not parked for 10x it.
1916 const cas = makeCas(merged);
1917 const late = () => now + TTL + 1;
1918 const res = await L.take(TID, { holder: 'Rescuer', eid: 'eRes' }, cas, late);
1919 check('a clamped lease is reclaimable at the normal TTL (not parked)', res.won === true);
1920 }
1921
1922 // ── HARDENING b: equal renewedAt, 'released' beats 'running'. ──
1923 {
1924 const t = 5000;
1925 const running = { turnId: TID, eid: 'e', holder: 'H', mode: 'running', expiry: 9e12, renewedAt: t };
1926 const released = { turnId: TID, eid: 'e', holder: 'H', mode: 'released', expiry: 0, renewedAt: t };
1927 check('equal renewedAt: released beats running (incoming released)',
1928 L.mergeOne(running, released, 100).mode === 'released');
1929 check('equal renewedAt: released beats running (local released)',
1930 L.mergeOne(released, running, 100).mode === 'released');
1931 }
1932}
1933
1934// ── Remote consent for a handed-off turn ───────────────────
1935//
1936// The two envelopes (round-trip + the exact-act binding), the forged/replayed-grant
1937// defence, PARK reporting-then-releasing with a terminal fail at MAX_PARKS, the
1938// GLOBAL two-device parkCount bound (the money guarantee), policy composition, and
1939// attended-only routing. All against the REAL DaimondPeer under node.
1940async function runRemoteConsentAcceptance(phone, laptop, stranger, check) {
1941 const P = phone.DaimondPeer;
1942 const Pl = laptop.DaimondPeer;
1943 const Ps = stranger.DaimondPeer;
1944 const MAX = P.MAX_PARKS;
1945
1946 // ── A. The two envelopes: round-trip, exact-act binding, fresh cid. ──
1947 console.log('\nRemote consent — the ask/grant round-trip and the exact-act binding');
1948 {
1949 const ask = P.makeAsk({ eid: 'e1', turnId: 't1', chatId: 'c1', tool: 'web_type',
1950 host: 'shop.test', detail: 'card 4111 1111 1111 1111',
1951 deadline: 1700000000000 + 60000, dispatchedBy: 'devPHONE' });
1952 const askBody = await P.sealForSelf(ask);
1953 // The attended peer (same account) opens AND verifies the runner's question.
1954 const opened = await Pl.openEnvelope(askBody.envelope);
1955 check('consent-ask opens on an attended peer with tool/host/detail intact (uncut)',
1956 opened.t === 'consent-ask' && opened.tool === 'web_type' && opened.host === 'shop.test'
1957 && opened.detail === 'card 4111 1111 1111 1111' && opened.turnId === 't1'
1958 && opened.cid === ask.cid && opened.dispatchedBy === 'devPHONE');
1959
1960 // The attended device answers: a grant naming the SAME cid.
1961 const grant = Pl.makeGrant({ cid: ask.cid, eid: 'e1', turnId: 't1', verdict: 'allow', by: 'devLAPTOP' });
1962 const grantBody = await Pl.sealForSelf(grant);
1963 const gOpened = await P.openEnvelope(grantBody.envelope);
1964 check('consent-grant opens on the runner with the verdict and cid intact',
1965 gOpened.t === 'consent-grant' && gOpened.verdict === 'allow'
1966 && gOpened.cid === ask.cid && gOpened.turnId === 't1');
1967 check('the grant does NOT carry tool/host/detail (runner replays the EXACT held act, never re-derives)',
1968 gOpened.tool === undefined && gOpened.host === undefined && gOpened.detail === undefined);
1969
1970 const ask2 = P.makeAsk({ turnId: 't1', tool: 'web_type' });
1971 check('a FRESH cid is minted on every ask (a replayed grant matches no new cid)',
1972 ask2.cid && ask2.cid !== ask.cid);
1973 }
1974
1975 // ── B. Forged / replayed grant rejected. ──
1976 console.log('\nRemote consent — a forged or replayed grant authorises nothing');
1977 {
1978 // A STRANGER (different account) seals a grant; the runner cannot open it (it
1979 // is sealed to another account), and were the bytes forced in the signature is
1980 // not this account's -- either way it is refused before it reaches deliverGrant.
1981 const strangerGrant = Ps.makeGrant({ cid: 'deadbeef', turnId: 't1', verdict: 'allow', by: 'devEVIL' });
1982 const sBody = await Ps.sealForSelf(strangerGrant);
1983 let why = '';
1984 try { await P.openEnvelope(sBody.envelope); }
1985 catch (e) { why = String(e && e.message || e); }
1986 check('a grant sealed by a STRANGER account is refused by the runner (open/verify throws)',
1987 /not sealed|not signed|not for this device/i.test(why));
1988
1989 // The spent-cid drop mirrors daimond.js `deliverGrant`: only an OUTSTANDING cid
1990 // resolves a waiting runner, and it is spent on first use, so a captured grant
1991 // replayed after the act ran authorises nothing.
1992 const outstanding = Object.create(null);
1993 outstanding['cidLIVE'] = { turnId: 't1' };
1994 function deliver(g) {
1995 const w = outstanding[g.cid];
1996 if (!w) return 'dropped'; // spent / unknown / replayed
1997 if (String(g.turnId) !== String(w.turnId)) return 'dropped';
1998 delete outstanding[g.cid]; // spend the cid
1999 return 'resolved';
2000 }
2001 check('a grant for an OUTSTANDING cid resolves the waiting runner exactly once',
2002 deliver({ cid: 'cidLIVE', turnId: 't1', verdict: 'allow' }) === 'resolved');
2003 check('the SAME grant REPLAYED after the cid is spent is dropped (authorises nothing)',
2004 deliver({ cid: 'cidLIVE', turnId: 't1', verdict: 'allow' }) === 'dropped');
2005 check('a grant for an UNKNOWN cid is dropped',
2006 deliver({ cid: 'nope', turnId: 't1', verdict: 'allow' }) === 'dropped');
2007 }
2008
2009 // The deps a park test runs an errand over: the turn SPENDS (increments runCount)
2010 // then egressAllowed aborts it for consent (runTurn throws), and parkRequested
2011 // tells runErrand to park rather than treat the abort as a crash.
2012 function parkDeps(sync, selfId, reports, runCounter, terminalGuard) {
2013 return {
2014 selfId, cas: P.syncCas(sync), now: () => 5000, maxParks: MAX,
2015 // FINISHED stands a device down once a TERMINAL report exists, mirroring
2016 // daimond.js: a device that collects the aborted report must not respend a
2017 // turn that already failed clean. Absent otherwise.
2018 finished: terminalGuard ? (async () => terminalGuard()) : undefined,
2019 reconstruct: async () => ({ chat: { id: 'c', messages: [] }, app: {} }),
2020 runTurn: async () => { if (runCounter) runCounter.n += 1; throw new Error('aborted for consent'); },
2021 parkRequested: () => ({ why: null }), // let the default sentences stand
2022 abort: () => {}, pushResult: async () => 1,
2023 post: async (r) => { reports.push(r); }, ack: async () => {},
2024 };
2025 }
2026
2027 // ── C. PARK reports then releases; parked below the bound, terminal at it. ──
2028 console.log('\nRemote consent — PARK reports then releases; terminal at MAX_PARKS');
2029 {
2030 phone.DaimondLease.forget();
2031 const sync = makeLeaseSync({});
2032 const reports = [], rc = { n: 0 };
2033 const errand = P.makeErrand({ turnId: 't-park', chatId: 'c', prompt: 'go', eid: 'e', deadline: 9e15, parkCount: 0 });
2034 const res = await P.runErrand(errand, parkDeps(sync, 'runnerDev', reports, rc));
2035 check('a park below the bound is NOT terminal', res.parked === true && res.terminal === false);
2036 check('the turn spent exactly once before parking', rc.n === 1);
2037 check('a park posts a PARKED report carrying the bumped GLOBAL count',
2038 reports.length === 1 && reports[0].status === 'parked' && reports[0].parkCount === 1);
2039 check('a park RELEASES the lease (not stranded)', sync.leases()['t-park'].mode === 'released');
2040 check('a park REPORTS before it RELEASES (mirror the reconstruct-fail order)',
2041 res.trace.indexOf('report') < res.trace.indexOf('release'));
2042
2043 phone.DaimondLease.forget();
2044 const sync2 = makeLeaseSync({});
2045 const reports2 = [], rc2 = { n: 0 };
2046 const errandN = P.makeErrand({ turnId: 't-term', chatId: 'c', prompt: 'go', eid: 'e2', deadline: 9e15, parkCount: MAX - 1 });
2047 const res2 = await P.runErrand(errandN, parkDeps(sync2, 'runnerDev', reports2, rc2));
2048 check('a park AT the bound is TERMINAL', res2.terminal === true && res2.parked === false);
2049 check('the terminal park posts an ABORTED report, the count at MAX_PARKS',
2050 reports2.length === 1 && reports2[0].status === 'aborted' && reports2[0].parkCount === MAX);
2051 check('the terminal report INFORMS the user (permission / did not run)',
2052 /permission/i.test(reports2[0].why) && /did not run/i.test(reports2[0].why));
2053 check('the terminal park RELEASES the lease (never stranded)', sync2.leases()['t-term'].mode === 'released');
2054 check('the terminal park REPORTS before it RELEASES',
2055 res2.trace.indexOf('report') < res2.trace.indexOf('release'));
2056 }
2057
2058 // ── D. GLOBAL parkCount: two devices re-dispatching cannot exceed MAX_PARKS. ──
2059 //
2060 // The money guarantee. The count rides the errand and the parked report (SYNCED),
2061 // so a re-dispatch bumps from the true GLOBAL total, and the single-runner lease
2062 // plus the terminal-report stand-down cap the respend at MAX_PARKS -- NOT MAX_PARKS
2063 // per device, which a device-local counter would have allowed.
2064 console.log('\nRemote consent — GLOBAL parkCount: two devices cannot exceed MAX_PARKS total');
2065 {
2066 phone.DaimondLease.forget(); laptop.DaimondLease.forget();
2067 const sync = makeLeaseSync({});
2068 const reports = [], rc = { n: 0 };
2069 const TID = 't-global';
2070 const terminal = () => reports.some((r) => r.turnId === TID && r.status === 'aborted');
2071
2072 // Dispatch #1 (parkCount 0): one runner spends and parks -> parked report, count 1.
2073 const r1 = await Pl.runErrand(
2074 P.makeErrand({ turnId: TID, chatId: 'c', prompt: 'go', eid: 'e0', deadline: 9e15, parkCount: 0 }),
2075 parkDeps(sync, 'lapDev', reports, rc, terminal));
2076 check('two-device: the first dispatch spends once and parks (GLOBAL count -> 1)',
2077 r1.parked === true && rc.n === 1 && reports[reports.length - 1].parkCount === 1);
2078
2079 // BOTH devices read the SAME parked report's count and re-dispatch errand(parkCount 1)
2080 // for the SAME turnId, over the ONE shared lease. Exactly one re-runs; the other
2081 // stands down on the terminal report the winner posts.
2082 const carried = reports[reports.length - 1].parkCount; // the GLOBAL count off the synced report
2083 check('two-device: the re-dispatch reads the count off the SYNCED report (not a device-local zero)',
2084 carried === 1);
2085 const rA = await phone.DaimondPeer.runErrand(
2086 P.makeErrand({ turnId: TID, chatId: 'c', prompt: 'go', eid: 'eA', deadline: 9e15, parkCount: carried }),
2087 parkDeps(sync, 'phoneDev', reports, rc, terminal));
2088 const rB = await laptop.DaimondPeer.runErrand(
2089 P.makeErrand({ turnId: TID, chatId: 'c', prompt: 'go', eid: 'eB', deadline: 9e15, parkCount: carried }),
2090 parkDeps(sync, 'lap2Dev', reports, rc, terminal));
2091 check('two-device: exactly ONE re-run spends; the other STANDS DOWN on the terminal report',
2092 rc.n === 2 && (rA.ran === false || rB.ran === false));
2093 check('two-device: the re-run is TERMINAL at MAX_PARKS (' + MAX + ')',
2094 (rA.terminal || rB.terminal) === true
2095 && reports.some((r) => r.status === 'aborted' && r.parkCount === MAX));
2096 check('two-device: TOTAL respends never exceed MAX_PARKS (' + MAX + ') — the spend cap holds',
2097 rc.n <= MAX);
2098 check('two-device: the lease is RELEASED after the loop (not stranded)',
2099 sync.leases()[TID].mode === 'released');
2100 }
2101
2102 // ── E. Policy composition: a covered act never asks. ──
2103 console.log('\nRemote consent — policy composition: a covered act never asks');
2104 {
2105 const now = 1700000000000;
2106 const attended = { lap: { name: 'lap', lastSeen: now - 1000, attended: true, attendedAt: now - 1000 } };
2107 const cov = P.consentRouteDecision(attended, 'self', now, { covered: true });
2108 check('a COVERED act resolves ALLOW with no ask (composes with the synced policy)',
2109 cov.action === 'allow' && cov.verdict === 'allow');
2110 check('a covered act never asks even with an attended peer present',
2111 P.consentRouteDecision(attended, 'self', now, { covered: true }).action !== 'ask');
2112 const unc = P.consentRouteDecision(attended, 'self', now, { covered: false });
2113 check('an UNCOVERED act with an attended peer ASKS that peer',
2114 unc.action === 'ask' && unc.peer && unc.peer.deviceId === 'lap');
2115 }
2116
2117 // ── F. Attended-only routing: an awake-but-unwatched device parks. ──
2118 console.log('\nRemote consent — attended-only routing; an awake-but-unwatched device parks');
2119 {
2120 const now = 1700000000000;
2121 const awakeNotAttended = { lap: { name: 'lap', lastSeen: now - 1000, attended: false, attendedAt: 0 } };
2122 check('an AWAKE but unattended peer is NOT asked -> park',
2123 P.consentRouteDecision(awakeNotAttended, 'self', now, { covered: false }).action === 'park');
2124 check('attendedPeer is null for an awake-but-unattended map',
2125 P.attendedPeer(awakeNotAttended, 'self', now) === null);
2126 const attendedFresh = { lap: { name: 'lap', lastSeen: now - 1000, attended: true, attendedAt: now - 1000 } };
2127 check('a FRESH attended peer is routable',
2128 P.attendedPeer(attendedFresh, 'self', now).deviceId === 'lap');
2129 const attendedStale = { lap: { name: 'lap', lastSeen: now - 1000, attended: true, attendedAt: now - 10 * 60 * 1000 } };
2130 check('an attended peer whose attention has AGED OUT is not routable -> park',
2131 P.consentRouteDecision(attendedStale, 'self', now, { covered: false }).action === 'park');
2132 const indeterminate = { lap: { name: 'lap', lastSeen: now - 1000 } }; // no attention signal
2133 check('attention-INDETERMINATE (no signal) fails SAFE to park',
2134 P.consentRouteDecision(indeterminate, 'self', now, { covered: false }).action === 'park');
2135 }
2136}
2137
2138/// The mock model turn. `runTurn` (daimond.js:17552) is the real engine the peer
2139/// runs; step 1 stands a deterministic answer in for the provider call, because
2140/// the seam -- not the model -- is what is under test.
2141function mockRunTurn(prompt) {
2142 return 'The answer to "' + prompt + '" is 4.';
2143}
2144
2145main().catch((e) => { console.error('test crashed:', e); process.exitCode = 1; });