115 lines
6.4 KiB
JavaScript
115 lines
6.4 KiB
JavaScript
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 } from '../src/orgchain/actions.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);
|
||
|
|
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, 2);
|
||
|
|
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' });
|
||
|
|
const kid = act(lab, 'birth', studio, { name: 'kid' }), grad = act(lab, 'birth', studio, { name: 'grad' });
|
||
|
|
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);
|
||
|
|
});
|