import test from 'node:test'; import assert from 'node:assert/strict'; import { MetadataIndex } from '../src/adapters/metadata-index.js'; import { lineageEvents, reduce } from '../src/orgchain/lineage.js'; import { LAB_KEY, decodeLab, emptyLab, encodeLab, mutate, setTempo, tick } from '../src/orgchain/store.js'; import { SAMPLE, act, seedSample, send } from '../src/orgchain/actions.js'; import { SAT } from '../src/orgchain/money.js'; const state = (lab, options = { pending: true }) => reduce(lineageEvents(new MetadataIndex(lab.chain)), options); test('at the default tempo each act is its own beat, and the first act funds the lab', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' }); assert.equal(lab.chain.blocks.length, 3); // funding, founding, birth const s = state(lab, {}); assert.deepEqual([root, ops].map(id => s.nodes.get(id).born), [2, 3]); assert.equal(lab.chain.mempool.length, 0); const record = lab.chain.transactions.get(ops.slice(0, -2)).inscription; assert.equal(record.text, 'birth ops'); assert.equal(record.metaprotocol, 'lineage/v0'); assert.equal(record.parent, root); }); test('a timed tempo cues acts, chains cued spends of one parent, and beats once when due', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }); setTempo(lab, 10, 1000); const a = act(lab, 'birth', root, { name: 'a' }, 2000), b = act(lab, 'birth', root, { name: 'b' }, 3000); send(lab, root, a, SAT, 3500); // a pays its own gas, out of money still cued act(lab, 'birth', a, { name: 'c', kind: 'agent' }, 4000); assert.deepEqual(lab.clock, { tempo: 10, nextAt: 12000 }); assert.equal(state(lab, {}).nodes.has(a), false); assert.equal(state(lab).nodes.get(b).born, 'cued'); assert.equal(tick(lab, 11999), false); assert.equal(tick(lab, 12000), true); assert.deepEqual(lab.clock, { tempo: 10, nextAt: null }); assert.deepEqual([a, b].map(id => state(lab, {}).nodes.get(id).born), [3, 3]); assert.equal(tick(lab, 99999), false); // nothing cued: no empty beats }); test('manual waits for a beat; switching back to each lands the cue', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }); setTempo(lab, 'manual'); act(lab, 'decide', root, { title: 'Ship weekly', why: '' }); assert.equal(tick(lab, Date.now() + 1e9), false); assert.equal(lab.chain.mempool.length, 3); // a gas top-up from acme's money, then the commit and the reveal setTempo(lab, 'each'); assert.equal(lab.chain.mempool.length, 0); assert.deepEqual(state(lab, {}).nodes.get(root).decisions.map(d => [d.title, d.why]), [['Ship weekly', '']]); }); test('the reducer vets an act before anything is signed', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }), bot = act(lab, 'birth', root, { name: 'bot', kind: 'agent' }); const before = encodeLab(lab); assert.throws(() => act(lab, 'birth', bot, { name: 'x' }), /^Error: Agents do not give birth\.$/); assert.throws(() => act(lab, 'found', null, { name: 'acme' }), /already named acme/); assert.throws(() => act(lab, 'award', root, { to: bot, skill: 'launch', belt: 'gold' }), /belt/); assert.throws(() => act(lab, 'graduate', root, { of: bot }), /Only orgs graduate/); assert.throws(() => act(lab, 'sing', root, {}), /Unknown verb/); assert.equal(encodeLab(lab), before); }); test('closing spends the node; derived children close with it and graduated ones stay open', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }), studio = act(lab, 'birth', root, { name: 'studio' }); send(lab, root, studio, SAT); const kid = act(lab, 'birth', studio, { name: 'kid' }), grad = act(lab, 'birth', studio, { name: 'grad' }); send(lab, studio, grad, 0.1 * SAT); act(lab, 'graduate', studio, { of: grad }); const close = act(lab, 'close', studio, {}); const s = state(lab, {}); assert.deepEqual([studio, kid, grad].map(id => s.nodes.get(id).status), ['closed', 'closed', 'active']); assert.equal(s.nodes.get(grad).edge, 'adopted'); assert.equal(lab.chain.transactions.get(close.slice(0, -2)).inscription.parent, studio); assert.throws(() => act(lab, 'birth', studio, { name: 'again' }), /parent is closed/); act(lab, 'decide', grad, { title: 'Carry on' }); }); test('the yolopreneur sample is six beats of the drawing in docs/lineage.md', () => { const lab = emptyLab(); const root = seedSample(lab); assert.equal(lab.chain.blocks.length, 7); // funding + six const s = state(lab, {}), named = name => [...s.nodes.values()].find(x => x.name === name); assert.ok(s.log.every(e => e.ok)); assert.equal(s.nodes.get(root).name, SAMPLE); assert.deepEqual([...s.nodes.values()].filter(x => x.root === root).length, 12); assert.equal(named('lineage').edge, 'adopted'); assert.deepEqual(named('sol').badges.map(b => `${b.skill}:${b.belt}`), ['launch:green']); assert.deepEqual(named('time').children.map(id => s.nodes.get(id).name), ['luna', 'sol', 'astra', 'docscope', 'dollhouse', 'doodleloop', 'hearthframe']); assert.deepEqual([2, 3, 4, 5].map(upTo => state(lab, { upTo }).nodes.size), [1, 3, 8, 12]); assert.throws(() => seedSample(lab), /already named yolopreneur/); }); test('labs round-trip, refuse foreign backups, and share one lock across tabs', async () => { const lab = emptyLab(); act(lab, 'found', null, { name: 'acme' }); setTempo(lab, 30, 1000); const copy = decodeLab(encodeLab(lab)); assert.deepEqual(copy.clock, { tempo: 30, nextAt: null }); assert.equal(copy.chain.blocks.length, 2); assert.equal(copy.wallet.secret, lab.wallet.secret); assert.throws(() => decodeLab(JSON.stringify({ kind: 'test-lab', network: 'super-testnet-local-v0', version: 1 })), /not an Orgchain v1 lab/); assert.throws(() => decodeLab('{bad')); assert.throws(() => decodeLab(encodeLab(lab).replace('"tempo":30', '"tempo":15')), /Invalid clock/); let stored = encodeLab(emptyLab()), tail = Promise.resolve(), writes = 0; const names = new Set(); const io = { read: () => decodeLab(stored), save: l => { stored = encodeLab(l); writes++; }, locks: { request(name, fn) { names.add(name); const next = tail.then(fn); tail = next.catch(() => {}); return next; } } }; await Promise.all([mutate(l => act(l, 'found', null, { name: 'one' }), io), mutate(l => act(l, 'found', null, { name: 'two' }), io)]); assert.deepEqual([...names], [LAB_KEY]); assert.deepEqual(state(decodeLab(stored)).roots.map(r => r.name), ['one', 'two']); const { result } = await mutate(l => tick(l), io); assert.equal(result, false); assert.equal(writes, 2); await assert.rejects(mutate(l => act(l, 'found', null, { name: 'one' }), io), /already named one/); assert.equal(writes, 2); });