Files
jason.woltje 5ffe38e3b2 feat(packs): the chief applies a pack's non-definition half and raises hires from its proposals (#1873 SP5)
pack.preview, pack.apply, board.set and workflow.set accept an agent session whose role
lists organization.change (Jason 11-12 Q2): the check runs before the read, and the grant
is spent once after a write commits. A role without it gets decision-required. The packs
read lists packs for a within-role holder and spends nothing.

An agent apply never writes a definition. A definition a human apply would add is marked
not-written and its hire is pinned to the template or the inline entry. A workflow step
naming such a definition is left out and listed, and a workflow left with no steps is not
written. A definitions rename or a saved template refuses human-required, with the writer's
own guard behind both.

hire.raise takes {proposal: {pack, project?, hire}, route, reason?} from the chief. The broker
reads the recorded proposal, checks an inline pin's hash (starter-pack-proposal-stale) and
launch.noHire, and hands the pin to the hire service, which refuses a moved template or
definition as decision-stale or definition-rev-conflict.
2026-11-12 19:05:33 -06:00

403 lines
27 KiB
JavaScript

// #1873 SP4: starter packs at the bus. The owner previews and applies a pack; the plan is the write.
// Preview writes nothing; apply writes only what preview marked `add` plus one packs entry, and
// raises no decision. board.set and workflow.set are one-key plans through the same writer.
import test from 'node:test';
import assert from 'node:assert/strict';
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { PACK_BUS_KEYS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs';
import { pluginHash } from '../../plugins/src/pinned.mjs';
import { installPlugin } from '../../plugins/src/install.mjs';
import { starterPack as codingPack } from '../../plugins/starter-pack/coding/index.mjs';
const TOKEN = 'fixture-held-token-sp4-7d1f2b';
const SUBJECT = { provider: 'test', iss: 'test', sub: 'owner' };
async function harness(t, { mutate = () => {} } = {}) {
const { startBroker } = await import('../src/runtime.mjs');
const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../organization/tests/helpers.mjs');
const root = mkdtempSync(join(tmpdir(), 'packs-'));
const rolesDir = rolesCopy(root);
const doc = businessDoc(root, 'demo');
doc.plugins = { harness: ['pi'] };
mutate(doc);
writeJson(join(root, 'businesses/demo.json'), doc);
const other = businessDoc(join(root, 'b2'), 'other');
other.human = 'alex';
writeJson(join(root, 'businesses/other.json'), other);
const data = join(root, 'data');
mkdirSync(data, { mode: 0o700 });
const token = join(root, 'held.token');
writeFileSync(token, TOKEN, { mode: 0o600 });
chmodSync(token, 0o600);
const roles = { chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: { authority: { withinRole: [], crossRole: [] }, credentials: { gitea: { file: token, rotateBy: '2099-01-01' } } } };
const businesses = {
demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, roles },
other: { id: 'other', human: 'alex', arbiters: { technical: 'chief', delivery: 'chief' }, roles: { chief: roles.chief } },
};
const bus = await startBroker({ dataRoot: data, businesses, organization: { dir: root, rolesDir, system: systemFor(root) } });
// An installed plugin's directories are read-only; open them so the scratch tree can go.
const thaw = (p) => { const st = lstatSync(p); if (st.isSymbolicLink()) return; if (st.isDirectory()) { chmodSync(p, 0o700); for (const e of readdirSync(p)) thaw(join(p, e)); } };
t.after(async () => { await bus.close(); thaw(root); rmSync(root, { recursive: true, force: true }); });
const b = bus.broker;
const consoleOwner = b.bindConsole({ business: 'demo', human: 'jason', subject: SUBJECT });
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const call = async (verb, args = {}, cap = consoleOwner) => b.request(cap, { verb, args });
const path = join(root, 'businesses/demo.json');
const file = () => JSON.parse(readFileSync(path, 'utf8'));
const revision = () => b.request(human, { verb: 'limits' }).revision;
// A pack installed on this machine (origin installed), as the plugin installer writes it.
let n = 0;
const install = (pack) => {
const src = join(root, `src-${n++}`);
mkdirSync(join(src, 'starter-pack'), { recursive: true });
writeFileSync(join(src, 'starter-pack', 'index.mjs'), `export const interfaceVersion = 1;\nexport function starterPack() { return ${JSON.stringify(pack)}; }\n`);
const manifest = { name: pack.id, version: '1.0.0', kinds: ['starter-pack'], interfaceVersions: { 'starter-pack': 1 }, sha256: '0'.repeat(64),
source: { path: 'fixture', version: '1.0.0' }, signature: null, tools: [], actions: [], services: [], roleTemplates: [] };
writeFileSync(join(src, 'plugin.json'), JSON.stringify(manifest));
manifest.sha256 = pluginHash(src);
writeFileSync(join(src, 'plugin.json'), JSON.stringify(manifest));
installPlugin({ dataRoot: data, source: src, expectedHash: manifest.sha256 });
};
// A pack an owner saved, under <configDir>/templates/<account>/<name>.json (origin saved).
const save = (account, name, pack) => {
const dir = join(root, 'templates', account);
mkdirSync(dir, { recursive: true, mode: 0o700 });
chmodSync(join(root, 'templates'), 0o700);
chmodSync(dir, 0o700);
writeFileSync(join(dir, `${name}.json`), JSON.stringify(pack), { mode: 0o600 });
};
const decisions = () => ['roles', 'recent'].map(list => bus.broker.request(human, { verb: 'decisions', args: { list } }));
return { bus, b, root, rolesDir, path, consoleOwner, human, call, file, revision, install, save, decisions };
}
// A pack built from the shipped coding pack, renamed and patched.
const variant = (id, patch = () => {}) => { const p = structuredClone(codingPack()); p.id = id; patch(p); return p; };
const marks = (preview) => Object.fromEntries(preview.items.map(i => [i.path, i.mark]));
test('SP4 preview writes nothing; apply writes the add items and one packs entry, raises no decision, and a second apply is all same', async t => {
const { call, file, path, decisions, root, rolesDir } = await harness(t);
const before = readFileSync(path);
const decided = JSON.stringify(decisions());
const preview = await call('pack.preview.console', { pack: 'coding' });
assert.deepEqual(readFileSync(path), before);
assert.equal(preview.pack.origin, 'shipped');
assert.ok(preview.items.every(i => i.mark === 'add'), JSON.stringify(preview.items));
assert.deepEqual(preview.hires.map(h => [h.name, h.mark, h.noHire]), [['pm-1', 'propose', false], ['coder-1', 'propose', false], ['reviewer-1', 'propose', false]]);
// The chief is known, so every proposed hire lists its flagged and credential lines.
assert.equal(preview.chief, true);
for (const h of preview.hires) { assert.ok(Array.isArray(h.flagged)); assert.ok(Array.isArray(h.credentialLines)); }
// coder-1's credential lines name the services the coder template reaches.
const coderServices = JSON.parse(readFileSync(join(rolesDir, 'coder.json'), 'utf8')).credentials.map(c => c.service).sort();
assert.ok(coderServices.length > 0);
const lines = preview.hires.find(h => h.name === 'coder-1').credentialLines;
assert.ok(lines.length >= coderServices.length);
assert.deepEqual([...new Set(lines.map(l => l.service))].sort(), [...new Set(coderServices)]);
// On an empty data root every pack skill is not enabled; an enabled one drops out.
assert.deepEqual(preview.skills.notEnabled, codingPack().skills);
mkdirSync(join(root, 'data', 'skills-enabled', 'ms-sdlc'), { recursive: true });
assert.deepEqual((await call('pack.preview.console', { pack: 'coding' })).skills.notEnabled, codingPack().skills.filter(n => n !== 'ms-sdlc'));
assert.match(preview.digest, /^[0-9a-f]{64}$/);
const out = await call('pack.apply.console', { pack: 'coding', digest: preview.digest });
assert.equal(out.pack, 'coding');
const after = file();
assert.deepEqual(Object.keys(after.definitions).sort(), ['coder', 'cto', 'merge-gate', 'pm', 'reviewer', 'security-reviewer']);
assert.equal(after.board.layout, 'kanban');
// The organization's workflow drops the pack's step ids.
assert.deepEqual(after.workflows.change.steps[0], { title: 'Write the task', state: 'todo', roles: ['pm'] });
assert.ok(after.workflows.change.steps.every(s => !Object.hasOwn(s, 'id')));
assert.equal(after.packs.length, 1);
const entry = after.packs[0];
assert.equal(entry.id, 'coding');
assert.equal(entry.origin, 'shipped');
assert.ok(entry.added.includes('definitions.coder') && entry.added.includes('board') && entry.added.includes('workflows.change'));
assert.deepEqual(entry.proposedHires.find(h => h.name === 'coder-1').pin, { definition: 'coder', rev: 1 });
// No decision is raised: the owner hires from the proposals later.
assert.equal(JSON.stringify(decisions()), decided);
const again = await call('pack.preview.console', { pack: 'coding' });
assert.ok(again.items.every(i => i.mark === 'same'), JSON.stringify(again.items));
await call('pack.apply.console', { pack: 'coding', digest: again.digest });
assert.deepEqual(file().definitions, after.definitions);
assert.deepEqual(file().workflows, after.workflows);
// An apply that adds nothing and proposes what the last apply proposed writes nothing at all.
const settled = readFileSync(path);
const third = await call('pack.preview.console', { pack: 'coding' });
const out3 = await call('pack.apply.console', { pack: 'coding', digest: third.digest });
assert.deepEqual(out3.added, []);
assert.equal(out3.revision, third.revision);
assert.deepEqual(readFileSync(path), settled);
assert.equal(file().packs.length, 2);
});
test('SP4 a stale digest refuses pack-plan-stale and writes nothing', async t => {
const { call, path, revision } = await harness(t);
const preview = await call('pack.preview.console', { pack: 'coding' });
await call('workflow.set.console', { revision: revision(), name: 'intake', workflow: { description: 'Intake.', steps: [{ title: 'Take it', state: 'todo', roles: ['human'] }] } });
const before = readFileSync(path);
await assert.rejects(call('pack.apply.console', { pack: 'coding', digest: preview.digest }), /pack-plan-stale/);
await assert.rejects(call('pack.apply.console', { pack: 'coding', digest: 'f'.repeat(64) }), /pack-plan-stale/);
assert.deepEqual(readFileSync(path), before);
});
test('SP4 an edited definition is kept with its diff; a removed item stays removed; another pack naming it adds it', async t => {
const { call, human, file, revision, install, save } = await harness(t);
const first = await call('pack.preview.console', { pack: 'coding' });
await call('pack.apply.console', { pack: 'coding', digest: first.digest });
const coder = file().definitions.coder;
const { rev } = coder;
const definition = { role: coder.role, instructions: `${coder.instructions}Keep each change small.\n`, ...(coder.personality === undefined ? {} : { personality: coder.personality }) };
await call('definition.edit', { revision: revision(), rev, name: 'coder', definition }, human);
await call('definition.remove', { revision: revision(), rev: file().definitions.reviewer.rev, name: 'reviewer' }, human);
await call('board.set.console', { revision: revision(), board: null });
await call('workflow.set.console', { revision: revision(), name: 'change', workflow: null });
assert.equal(file().board, undefined);
assert.equal(file().workflows?.change, undefined);
const next = await call('pack.preview.console', { pack: 'coding' });
const m = marks(next);
assert.equal(m['definitions.coder'], 'kept');
assert.deepEqual(next.items.find(i => i.path === 'definitions.coder').diff.primitives, ['instructions']);
assert.equal(m['definitions.reviewer'], 'removed');
assert.equal(m.board, 'removed');
assert.equal(m['workflows.change'], 'removed');
assert.equal(m['definitions.pm'], 'same');
// The reviewer instance, its definition gone, pins the template the pack names.
assert.deepEqual(next.hires.find(h => h.name === 'reviewer-1').pin, { template: 'reviewer', sha256: codingPack().definitions.reviewer.sha256 });
const kept = file();
await call('pack.apply.console', { pack: 'coding', digest: next.digest });
assert.deepEqual(file().definitions, kept.definitions);
assert.equal(file().board, undefined);
assert.deepEqual(file().packs.at(-1).added, []);
// A different pack naming the same items adds them: a tombstone is per pack.
install(variant('teamkit'));
const other = await call('pack.preview.console', { pack: 'teamkit' });
assert.equal(other.pack.origin, 'installed');
const o = marks(other);
assert.equal(o['definitions.reviewer'], 'add');
assert.equal(o.board, 'add');
assert.equal(o['workflows.change'], 'add');
assert.equal(o['definitions.coder'], 'kept');
// A saved template reusing the shipped id is another pack: the shipped apply's removals don't bind it.
save('jason', 'mine', variant('coding'));
const mine = await call('pack.preview.console', { pack: 'saved:mine' });
assert.equal(mine.pack.origin, 'saved');
assert.equal(marks(mine)['definitions.reviewer'], 'add');
assert.equal(marks(mine).board, 'add');
});
test('SP4 --as renames a definition or workflow and pins the hire to the new name; a bad rename refuses', async t => {
const { call, file } = await harness(t);
const preview = await call('pack.preview.console', { pack: 'coding', as: ['definitions.coder=builder', 'workflows.change=ship'] });
const m = marks(preview);
assert.equal(m['definitions.builder'], 'add');
assert.equal(m['workflows.ship'], 'add');
assert.deepEqual(preview.hires.find(h => h.name === 'coder-1').pin, { definition: 'builder', rev: 1 });
await call('pack.apply.console', { pack: 'coding', as: ['definitions.coder=builder', 'workflows.change=ship'], digest: preview.digest });
assert.ok(file().definitions.builder);
assert.equal(file().definitions.coder, undefined);
assert.ok(file().workflows.ship);
// A rename onto another pack item's own name would show both as add while one replaced the other.
for (const as of [['definitions.nobody=x'], ['definitions.coder=a', 'definitions.coder=b'], ['definitions.coder=x', 'definitions.pm=x'], ['roles.coder=x'], 'definitions.coder=x', ['definitions.coder=reviewer'], ['definitions.coder=human'], ['definitions.merge-gate=human']])
await assert.rejects(call('pack.preview.console', { pack: 'coding', as }), /invalid-request/);
});
test('SP4 --as onto another workflow of the pack refuses; a renamed inline definition renames the workflow roles naming it', async t => {
const { call, file, install, rolesDir } = await harness(t);
install(variant('twoflow', p => { p.workflows.other = structuredClone(p.workflows.change); }));
await assert.rejects(call('pack.preview.console', { pack: 'twoflow', as: ['workflows.change=other'] }), /invalid-request/);
const { entryFromTemplate, readTemplate } = await import('../../organization/src/definitions.mjs');
const { rev: _rev, ...scribe } = entryFromTemplate(readTemplate(rolesDir, 'coder'));
install(variant('scribekit', p => {
p.definitions = { scribe };
p.hires = [];
p.workflows = { notes: { description: 'Notes.', steps: [{ id: 'write', title: 'Write it', state: 'todo', roles: ['scribe', 'human'] }] } };
}));
const as = ['definitions.scribe=writer'];
const preview = await call('pack.preview.console', { pack: 'scribekit', as });
assert.equal(marks(preview)['definitions.writer'], 'add');
assert.deepEqual(preview.plan.workflows.notes.steps[0].roles, ['writer', 'human']);
await call('pack.apply.console', { pack: 'scribekit', as, digest: preview.digest });
assert.ok(file().definitions.writer);
assert.equal(file().definitions.scribe, undefined);
assert.deepEqual(file().workflows.notes.steps[0].roles, ['writer', 'human']);
// A rename onto a role the same step already names refuses at preview, not only at apply.
install(variant('pairkit', p => {
p.definitions = { scribe };
p.hires = [];
p.workflows = { notes: { description: 'Notes.', steps: [{ id: 'write', title: 'Write it', state: 'todo', roles: ['scribe', 'pm'] }] } };
}));
await call('pack.preview.console', { pack: 'pairkit' });
await assert.rejects(call('pack.preview.console', { pack: 'pairkit', as: ['definitions.scribe=pm'] }), /invalid-request/);
// The reserved human step role never follows a rename, even when the pack defines a definition by that name.
install(variant('humankit', p => {
p.definitions = { human: scribe };
p.hires = [];
p.workflows = { signoff: { description: 'Sign-off.', steps: [{ id: 'sign', title: 'Sign it', state: 'todo', roles: ['human'] }] } };
}));
const renamed = await call('pack.preview.console', { pack: 'humankit', as: ['definitions.human=bot'] });
assert.deepEqual(renamed.plan.workflows.signoff.steps[0].roles, ['human']);
});
test('SP4 a changed pack with nothing to add still records its entry; an identical repeat writes nothing', async t => {
const { call, file, save } = await harness(t);
save('jason', 'mine', variant('mine'));
// The first apply pins the definitions it adds; the second pins the pack's template refs, so it
// records an entry too. From the second on, the proposals are settled.
for (let i = 0; i < 2; i++) {
const p = await call('pack.preview.console', { pack: 'saved:mine' });
await call('pack.apply.console', { pack: 'saved:mine', digest: p.digest });
}
assert.equal(file().packs.length, 2);
save('jason', 'mine', variant('mine', p => { p.version = '1.0.1'; p.description = 'A changed description.'; }));
const changed = await call('pack.preview.console', { pack: 'saved:mine' });
assert.ok(changed.items.every(i => i.mark === 'same'), JSON.stringify(changed.items));
await call('pack.apply.console', { pack: 'saved:mine', digest: changed.digest });
assert.equal(file().packs.length, 3);
assert.equal(file().packs.at(-1).version, '1.0.1');
assert.notEqual(file().packs.at(-1).sha256, file().packs[1].sha256);
const repeat = await call('pack.preview.console', { pack: 'saved:mine' });
const out = await call('pack.apply.console', { pack: 'saved:mine', digest: repeat.digest });
assert.deepEqual(out.added, []);
assert.equal(file().packs.length, 3);
});
test('SP4 an instance by the hire name drops the proposal; launch.noHire marks it', async t => {
const { call } = await harness(t, { mutate: doc => {
doc.roles['coder-1'] = { ...structuredClone(doc.roles.reviewer), definition: 'coder', tracker: { bot: 'bot-demo-coder-1', botId: 7 } };
doc.launch.noHire = ['pm'];
} });
const preview = await call('pack.preview.console', { pack: 'coding' });
const hires = Object.fromEntries(preview.hires.map(h => [h.name, h]));
assert.equal(hires['coder-1'].mark, 'dropped');
assert.equal(hires['pm-1'].noHire, true);
assert.equal(hires['reviewer-1'].noHire, false);
assert.deepEqual(preview.plan.packs.proposedHires.map(h => h.name), ['pm-1', 'reviewer-1']);
});
test('SP4 a held credential in a pack, a board or a workflow refuses credential-leak and is never echoed', async t => {
const { call, path, revision, save } = await harness(t);
save('jason', 'leaky', variant('leaky', p => { p.description = `Team notes ${TOKEN}`; }));
const before = readFileSync(path);
const leaks = [
() => call('pack.preview.console', { pack: 'saved:leaky' }),
() => call('pack.apply.console', { pack: 'saved:leaky', digest: 'a'.repeat(64) }),
() => call('board.set.console', { revision: revision(), board: { columns: codingPack().board.columns.map((c, i) => i ? c : { ...c, label: TOKEN }), layout: 'kanban' } }),
() => call('workflow.set.console', { revision: revision(), name: 'leak', workflow: { description: 'x', steps: [{ title: TOKEN, state: 'todo', roles: ['human'] }] } }),
];
for (const leak of leaks) {
await assert.rejects(leak(), e => { assert.match(e.message, /credential-leak/); assert.ok(!e.message.includes(TOKEN)); assert.ok(!JSON.stringify(e).includes(TOKEN)); return true; });
}
assert.deepEqual(readFileSync(path), before);
});
test('SP4 a template reference to a definition a hired agent may not hold refuses at preview', async t => {
const { call, install, rolesDir } = await harness(t);
const { templateHash } = await import('../../organization/src/definitions.mjs');
const cos = templateHash(rolesDir, 'chief-of-staff');
install(variant('chiefkit', p => { p.definitions.cos = { template: 'chief-of-staff', sha256: cos }; }));
await assert.rejects(call('pack.preview.console', { pack: 'chiefkit' }), /invalid-request/);
});
test('SP4 who may call: the owner only; the tester, a member, an agent and a console cap on the cli name refuse', async t => {
const { b, call, human, revision, path } = await harness(t);
// TESTER_ORGANIZATION_VERBS is unchanged: no pack, board or workflow verb.
assert.deepEqual(TESTER_ORGANIZATION_VERBS, ['limits', 'plugins', 'instances', 'plugins.set', 'instances.set', 'routes.set', 'routes']);
const tester = b.bindTester({ business: 'demo' });
const before = readFileSync(path);
assert.throws(() => b.request(tester, { verb: 'board.set', args: { revision: revision(), board: null } }), /tester-refused/);
assert.throws(() => b.request(tester, { verb: 'workflow.set', args: { revision: revision(), name: 'x', workflow: null } }), /tester-refused/);
assert.throws(() => b.request(tester, { verb: 'pack.preview', args: { pack: 'coding' } }), /tester-refused/);
const member = b.bindHireConsole({ business: 'demo', actor: 'alice', subject: { provider: 'test', iss: 'test', sub: 'alice' }, rights: ['org.hire', 'board.read'] });
for (const [verb, args] of [['pack.apply.console', { pack: 'coding', digest: 'a'.repeat(64) }], ['board.set.console', { revision: revision(), board: null }], ['workflow.set.console', { revision: revision(), name: 'x', workflow: null }], ['packs.console', {}],
['pack.apply', { pack: 'coding', digest: 'a'.repeat(64) }], ['board.set', { revision: revision(), board: null }]])
assert.throws(() => b.request(member, { verb, args }), /human-required/);
const agent = b.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi', address: 'coder-run' });
// SP5: an agent reaches the unsuffixed names through its role's organization.change; the coder
// lists none, so its write needs a decision (packs-agent.test.mjs covers the chief).
b.request(agent, { verb: 'role.claim' });
assert.throws(() => b.request(agent, { verb: 'pack.apply', args: { pack: 'coding', digest: 'a'.repeat(64) } }), /decision-required/);
assert.throws(() => b.request(agent, { verb: 'pack.preview.console', args: { pack: 'coding' } }), /human-required/);
// The owner's console cap calls the .console names; the cli cap the unsuffixed ones.
await assert.rejects(call('pack.preview', { pack: 'coding' }), /human-required/);
await assert.rejects(call('pack.preview.console', { pack: 'coding' }, human), /human-required/);
assert.deepEqual(readFileSync(path), before);
});
test('SP4 the request carries no origin, actor or unknown key; scope and argument checks refuse invalid-request', async t => {
const { call, revision, install } = await harness(t);
for (const args of [{ pack: 'coding', origin: 'shipped' }, { pack: 'coding', actor: 'mallory' }, {}, { pack: 7 }, { pack: 'coding', project: 'stack' }])
await assert.rejects(call('pack.preview.console', args), /invalid-request/);
await assert.rejects(call('pack.apply.console', { pack: 'coding' }), /invalid-request/);
await assert.rejects(call('pack.preview.console', { pack: 'nosuch' }), /starter-pack-not-found/);
await assert.rejects(call('board.set.console', { revision: 'nope', board: null }), /invalid-request/);
await assert.rejects(call('board.set.console', { revision: revision(), board: [] }), /invalid-request/);
await assert.rejects(call('workflow.set.console', { revision: revision(), name: 'x', workflow: 'y' }), /invalid-request/);
// A workflow step naming a role no definition, template or `human` resolves refuses.
await assert.rejects(call('workflow.set.console', { revision: revision(), name: 'x', workflow: { description: 'x', steps: [{ title: 'Do', state: 'todo', roles: ['nobody-at-all'] }] } }), /invalid-request/);
// A project pack needs a code project; an org pack takes none.
install(variant('projkit', p => { p.scope = 'project'; }));
await assert.rejects(call('pack.preview.console', { pack: 'projkit' }), /invalid-request/);
await assert.rejects(call('pack.preview.console', { pack: 'projkit', project: 'nosuch' }), /invalid-request/);
const preview = await call('pack.preview.console', { pack: 'projkit', project: 'stack' });
assert.equal(marks(preview)['projects.stack.board'], 'add');
assert.equal(marks(preview)['projects.stack.workflows.change'], 'add');
assert.equal(marks(preview)['definitions.coder'], 'add');
});
test('SP4 a saved template is read only from its own owner, and packs lists none', async t => {
const { b, call, save } = await harness(t);
save('jason', 'mine', variant('mine'));
const preview = await call('pack.preview.console', { pack: 'saved:mine' });
assert.equal(preview.pack.origin, 'saved');
const alex = b.bindHuman({ business: 'other', human: 'alex', via: 'cli', outsideAgent: true });
await assert.rejects(b.request(alex, { verb: 'pack.preview', args: { pack: 'saved:mine' } }), /starter-pack-not-found/);
const listed = await call('packs.console');
assert.ok(listed.packs.length >= 5);
assert.ok(listed.packs.every(p => p.origin !== 'saved' && p.id !== 'mine'));
for (const name of ['saved:../jason/mine', 'saved:', 'saved:a/b']) await assert.rejects(call('pack.preview.console', { pack: name }), /starter-pack-invalid|invalid-request/);
});
test('SP4 board.set changes only the board; workflow.set and remove change only that workflow', async t => {
const { call, file, revision } = await harness(t);
const before = file();
const board = structuredClone(codingPack().board);
await call('board.set.console', { revision: revision(), board });
const after = file();
assert.deepEqual(after.board, board);
delete after.board;
assert.deepEqual(after, before);
const flow = { description: 'One.', steps: [{ title: 'Do it', state: 'todo', roles: ['coder'] }] };
await call('workflow.set.console', { revision: revision(), name: 'one', workflow: flow });
await call('workflow.set.console', { revision: revision(), name: 'two', workflow: flow });
await call('workflow.set.console', { revision: revision(), name: 'one', workflow: null });
assert.deepEqual(file().workflows, { two: flow });
assert.deepEqual(file().board, board);
});
test('SP4 caps: at most 32 workflows and 256 packs entries', async t => {
const { call, file, revision, path } = await harness(t);
const flow = { description: 'x', steps: [{ title: 'Do it', state: 'todo', roles: ['human'] }] };
const doc = file();
doc.workflows = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [`w${i}`, flow]));
writeFileSync(path, JSON.stringify(doc, null, 2) + '\n');
await assert.rejects(call('workflow.set.console', { revision: revision(), name: 'w32', workflow: flow }), /invalid-request/);
await call('workflow.set.console', { revision: revision(), name: 'w0', workflow: null });
const preview = await call('pack.preview.console', { pack: 'coding' });
await call('pack.apply.console', { pack: 'coding', digest: preview.digest });
const full = file();
full.packs = Array.from({ length: 256 }, () => structuredClone(full.packs[0]));
writeFileSync(path, JSON.stringify(full, null, 2) + '\n');
const again = await call('pack.preview.console', { pack: 'coding' });
await assert.rejects(call('pack.apply.console', { pack: 'coding', digest: again.digest }), /invalid-request/);
});
test('SP4 the broker knows each verb under both names', () => {
assert.deepEqual(Object.keys(PACK_BUS_KEYS).sort(), ['board.set', 'board.set.console', 'pack.apply', 'pack.apply.console', 'pack.preview', 'pack.preview.console', 'packs', 'packs.console', 'workflow.set', 'workflow.set.console']);
});