Files
orgchain/tests/lab.test.js
T

123 lines
7.1 KiB
JavaScript
Raw Normal View History

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 give birth only once they graduate\.$/);
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', bot, { of: root }), /Only its parent can graduate a child/);
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, 11);
assert.equal(named('lineage').edge, 'adopted');
assert.deepEqual(s.roots.map(r => r.name), ['yolopreneur', 'lineage']); // graduated: a holding of its own
assert.deepEqual(named('sol').badges.map(b => `${b.skill}:${b.belt}`), ['launch:green']);
// Three keyrings; lineage's retires when it graduates, since its keys change with its root.
assert.deepEqual(['yolopreneur', 'sol', 'lineage'].map(x => [named(x).keyring.beat, named(x).keyring.retired?.cause ?? null]),
[[3, null], [6, null], [6, 'graduate']]);
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);
});