Files
orgchain/tests/lab.test.js
T
teahubxyzandClaude Opus 5.5 6acc319ee7 Orgchain v1: create and operate lineage holdings across time
Super Testnet's kitchen sink becomes lineage's app. Lineage gives the
structure; the chain adds time.

- Holdings home, a tree drawn like docs/lineage.md, act and inspect panels,
  and a timeline grouped by beat.
- Verbs: found, birth, award, decide, graduate, close. Each is a lineage/v0
  inscription whose parent is the node that authorizes it.
- One reducer (src/orgchain/lineage.js) folds the events into trees and vets
  every act before it is signed. Closing cascades through derived children;
  graduated (adopted) ones stay open.
- Time: tempo each act / 10s / 30s / 60s / manual, Beat now, and a view
  slider with replay. A beat is a block; cued acts wait in the mempool.
- Its own lab (orgchain:lab:v1) with save, load and reset; a yolopreneur
  sample of six beats.
- Removed: the recursive preview, explorer UI, server, Docker and Fly files.
  The engine (src/adapters, src/upstream, storage, miner) is unchanged.
- Tests: reducer and lab (node --test), plus a Playwright spec for desktop
  and mobile that also runs against a deployed copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
2026-10-14 17:28:33 -07:00

116 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);
});