git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
67 lines
9.0 KiB
JavaScript
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('../../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);
|
|
});
|