Files
stack/packages/bus/tests/project-runtime.test.mjs
T
jason.woltje c216f47dbb plugins, cli, bus, business, webui: harness, model and thinking from the instance vars, a fallback route, and a harness plug-in kind (row 65, #1558)
Adds the harness plug-in kind (interface 1). packages/plugins/harness/pi
and claude-code hold what core branched on: the adapter, thinking levels,
preflight, version probe, sandbox mounts, builtins line, typed tool names,
gate timeout, bundle files, adapter env, turn end and session-files path.
Core reaches a module only through the loader.

A session's route is the instance's harness, model and thinking vars, or
the three fallback. vars; a missing one refuses no-route, and the system
file's execution keys are never read for a session. The launcher checks
the primary route, then the fallback: harness-off, harness-refused,
thinking-unsupported, then preflight. A failed preflight falls back when
a fallback is set, and any refusal of the fallback names the primary's
failure too. Account checks run
only when the route's module counts an account. The manifest records
route, routeReason and harnessVersion, replacing piVersion and
claudeVersion. pi gets --thinking, Claude Code --effort, both from
MOSAIC_THINKING.

The business file refuses a partial fallback. routes and routes.set are
human-only bus verbs outside ORGANIZATION_KEYS; checkRoutes is shared by
routes.set, instances.set and limits.set and checks only changed routes.
The chief of staff changes an instance's model, and the assistant's whole
vars (Q169), through limits.set, so its route write runs the same checks.
A model outside launch.models refuses unknown-model, primary or fallback,
and routes.set passes that code through.
A new run binds only on a switched-on harness module name and records
its transcript path, which the usage reader uses. Settings gains a
Routing table with six selects per instance and loses the Role models
fieldset; it posts changed rows to /api/routes. webui/src/bus.mjs is
unchanged.

Until Jason's `mosaic plugins set` adds "harness": ["claude-code"], a new
launch refuses harness-off.
2026-10-19 11:27:19 -05:00

67 lines
4.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { startBroker } from '../src/runtime.mjs';
import { Client } from '../src/client.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { initProject } from '../../project/src/index.mjs';
import { fixture } from './spec-fixtures.mjs';
test('Broker keeps the sandbox client import closure: only the listed imports, no lazy project import', () => {
const source = readFileSync(new URL('../src/broker.mjs', import.meta.url), 'utf8');
assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), [
"import { randomUUID, randomBytes } from 'node:crypto';",
// Row 65 (#1558): bindLaunch checks a run's transcript path is absolute.
"import { isAbsolute } from 'node:path';",
// Row 65: the plug-in name pattern a harness takes. kinds.mjs is in RUNNER_FILES and imports nothing.
"import { NAME as HARNESS_NAME } from '../../plugins/src/kinds.mjs';",
"import { loadRequirements } from '../../tasks/src/requirements.mjs';",
// Row 137 (#1631): land-work's checks, in RUNNER_FILES beside the broker. They import nothing.
"import { MERGE, LandingRefusal, readLanding } from './landing.mjs';",
// Row 141 (#1635): the business ID_PATTERN for instance names. The vocabulary is already in
// RUNNER_FILES and imports nothing. Row 155 (#1649) reads THREAD_DEFINITION and END_PROTECTED
// from it too.
"import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED } from '../../business/src/vocabulary.mjs';",
]);
assert.doesNotMatch(source, /\bimport\s*\(/);
assert.doesNotMatch(readFileSync(new URL('../src/landing.mjs', import.meta.url), 'utf8'), /\bimport\b/);
assert.doesNotMatch(readFileSync(new URL('../../business/src/vocabulary.mjs', import.meta.url), 'utf8'), /\bimport\b/);
assert.doesNotMatch(readFileSync(new URL('../../plugins/src/kinds.mjs', import.meta.url), 'utf8'), /\bimport\b/);
});
test('without a trusted project adapter all three async paths refuse project-not-configured', async t => {
const f = await fixture(t, { adapter: false });
for (const [verb, args] of [
['spec.read', { project: 'stack' }],
['record.write', { project: 'stack', path: 'docs/a.md', record: '# A' }],
['decision.raise', { project: 'stack', spec_path: 'spec/north-star.md', spec_commit: f.commit }],
]) await assert.rejects(f.call(f.coder, verb, args), { code: 'project-not-configured' });
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, 0);
});
test('real startBroker injects the project adapter for socket reads and bound-author record writes', async t => {
const root = mkdtempSync(join(tmpdir(), 'project-runtime-'));
let runtime;
t.after(async () => { await runtime?.close(); rmSync(root, { recursive: true, force: true }); });
// Row 65: the bound run's harness is switched on.
const business = { id: 'demo', human: 'jason', plugins: { harness: ['pi'] }, projects: { stack: { root: join(root, 'code') } },
arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: {
definition: 'coder', authority: { withinRole: ['record.write'], crossRole: [] },
} } };
const initialized = await initProject({ business, project: 'stack', dataRoot: root });
runtime = await startBroker({ dataRoot: root, businesses: { demo: business }, launches: [{
business: 'demo', role: 'coder', run: 'runtime-run', harness: 'pi',
pid: process.pid, startTime: startTimeOf(process.pid),
}] });
const client = new Client({ path: runtime.path, cap: runtime.launches[0].cap });
await client.call('role.claim');
const spec = await client.call('spec.read', { project: 'stack' });
assert.equal(spec.commit, initialized.commit);
assert.equal(spec.confirmed, true);
const record = await client.call('record.write', { project: 'stack', path: 'docs/runtime.md', record: '# Runtime\n' });
assert.match(record.commit, /^[0-9a-f]{40}$/);
assert.equal(readFileSync(join(initialized.repo, record.path), 'utf8'), '# Runtime\n');
});