// 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('../../organization/src/organization-config.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); });