Files
stack/packages/cli/tests/limits.test.mjs
T
jason.woltjeandClaude Opus 5.5 b2f57ca70d bus, business, harness, webui: the human and the chief of staff configure the assistant (row 155, Q169, #1649)
Jason's Q169: only the chief of staff removes the assistant; the chief
and the human configure it and its seat name.

Configure. A new organization key, assistants, replaces the vars,
tracker or definition of an instance whose definition is already the
assistant. holder and credentials are refused, and the whole document
still goes through validateBusinessDocument. set() refuses
organization-definition-refused if any other instance's definition
changed. THREAD_DEFINITION and END_PROTECTED move to
packages/business/src/vocabulary.mjs, so the broker and
launch-settings.mjs read one list. The human writes it with
mosaic limits set --file or the Console Settings view's Assistant
section; the chief writes it with change_organization. Both carry the
file's revision.

Seat name. The chief gets a typed set_name tool (name.set,
instance.name). The Console gets POST /api/name on the settings
boundary, through human-cli.

Remove. role.end and role.revoke of a threaded instance revoke every
thread of that instance whose latest claim is a claim, and report
threads: N. The raise and the check both record a missing holder run as
null. A new human-only verb, assistant.stop, revokes the human's own
threads of an assistant instance; the Console calls it from POST
/api/assistant-stop. The bundle, the end_run description and
roles/chief-of-staff.md now say the chief ends and configures the
assistant. No schema step.

Owner Darkwing, brief docs/plans/2026-10-16_q169-assistant-config.md.
Reviewers: Dewey (security) r1 approve 29012, r2 29014, r3 29018; Filbert r1 29010,
r2 29015, r3 29017, on candidate manifest 96cae28e...9016. Round 2 is
confirm-only after rows 162 and 125 moved the base under docs/TOOLS.md
and the cli README. Round 3 bumps row 164's webui/src/bus.mjs byte
baseline in packages/bus/tests/channel-proof.test.mjs for the eight
STATUS entries; Sage's gate on r2 failed only there. Mos's decision 123 go for the public/bus.js diff
(mos-0816) covers 9f9ce31b, unchanged here. Sage gate on 197722caa:
GATE_PASSED 25/25 (node 2370, selftests 422) 21:11:38Z, a rerun on the same tree after a first run stopped at pkg-webui on a board-held.test.mjs timeout that passes singly.

Nothing live. Follow-up brief in the packet: the names read rethrow, one
shared settings snapshot, null tests for the two new routes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 16:18:47 -05:00

67 lines
9.0 KiB
JavaScript

// REQ-LIMIT-1 and decision145: human transport and print-only migration.
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync,statSync} from 'node:fs';
import {join} from 'node:path';
import {main} from '../src/cli.mjs';
import {limitsCommand} from '../src/limits.mjs';
import {fixture,io,tmp} from './helpers.mjs';
import {writeJson} from '../../business/tests/helpers.mjs';
test('migration needs no bus/config, prints whole converted document, is idempotent and never writes',async t=>{
const root=tmp(t),f=fixture(root,'acme'),file=join(root,'config/businesses/acme.json');
const old=f.doc;old.roles.pm.vars.model='claude-opus-5-5';old.roles.cto.vars.model='claude-opus-5-5';old.roles.coder.vars.model='claude-sonnet-5-5';old.roles.reviewer.vars={model:'claude-opus-5-5'};old.roles.chief.vars={model:'claude-opus-5-5'};old.launch={by:'chief',instances:['coder','reviewer'],max:{opus:4,sonnet:4}};writeJson(file,old);
const mapping={models:{opus:{provider:'anthropic',model:'claude-opus-5-5'},sonnet:{provider:'anthropic',model:'claude-sonnet-5-5'}},families:{opus:'opus',sonnet:'sonnet'},instances:{pm:'opus',cto:'opus',coder:'sonnet',reviewer:'opus',chief:'opus'}};
const map=writeJson(join(root,'mapping.json'),mapping),args=['limits','migrate','--file',file,'--models',map,'--roles-dir',f.roles],bytes=readFileSync(file,'utf8'),mtime=statSync(file).mtimeNs??statSync(file).mtimeMs;
const x=io({MOSAIC_CONFIG:'/nonexistent'});await main(args,x,{transport:()=>assert.fail('migration contacted bus')});const migrated=JSON.parse(x.out.text);
assert.equal(migrated.roles.cto.vars.model,'opus');assert.deepEqual(migrated.launch.limits,{models:{anthropic:{'claude-opus-5-5':4,'claude-sonnet-5-5':4}}});assert.equal(migrated.roles.pm.vars.harness,'pi');assert.equal(readFileSync(file,'utf8'),bytes);assert.equal(statSync(file).mtimeNs??statSync(file).mtimeMs,mtime);
for(const bad of [{...mapping,families:{opus:'missing',sonnet:'sonnet'}},{...mapping,families:{opus:'opus',sonnet:'opus'}}]){writeJson(map,bad);await assert.rejects(main(args,io(),{}));assert.equal(readFileSync(file,'utf8'),bytes);assert.equal(statSync(file).mtimeMs,mtime);}
writeJson(map,mapping);const migratedFile=writeJson(join(root,'acme.json'),migrated),before=readFileSync(migratedFile,'utf8'),mt=statSync(migratedFile).mtimeMs,y=io();await main(['limits','migrate','--file',migratedFile,'--models',map,'--roles-dir',f.roles],y,{});assert.deepEqual(JSON.parse(y.out.text),migrated);assert.equal(readFileSync(migratedFile,'utf8'),before);assert.equal(statSync(migratedFile).mtimeMs,mt);
await assert.rejects(main([...args,'--write'],io(),{}),/mosaic limits/);
});
test('limits view/set use the human bus transport and pass only the explicit patch',async t=>{
const root=tmp(t),patch={revision:'a'.repeat(64),limits:{total:9},instances:{coder:'opus'}},file=writeJson(join(root,'patch.json'),patch),calls=[];
const deps={system:{dataRoot:root},transport:business=>(verb,args)=>{calls.push({business,verb,args});return {revision:'b'.repeat(64),limits:args.limits??{}};}};
const x=io();await main(['limits','--business','acme','--json'],x,deps);await main(['limits','set','--business','acme','--file',file],io(),deps);
assert.deepEqual(calls,[{business:'acme',verb:'limits',args:{}},{business:'acme',verb:'limits.set',args:patch}]);assert.equal(JSON.parse(x.out.text).revision,'b'.repeat(64));
await assert.rejects(main(['limits','--business','acme'],io({CODEX_THREAD_ID:'fixture-agent'}),deps),/agent/i);assert.equal(calls.length,2);
});
// Row 155 (#1649, Q169): the human configures the assistant with the same command.
test('limits set passes an assistants patch through unchanged, and the usage names the key',async t=>{
const root=tmp(t),patch={revision:'a'.repeat(64),assistants:{assistant:{vars:{model:'opus',thinking:'high'},tracker:null,definition:'assistant'}}},file=writeJson(join(root,'patch.json'),patch),calls=[];
const deps={system:{dataRoot:root},transport:business=>(verb,args)=>{calls.push({business,verb,args});return {revision:'b'.repeat(64)};}};
await main(['limits','set','--business','acme','--file',file],io(),deps);
assert.deepEqual(calls,[{business:'acme',verb:'limits.set',args:patch}]);
await assert.rejects(main(['limits','set','--business','acme'],io(),deps),/PATCH keys: revision, limits, models, instances, reset, assistants/);assert.equal(calls.length,1);
});
test('post-119 example and Jason fixture migrate with checked inherited models; wrong or absent config refuses',async t=>{
const {execFileSync}=await import('node:child_process');const {validateBusinessDocument}=await import('../../business/src/business.mjs');
const root=tmp(t),f=fixture(root,'acme'),doc=JSON.parse(execFileSync('git',['show','94e41ae29:packages/business/examples/mosaic-stack.example.json'],{encoding:'utf8'}));
doc.id='acme';let bot=10;for(const r of Object.values(doc.roles))if(r.tracker)r.tracker.botId=bot++;
doc.tracker.sync.botId=bot;for(const r of [...Object.values(doc.roles),{credentials:doc.tracker.sync.credentials}])for(const ref of Object.values(r.credentials??{})){if(ref.rotateBy)ref.rotateBy='2099-01-01';if(ref.expires)ref.expires='2099-01-01';}
const models={opus:{provider:'anthropic',model:'claude-opus-5-5'},sonnet:{provider:'anthropic',model:'claude-sonnet-5-5'}},instances=Object.fromEntries(Object.keys(doc.roles).map(k=>[k,'opus']));
const mapping={models,families:{opus:'opus',sonnet:'sonnet'},instances},map=writeJson(join(root,'mapping.json'),mapping),file=writeJson(join(root,'config/businesses/acme.json'),doc);
const config=JSON.parse(readFileSync(f.config,'utf8'));config.execution.provider='anthropic';config.execution.model='claude-opus-5-5';writeJson(f.config,config);
const args=['limits','migrate','--file',file,'--models',map,'--roles-dir',f.roles],run=async()=>{const x=io(f.env);await main(args,x,{});return JSON.parse(x.out.text);};
const before=readFileSync(file,'utf8'),mt=statSync(file).mtimeMs,configBytes=readFileSync(f.config,'utf8'),configMt=statSync(f.config).mtimeMs;
let converted=await run();assert.equal(converted.launch.by,'chief');assert.equal(converted.roles.assistant.holder,'mos');assert.equal(converted.roles.chief.vars.model,'opus');assert.equal(converted.roles.assistant.vars.model,'opus');validateBusinessDocument(converted,file,{rolesDir:f.roles});assert.equal(readFileSync(file,'utf8'),before);assert.equal(statSync(file).mtimeMs,mt);assert.equal(readFileSync(f.config,'utf8'),configBytes);assert.equal(statSync(f.config).mtimeMs,configMt);
// Jason's known four explicit selections plus 119's inherited chief/assistant.
for(const [name,key]of Object.entries({pm:'opus',cto:'opus',coder:'sonnet',reviewer:'opus'})){doc.roles[name].vars={...doc.roles[name].vars,model:models[key].model};mapping.instances[name]=key;}
writeJson(file,doc);writeJson(map,mapping);const jasonBytes=readFileSync(file,'utf8'),jasonMt=statSync(file).mtimeMs;converted=await run();validateBusinessDocument(converted,file,{rolesDir:f.roles});assert.equal(converted.roles.coder.vars.model,'sonnet');assert.equal(converted.roles.chief.vars.model,'opus');assert.equal(readFileSync(f.config,'utf8'),configBytes);assert.equal(statSync(f.config).mtimeMs,configMt);
for(const bad of [{...mapping,instances:{...mapping.instances,assistant:'sonnet'}},{...mapping,models:{...models,opus:{...models.opus,provider:'wrong-provider'}}}]){writeJson(map,bad);await assert.rejects(run(),/inherited provider\/model/);assert.equal(readFileSync(file,'utf8'),jasonBytes);assert.equal(statSync(file).mtimeMs,jasonMt);}
writeJson(map,mapping);await assert.rejects(main(args,io({...f.env,MOSAIC_CONFIG:join(root,'missing.json')}),{}),/system config/);assert.equal(readFileSync(file,'utf8'),jasonBytes);assert.equal(statSync(file).mtimeMs,jasonMt);writeJson(f.config,'invalid JSON');await assert.rejects(run(),/system config/);assert.equal(readFileSync(file,'utf8'),jasonBytes);assert.equal(statSync(file).mtimeMs,jasonMt);
writeJson(f.config,configBytes);assert.equal(readFileSync(f.config,'utf8'),configBytes);assert.equal(readFileSync(file,'utf8'),jasonBytes);
});
test('plugins view/set use the human bus transport and pass only the explicit patch (vikunja:19/305)',async t=>{
const root=tmp(t),patch={revision:'a'.repeat(64),plugins:{'search-source':'searxng-url'}},file=writeJson(join(root,'plugins.json'),patch),calls=[];
const deps={system:{dataRoot:root},transport:business=>(verb,args)=>{calls.push({business,verb,args});return {revision:'b'.repeat(64),plugins:args.plugins??{}};}};
const x=io();await main(['plugins','--business','acme','--json'],x,deps);await main(['plugins','set','--business','acme','--file',file],io(),deps);
assert.deepEqual(calls,[{business:'acme',verb:'plugins',args:{}},{business:'acme',verb:'plugins.set',args:patch}]);assert.equal(JSON.parse(x.out.text).revision,'b'.repeat(64));
await assert.rejects(main(['plugins','--business','acme'],io({CODEX_THREAD_ID:'fixture-agent'}),deps),/agent/i);
for(const bad of [['plugins','set','--business','acme'],['plugins','view','--file',file],['plugins','nope'],['plugins','--write']])await assert.rejects(main(bad,io(),deps),/mosaic plugins/);
assert.equal(calls.length,2);
});