Files
stack/packages/cli/tests/project.test.mjs
T
jason.woltjeandClaude Opus 5.5 ade628c8b5 cli: mosaic makes projects and threads and talks in a named thread (row 128, #1622)
Row 128 round 1, Researcher's candidate (agents/researcher/work/queue-128/,
row128.patch, manifest 9eed6a78, 8 paths, base 0f3eb25ea). Filbert
approved round 1 (comment 28895).

`mosaic project create` and `project list` call project.create and
project.list; `init` and `check` keep their arguments and refusals.
`mosaic threads`, `thread create`, `thread open` and `thread read` cover
the thread verbs, and `mosaic talk --thread` sends thread.send as a
REQUEST and prints only the reply to the sent message. `mosaic follow
--thread` is the same loop without the send. A thread reference is its
id, an id prefix of 8 or more characters, or <project>/<name>; unknown,
short and ambiguous references refuse with exit 2 before any send. Every
verb refuses inside an agent run. There is no default thread, so a plain
`talk assistant` still meets thread-required-use-thread-send.
Acceptance 314 step 1 now creates the project and the assistant thread.
New: packages/cli/src/thread.mjs, packages/cli/tests/thread-cli.test.mjs.

Sage's landing gate ran on 77b1a6e8a plus the candidate: 25 steps, all
rc 0 (15 packages, 10 script suites), 14:44:02Z to 14:48:42Z, network
sage-r128_default removed. The commit sits on 7de8b7270, which adds one
docs/SESSIONS.md line that no test reads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 09:49:14 -05:00

62 lines
3.3 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { main, run } from '../src/cli.mjs';
import { fixture, humanEnv, io, tmp } from './helpers.mjs';
import { writeJson } from '../../business/tests/helpers.mjs';
test('project init/check use declared business repo and never contact the bus', async t => {
const root = tmp(t), f = fixture(root);
f.doc.projects.stack.repo = join(root, 'records');
writeJson(join(root, 'config/businesses/acme.json'), f.doc);
const x = io(humanEnv(f.env));
const deps = { transport: () => assert.fail('local project command contacted bus') };
await main(['project', 'init', 'acme', 'stack'], x, deps);
assert.ok(existsSync(join(root, 'records/spec/north-star.md')));
const checked = io(humanEnv(f.env));
assert.equal(await run(['project', 'check', 'acme', 'stack'], checked, deps), 0);
assert.match(checked.out.text, /valid|checked/);
writeFileSync(join(root, 'records/unknown'), 'bad');
const bad = io(humanEnv(f.env));
assert.equal(await run(['project', 'check', 'acme', 'stack'], bad, deps), 2);
assert.match(bad.err.text, /unknown/);
});
test('project commands validate args and refuse undeclared projects and agent init', async t => {
const root = tmp(t), f = fixture(root);
await assert.rejects(main(['project', 'init', 'acme', 'missing'], io(humanEnv(f.env)), {}), /declare/);
await assert.rejects(main(['project', 'init', 'acme', 'stack'], io({ ...f.env, CODEX_THREAD_ID: 'fixture-agent' }), {}), /agent/i);
for (const args of [[], ['other', 'acme', 'stack'], ['init', 'acme'], ['check', 'acme', 'stack', '--write']])
assert.equal(await run(['project', ...args], io(humanEnv(f.env)), {}), 4);
});
// Row 128 (#1622): create and list are bus verbs; they take their own arguments, and init and
// check keep theirs (a name for create is not the init/check <business> <project> pair).
test('project create and list validate their arguments before any bus call', async t => {
const root = tmp(t), f = fixture(root);
const deps = { transport: () => assert.fail('a usage error reached the bus') };
for (const args of [['create'], ['create', 'a', 'b'], ['create', 'a', '--repo'], ['create', 'a', '--repo', 'x', '--repo', 'y'], ['create', 'a', '--write'], ['list', 'extra'], ['list', '--repo', 'x']]) {
const x = io(humanEnv(f.env));
// --business is given, so only the arity check can produce the exit 4 (callFor would too, without it).
assert.equal(await run(['project', ...args, '--business', 'acme'], x, deps), 4, args.join(' '));
assert.match(x.err.text, /mosaic project create <name>/);
}
const listed = [];
const transport = () => async verb => (listed.push(verb), []);
const x = io(humanEnv(f.env));
assert.equal(await run(['project', 'list', '--business', 'acme'], x, { transport }), 0);
assert.deepEqual(listed, ['project.list']);
assert.equal(x.out.text, 'no projects\n');
});
test('project dispatcher reaches the human CLI', async t => {
const root = tmp(t), f = fixture(root);
const result = spawnSync(new URL('../../../scripts/mosaic', import.meta.url).pathname, ['project'], {
encoding: 'utf8', env: humanEnv(f.env),
});
assert.equal(result.status, 4);
assert.match(result.stderr, /mosaic project/);
});