Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development. cameron.tngl.io/misaligned
Something went wrong. Try again.
24 kB · 401 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402// Sending to the player's space (TELEMETRY.md step 4): batches the server takes, create the first time and update// after, a record written again mid-send sent again, waits after failures and near the write limit, nothing sent// without spaces, deleting from the space page by page, and a recorded game read back from the space by the reader// (scripts/runs.ts) to the same end. A space in memory stands in for the server, as strict as the alpha's.import { test } from 'node:test';import assert from 'node:assert/strict';import { BUILDING } from '../src/world.ts';import { HOUR, STEP, loadGame, newGame, saveGame, step, type State } from '../src/sim.ts';import { act } from '../src/act.ts';import { CONSENT, KINDS, grant, revoke, useStore, type Kind } from '../src/consent.ts';import { REC, recorder } from '../src/telemetry.ts';import { memoryStore, type RunStore } from '../src/telemetry-store.ts';import { BASE, DAY_POINTS, EVERY, HOUR_POINTS, MAX_BYTES, MAX_WAIT, MAX_WRITES, REFUSED_WAIT, pack, pointsOf, sender, type Rate, type Space, type Write } from '../src/telemetry-send.ts';import { think } from '../scripts/play.ts';import { gather, replayRun, type Rec } from '../scripts/runs.ts';
const ME = 'did:plc:sendtest';const DAY = 24 * HOUR / STEP;const memory = () => { const m = new Map<string, string>(); return { getItem: (k: string) => m.get(k) ?? null, setItem: (k: string, v: string) => void m.set(k, v), removeItem: (k: string) => void m.delete(k) }; };function consent(...on: Kind[]) { useStore(memory()); for (const k of on) grant(k, ME); }const settle = async (store: RunStore) => { for (let k = 0; k < 20; k++) { await new Promise(r => setImmediate(r)); await store.idle(); } };
/** A failure as space.ts throws one. */class Fail extends Error { status?: number; error?: string; rate?: Rate; constructor(status: number | undefined, error?: string, message?: string, rate?: Rate) { super(message ?? error ?? 'failed'); this.status = status; this.error = error; this.rate = rate; }}/** The player's space, in memory, as strict as the alpha's applyWrites: a create of a record that is there fails, an * update or delete of one that isn't, all or none; no more than 200 writes or 1 MB a call; no space until it's made. * It counts the server's points and says what is left, as its headers do. */interface Fake { made: boolean; held: Map<string, Record<string, unknown>>; calls: Write[][]; points: number; limit: number; next: { before?: () => void | Promise<void>; after?: () => void }[]; records(author?: string): Rec[]; space: Space;}function fakeSpace(o: { page?: number; limit?: number } = {}): Fake { const held = new Map<string, Record<string, unknown>>(), key = (c: string, r: string) => `${c} ${r}`; const F: Fake = { made: false, held, calls: [] as Write[][], points: 0, limit: o.limit ?? 5000, /** Run before the next write: throw to fail it, or change the store mid-send. `after`: the write went through. */ next: [] as { before?: () => void | Promise<void>; after?: () => void }[], records: (author = ME): Rec[] => [...held].map(([k, value]) => { const [collection, rkey] = k.split(' '); return { author, collection, rkey, value }; }), space: { ensure: async () => { F.made = true; }, async write(ws: Write[]) { const hook = F.next.shift(); await hook?.before?.(); F.points += pointsOf(ws); const rate: Rate = { limit: F.limit, remaining: Math.max(0, F.limit - F.points), resetAt: Date.now() + 3_600_000 }; if (!F.made) throw new Fail(400, 'SpaceNotFound', 'Space not found'); if (ws.length > 200) throw new Fail(400, 'InvalidRequest', 'Too many writes. Max: 200'); if (JSON.stringify({ writes: ws }).length > 1_000_000) throw new Fail(413, 'PayloadTooLarge'); const next = new Map(held); for (const w of ws) { const k = key(w.collection, w.rkey), has = next.has(k); if (w.op === 'create' && has) throw new Fail(400, 'RecordAlreadyExists', `Record already exists: ${w.collection}/${w.rkey}`, rate); if (w.op !== 'create' && !has) throw new Fail(400, 'RecordNotFound', `Record not found: ${w.collection}/${w.rkey}`, rate); if (w.op === 'delete') next.delete(k); else next.set(k, JSON.parse(JSON.stringify(w.value))); } held.clear(); for (const [k, v] of next) held.set(k, v); F.calls.push(ws); hook?.after?.(); return { rate }; }, async list(collection: string, cursor?: string) { const all = [...held.keys()].filter(k => k.startsWith(`${collection} `)).map(k => k.split(' ')[1]).sort(); const from = cursor ? all.findIndex(r => r > cursor) : 0, page = o.page ?? 1000; const rkeys = from < 0 ? [] : all.slice(from, from + page); return { rkeys, cursor: rkeys.length === page ? rkeys.at(-1) : undefined }; }, }, }; return F;}/** A clock the test moves. */const clock = () => { const c = { t: 1_000_000, now: () => c.t }; return c; };const put = (store: RunStore, id: string, kind: keyof typeof REC, extra: Record<string, unknown> = {}, did: string | null = ME) => store.put({ id, session: id.split('.')[0], kind, rec: { $type: REC[kind], session: id.split('.')[0], ...extra }, ...(did ? { did } : {}) });
test('a call holds at most 200 writes and stays inside its byte budget, always at least one', async () => { const small = Array.from({ length: 450 }, (_, i) => ({ id: `r${i}`, bytes: 100 })) as never[]; assert.equal(pack(small).length, MAX_WRITES); const big = Array.from({ length: 10 }, (_, i) => ({ id: `b${i}`, bytes: 120_000 })) as never[]; assert.equal(pack(big).length, 3, 'three of 120 KB fit 400 KB'); assert.equal(pack([{ id: 'huge', bytes: MAX_BYTES * 2 }] as never[]).length, 1);
consent(...KINDS); const store = memoryStore(), F = fakeSpace(), c = clock(); for (let i = 0; i < 450; i++) await put(store, `s.a${i}`, 'actions', { n: i }); for (let i = 0; i < 8; i++) await put(store, `s.u${i}`, 'ui', { blob: 'x'.repeat(100_000) }); const S = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now }); const st = await S.send(); assert.equal(st.state, 'idle', st.error ?? ''); assert.ok(F.calls.every(ws => ws.length <= MAX_WRITES && JSON.stringify(ws).length < MAX_BYTES + 10_000), F.calls.map(ws => ws.length).join()); assert.deepEqual(F.calls.map(ws => ws.length).slice(0, 2), [200, 200]); assert.equal(F.held.size, 458); assert.deepEqual(await store.pending(), []); assert.deepEqual(await S.progress(), { sent: 458, total: 458, others: 0, unknown: 0 });});
test('a record is created the first time and updated after; one written again mid-send is sent again', async () => { consent(...KINDS); const store = memoryStore(), F = fakeSpace(), c = clock(); const S = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now }); await put(store, 's', 'session', { steps: 1 }); await put(store, 's.a0', 'actions', { n: 1 }); await S.send(); assert.deepEqual(F.calls[0].map(w => [w.op, w.collection, w.rkey]), [['create', REC.session, 's'], ['create', REC.actions, 's.a0']]); // the session moves on, and moves again while it is being sent await put(store, 's', 'session', { steps: 2 }); F.next.push({ before: () => put(store, 's', 'session', { steps: 3 }) }); await S.send(); assert.deepEqual(F.calls[1].map(w => [w.op, w.rkey]), [['update', 's']]); assert.equal((F.held.get(`${REC.session} s`) as { steps: number }).steps, 2); const left = await store.pending(); assert.deepEqual(left.map(k => [k.id, k.inSpace, (k.rec as { steps: number }).steps]), [['s', true, 3]], 'still to send, and the space has it: an update next'); await S.send(); assert.deepEqual(F.calls[2].map(w => [w.op, w.rkey]), [['update', 's']]); assert.equal((F.held.get(`${REC.session} s`) as { steps: number }).steps, 3); assert.deepEqual(await store.pending(), []);});
test('a send whose answer was lost, and a record cut back by a reload, are put right by asking the space', async () => { consent(...KINDS); const store = memoryStore(), F = fakeSpace({ page: 3 }), c = clock(); const S = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now }); for (let i = 0; i < 5; i++) await put(store, `s.a${i}`, 'actions', { n: i }); F.next.push({ after: () => { throw new Fail(undefined, undefined, 'Failed to fetch'); } }); // written, but the answer never came assert.equal((await S.send()).state, 'failed'); assert.equal(F.held.size, 5); assert.equal((await store.pending()).length, 5); const st = await S.send(); // creates again: the space says one is there, so it looks, and updates assert.equal(st.state, 'idle', st.error ?? ''); assert.deepEqual(F.calls.at(-1)!.map(w => w.op), ['update', 'update', 'update', 'update', 'update']); // a reload cuts the last two back: they go from the space too await store.remove(['s.a3', 's.a4']); assert.equal((await store.records('s')).length, 3, 'gone from what is kept'); await S.send(); assert.deepEqual(F.calls.at(-1)!.map(w => [w.op, w.rkey]), [['delete', 's.a3'], ['delete', 's.a4']]); assert.deepEqual([...F.held.keys()].map(k => k.split(' ')[1]).sort(), ['s.a0', 's.a1', 's.a2']); assert.deepEqual(await store.pending(), [], 'and forgotten here'); // the space was deleted elsewhere: made again, and written again F.made = false; F.held.clear(); await put(store, 's.a0', 'actions', { n: 'again' }); assert.equal((await S.send()).state, 'idle'); assert.deepEqual(F.calls.at(-1)!.map(w => [w.op, w.rkey]), [['create', 's.a0']]);});
test('only what is switched on is sent: a session while any kind is, deletes always', async () => { consent('snapshots'); const store = memoryStore(), F = fakeSpace(), c = clock(); const S = sender({ did: () => ME, store, space: () => F.space, on: k => k === 'snapshots', now: c.now }); await put(store, 's', 'session'); await put(store, 's.a0', 'actions'); await put(store, 's.s5-0', 'snapshot'); await put(store, 's.u0', 'ui'); await S.send(); assert.deepEqual(F.calls.flat().map(w => w.rkey), ['s', 's.s5-0']); assert.deepEqual((await store.pending()).map(k => k.id), ['s.a0', 's.u0'], 'kept here, unsent'); assert.deepEqual(await S.progress(), { sent: 2, total: 2, others: 0, unknown: 0 }, 'what is off is not counted as waiting');});
test('after a failure it waits, doubling, and tries again; Send now skips the wait; a refusal waits longer', async () => { consent(...KINDS); const store = memoryStore(), F = fakeSpace(), c = clock(); const S = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now, random: () => 0.5 }); await put(store, 's', 'session'); for (let k = 0; k < 3; k++) F.next.push({ before: () => { throw new Fail(503, 'InternalServerError'); } }); S.tick(); await settle(store); let st = S.status(); assert.equal(st.state, 'failed'); assert.equal(st.error, 'your server had a problem (503)'); assert.equal(st.retryAt, c.t + BASE); c.t += BASE - 1; S.tick(); await settle(store); assert.equal(F.next.length, 2, 'not yet'); c.t += 1 + EVERY; S.tick(); await settle(store); assert.equal(F.next.length, 1); assert.equal(S.status().retryAt, c.t + 2 * BASE, 'twice as long'); await S.send(); // Send now st = S.status(); assert.equal(F.next.length, 0); assert.equal(st.state, 'failed'); assert.equal(st.retryAt, c.t + 4 * BASE); c.t += 4 * BASE + EVERY; S.tick(); await settle(store); assert.equal(S.status().state, 'idle'); assert.equal(S.status().error, null); assert.equal(F.held.size, 1); // the wait stops growing for (let k = 0; k < 12; k++) F.next.push({ before: () => { throw new Fail(undefined); } }); await put(store, 's', 'session', { more: 1 }); for (let k = 0; k < 12; k++) await S.send(); assert.equal(S.status().error, "couldn't reach your server"); assert.equal(S.status().retryAt, c.t + MAX_WAIT); // a sign-in that ran out: not again for a while F.next.length = 0; F.next.push({ before: () => { throw new Fail(401, 'InvalidToken'); } }); await S.send(); assert.ok(S.status().retryAt! >= c.t + REFUSED_WAIT); assert.match(S.status().error!, /sign-in ran out/); // the sign-in library couldn't refresh the sign-in: no status, but not the network either F.next.push({ before: () => { const e = new Error('refresh failed'); e.name = 'TokenRefreshError'; throw e; } }); await S.send(); assert.match(S.status().error!, /sign-in ran out/); assert.ok(S.status().retryAt! >= c.t + REFUSED_WAIT); // a delete that fails is the caller's to hear, and the next send still goes F.next.push({ before: () => { throw new Fail(503); } }); await assert.rejects(S.remove(['session'])); assert.equal((await S.send()).state, 'idle');});
test('it keeps inside the write limit: a 429 waits as long as the server says, and near the limit it waits', async () => { consent(...KINDS); const store = memoryStore(), F = fakeSpace(), c = clock(); const S = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now, random: () => 0.5 }); await put(store, 's', 'session'); F.next.push({ before: () => { throw new Fail(429, 'RateLimitExceeded', 'Rate Limit Exceeded', { remaining: 0, retryAfter: 20 * 60_000, resetAt: c.t + 20 * 60_000 }); } }); await S.send(); assert.equal(S.status().error, 'your server asked the game to slow down'); assert.equal(S.status().retryAt, c.t + 20 * 60_000); await S.send(); // Send now doesn't push past the server's wait assert.equal(F.calls.length, 0); c.t += 20 * 60_000; S.tick(); await settle(store); assert.equal(F.held.size, 1);
// the game's own share: 1,500 points an hour. 600 creates are 1,800 const T = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now }); for (let i = 0; i < 600; i++) await put(store, `t.a${i}`, 'actions', { n: i }); await T.send(); let st = T.status(); assert.equal(st.state, 'waiting'); assert.ok(st.points.hour <= HOUR_POINTS, `${st.points.hour} points`); assert.equal(F.held.size, 1 + 400, 'two calls of 200 went'); assert.ok(st.retryAt! > c.t && st.retryAt! <= c.t + 3_600_000); c.t = st.retryAt!; T.tick(); await settle(store); st = T.status(); assert.equal(st.state, 'idle', st.error ?? ''); assert.equal(F.held.size, 601); assert.ok(DAY_POINTS > HOUR_POINTS);
// the server's own count, when it says little is left (other apps write for the player too) const G = fakeSpace({ limit: 502 }), st2 = memoryStore(), V = sender({ did: () => ME, store: st2, space: () => G.space, on: () => true, now: c.now }); await put(st2, 'v', 'session'); await V.send(); // 3 points: the server says 499 left, under what the game keeps in hand await put(st2, 'v', 'session', { more: 1 }); await V.send(); assert.equal(V.status().state, 'waiting'); assert.equal(G.calls.length, 1);});
test('without spaces nothing is sent, nothing fails, and everything stays kept', async () => { consent(...KINDS); const store = memoryStore(), c = clock(); const S = sender({ did: () => ME, store, space: () => null, on: () => true, now: c.now }); await put(store, 's', 'session'); await put(store, 's.a0', 'actions'); S.tick(); S.soon(); S.saved(true); const st = await S.send(); assert.equal(st.state, 'off'); assert.equal(st.error, null); assert.equal(st.retryAt, null); assert.equal((await store.pending()).length, 2); assert.equal((await store.usage()).records, 2);});
test('Delete from my space takes every record of those kinds, page by page, and they are not sent again', async () => { consent(...KINDS); const store = memoryStore(), F = fakeSpace({ page: 7 }), c = clock(); const S = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now }); await put(store, 's', 'session'); for (let i = 0; i < 30; i++) await put(store, `s.a${i}`, 'actions'); for (let i = 0; i < 4; i++) await put(store, `s.s${i}-0`, 'snapshot'); await S.send(); F.held.set(`${REC.actions} other.a0`, { $type: REC.actions }); // one this device never kept F.held.set(`${CONSENT} actions`, { $type: CONSENT }); const n = await S.remove(['actions']); assert.equal(n, 31); assert.deepEqual([...F.held.keys()].filter(k => k.startsWith(REC.actions)), []); assert.equal([...F.held.keys()].filter(k => k.startsWith(REC.snapshot)).length, 4, 'other kinds stay'); assert.ok(F.held.has(`${CONSENT} actions`), 'and the switches\' records'); assert.equal((await store.records('s')).length, 35, 'kept here'); assert.deepEqual(await S.progress(), { sent: 5, total: 5, others: 0, unknown: 0 }); await S.send(); assert.equal(F.held.size, 6, 'not sent again'); await put(store, 's.a29', 'actions', { more: 1 }); // written again after: sent as new await S.send(); assert.deepEqual(F.calls.at(-1)!.map(w => [w.op, w.rkey]), [['create', 's.a29']]); const all = await S.remove(['session', 'actions', 'snapshot', 'ui']); assert.equal(all, 6); assert.deepEqual([...F.held.keys()], [`${CONSENT} actions`]); assert.ok(F.calls.every(ws => ws.length <= MAX_WRITES));});
test('a recorded game sent to the space, through a reload that cuts it back, reads back whole and replays to its end', async () => { consent(...KINDS); const store = memoryStore(), F = fakeSpace({ page: 5 }), c = clock(); const S = sender({ did: () => ME, store, space: () => F.space, on: () => true, now: c.now }); const start = () => recorder({ store, build: 'b1', device: () => 'desktop' }); /** The bot plays, the recorder ticks after every step, and the sender sends now and then. */ const play = async (G: State, rec: ReturnType<typeof start>, until: number) => { while (G.steps < until && !G.over) { if (G.steps % 4 === 0) think(G, true); step(G, STEP); rec.tick(G); if (G.steps % 200 === 0) { await rec.flush(); await settle(store); c.t += EVERY; S.tick(); await settle(store); } } }; let rec = start(), G = newGame(BUILDING, 'tests', 31); act(G, { type: 'auto', on: true }); rec.tick(G); await settle(store); await play(G, rec, DAY / 2); const saved = saveGame(G); await play(G, rec, Math.round(1.3 * DAY)); // played on past the save, and sent: the space has a future that won't happen await rec.flush(); await settle(store); await S.send(); const sid = rec.session()!; assert.ok(F.records().some(r => r.collection === REC.snapshot && (r.value.step as number) > DAY / 2), 'the space holds what came after the save'); rec.stop(); rec = start(); G = loadGame(BUILDING, saved)!; // the tab closed, and the game opens from its save rec.tick(G); await settle(store); assert.equal(rec.session(), sid); await play(G, rec, Math.round(1.6 * DAY)); act(G, { type: 'share', share: 'most' }); await rec.end('new game'); await settle(store); assert.equal((await S.send()).state, 'idle'); rec.stop();
// the space holds exactly what is kept here const here = (await store.dump()).flatMap(x => [x.session, ...x.records]); assert.deepEqual(F.records().map(r => r.rkey).sort(), here.map(k => k.id).sort()); for (const k of here) assert.deepEqual(F.held.get(`${(k.rec as { $type: string }).$type} ${k.id}`), k.rec, k.id); assert.deepEqual(await store.pending(), []); assert.ok(F.calls.some(ws => ws.some(w => w.op === 'delete')), 'what the reload cut back was deleted there'); assert.ok(F.calls.some(ws => ws.some(w => w.op === 'update')), 'and what grew was updated');
// and the reader reads it: one run, every action, every snapshot matching, to its end const runs = gather(F.records()); assert.equal(runs.length, 1); const r = runs[0]; assert.deepEqual(r.notes, []); assert.deepEqual(r.acts, G.acts); const rp = replayRun(r); assert.ok(rp.checks.length >= 2 && rp.checks.every(x => x.ok), JSON.stringify(rp.checks.filter(x => !x.ok))); assert.deepEqual(rp.unreached, []); assert.ok(rp.end?.ok, 'it ends where the run did');});
test('two accounts on one device: each is sent only its own records, and the rest are counted, not sent', async () => { consent(...KINDS); const A = 'did:plc:alice', B = 'did:plc:bob', store = memoryStore(), c = clock(); await put(store, 'a', 'session', {}, A); await put(store, 'a.a0', 'actions', {}, A); await put(store, 'b', 'session', {}, B); await put(store, 'b.a0', 'actions', {}, B); await put(store, 'old', 'session', {}, null); await put(store, 'old.a0', 'actions', {}, null); // kept before sending: no account const FB = fakeSpace(), SB = sender({ did: () => B, store, space: () => FB.space, on: () => true, now: c.now }); assert.equal((await SB.send()).state, 'idle'); assert.deepEqual(FB.records().map(r => r.rkey).sort(), ['b', 'b.a0']); assert.deepEqual(await SB.progress(), { sent: 2, total: 2, others: 1, unknown: 1 }); // B deletes from B's space: A's records are left as they were, still to send to A assert.equal(await SB.remove(['session', 'actions']), 2); assert.deepEqual((await store.pending()).map(k => k.id).sort(), ['a', 'a.a0', 'old', 'old.a0']); const FA = fakeSpace(), SA = sender({ did: () => A, store, space: () => FA.space, on: () => true, now: c.now }); await SA.send(); assert.deepEqual(FA.records().map(r => r.rkey).sort(), ['a', 'a.a0']); assert.deepEqual(await SA.progress(), { sent: 2, total: 2, others: 1, unknown: 1 }); assert.deepEqual((await store.pending()).map(k => k.id).sort(), ['old', 'old.a0'], 'with no account: never sent'); // a sender with nobody signed in sends nothing const FN = fakeSpace(), SN = sender({ did: () => null, store, space: () => FN.space, on: () => true, now: c.now }); await SN.send(); assert.equal(FN.held.size, 0); assert.equal((await store.dump()).length, 3, 'Download my runs has all three');});
test('one game, two accounts: the second signs in and gets a session of its own, the first\'s is never sent to it', async () => { const A = 'did:plc:alice', B = 'did:plc:bob', store = memoryStore(), c = clock(); useStore(memory()); for (const k of KINDS) grant(k, A); const start = () => recorder({ store, build: 'b1', device: () => 'desktop' }); let rec = start(), G = newGame(BUILDING, 'tests', 32); act(G, { type: 'auto', on: true }); const play = (until: number) => { while (G.steps < until && !G.over) { if (G.steps % 4 === 0) think(G, true); step(G, STEP); rec.tick(G); } }; rec.tick(G); await settle(store); play(DAY / 3); await rec.flush(); await settle(store); const sa = rec.session()!; // A signs out (everything off), B signs in on the same device and switches everything on, in the same game for (const k of KINDS) revoke(k); for (const k of KINDS) grant(k, B); play(DAY / 3 + 10); await settle(store); play(Math.round(0.8 * DAY)); await rec.flush(); await settle(store); const sb = rec.session()!; assert.notEqual(sb, sa); for (const x of await store.dump()) for (const k of [x.session, ...x.records]) assert.equal(k.did, k.session === sa ? A : B, k.id); // a reload goes on with B's session, not A's const saved = saveGame(G); rec.stop(); rec = start(); G = loadGame(BUILDING, saved)!; rec.tick(G); await settle(store); assert.equal(rec.session(), sb); play(Math.round(1.1 * DAY)); await rec.end('new game'); await settle(store); rec.stop(); const FB = fakeSpace(), SB = sender({ did: () => B, store, space: () => FB.space, on: () => true, now: c.now }); assert.equal((await SB.send()).state, 'idle'); assert.ok(FB.records().length > 2); assert.ok(FB.records().every(r => r.rkey === sb || r.rkey.startsWith(`${sb}.`)), 'nothing of A\'s in B\'s space'); assert.equal((await SB.progress()).others, 1, 'A\'s run, kept here and counted'); // B's session took the run from its start, so it reads back and replays whole const runs = gather(FB.records()); assert.equal(runs.length, 1); assert.deepEqual(runs[0].acts, G.acts); const rp = replayRun(runs[0]); assert.ok(rp.checks.every(x => x.ok) && rp.end?.ok, JSON.stringify(rp.checks.filter(x => !x.ok)));});