Files
orgchain/tests/lab.test.js
T
teahubxyzandClaude Opus 5.5 6998ae47aa Orgchain v1.10: keyrings, each key signing in its own chain's standard
A new lineage/v0 op binds a node's keys in one inscription whose parent is the
node itself: its Taproot and SegWit addresses, its npub and its Ethereum
address. Every key signs the same statement the way its own wallets sign a
message: BIP-322 simple signatures for the Bitcoin addresses, a NIP-01 note
(kind 1, created_at 0) for the npub, EIP-191 for Ethereum. The reducer checks
every proof, binds a standing keyring's keys to its node, lets the newest
keyring replace the older, and retires keyrings at a graduation (the keys
change with the new root) and at a close.

Every account gets an Ethereum key at m/44'/60'/account'/0/0; an org's, at
account 0, is the key tree page's legal-control key and matches its published
vector. Keyring is the ninth verb; Inspect shows each node's keyring and the
statement its keys signed, and Reveal its Ethereum private key. The sample
publishes three keyrings, and lineage's retires when it graduates.

Tests pin BIP-322's published vectors and values cross-checked with
bip322-js, ethers and nostr-tools. The regtest rehearsal, rerun with five
keyrings, confirmed 89 transactions on Bitcoin Core; ord's 26 events fold to
the same trees and keyrings, every proof checked from ord's copy, and the
fixture is re-recorded.

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

124 lines
7.1 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, 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, 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);
});