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.
270 lines
34 KiB
JavaScript
270 lines
34 KiB
JavaScript
// REQ-LIMIT-1 / REQ-ROLE-3: tunable settings, exact routing, audited writes.
|
|
import test,{after} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFileSync,statSync,writeFileSync,readdirSync,mkdirSync,symlinkSync,rmSync} from 'node:fs';
|
|
import {join} from 'node:path';
|
|
import {businessDoc,rolesCopy,tmp as makeRoot,writeJson,tokenFile,FAR_DATE} from './helpers.mjs';
|
|
import {validateBusinessDocument} from '../src/organization-config.mjs';
|
|
const roots=[];after(()=>roots.forEach(root=>rmSync(root,{recursive:true,force:true})));
|
|
function fixture(){const root=makeRoot();roots.push(root);const rolesDir=rolesCopy(root),doc=businessDoc(root,'acme');for(const r of Object.values(doc.roles))r.vars={...r.vars,model:'sonnet'};doc.roles.pm.vars.model='opus';doc.launch={by:'chief',instances:['coder','reviewer'],models:{opus:{provider:'anthropic',model:'claude-opus-5-5'},sonnet:{provider:'anthropic',model:'claude-sonnet-5-5'}},limits:{models:{anthropic:{'claude-opus-5-5':4,'claude-sonnet-5-5':4}}}};const file=join(root,'businesses/acme.json');writeJson(file,doc);return {root,rolesDir,doc,file};}
|
|
test('REQ-LIMIT-1 schema: every scope unset or zero, adjustable above four, bad values and unknown models refuse',()=>{
|
|
const f=fixture(),check=d=>validateBusinessDocument(d,f.file,{rolesDir:f.rolesDir});
|
|
for(const limits of [{},{total:0},{providers:{anthropic:0}},{models:{anthropic:{'claude-opus-5-5':0}}},{total:100,providers:{anthropic:25},models:{anthropic:{'claude-opus-5-5':12}}}]){const d=structuredClone(f.doc);d.launch.limits=limits;assert.deepEqual(check(d).launch.limits,limits);}
|
|
for(const bad of [-1,1.2,null,'4']){const d=structuredClone(f.doc);d.launch.limits={total:bad};assert.throws(()=>check(d));}
|
|
const old=structuredClone(f.doc);old.launch={by:'chief',instances:['coder'],max:{opus:4,sonnet:4}};assert.throws(()=>check(old),/migrat/);
|
|
const missing=structuredClone(f.doc);missing.roles.coder.vars.model='not-listed';assert.throws(()=>check(missing),/model/);
|
|
});
|
|
test('REQ-LIMIT-1 counts each exact scope at/over limit, unlimited and old tuple after model reassignment',async()=>{
|
|
const {limitHit}=await import('../src/launch.mjs');const sessions=[{business:'acme',provider:'anthropic',model:'opus-exact'},{business:'other',provider:'anthropic',model:'opus-exact'}];
|
|
for(const limits of [{total:1},{providers:{anthropic:1}},{models:{anthropic:{'opus-exact':1}}}]){assert.ok(limitHit(sessions,'acme',limits,{provider:'anthropic',model:'opus-exact'}));assert.equal(limitHit(sessions,'acme',JSON.parse(JSON.stringify(limits).replaceAll(':1',':2')),{provider:'anthropic',model:'opus-exact'}),null);assert.ok(limitHit(sessions,'acme',JSON.parse(JSON.stringify(limits).replaceAll(':1',':0')),{provider:'anthropic',model:'opus-exact'}));}
|
|
for(const limits of [{total:0},{providers:{anthropic:0}},{models:{anthropic:{'opus-exact':0}}}])assert.ok(limitHit([],'acme',limits,{provider:'anthropic',model:'opus-exact'}));
|
|
assert.equal(limitHit(sessions,'acme',{}, {provider:'anthropic',model:'opus-exact'}),null);
|
|
assert.equal(limitHit(sessions,'acme',{models:{anthropic:{'sonnet-exact':1}}},{provider:'anthropic',model:'sonnet-exact'}),null);
|
|
});
|
|
test('REQ-ROLE-3 writer: atomic validated revision, immutable previous bytes and authenticated actor/old/new receipt',async()=>{
|
|
const {organizationWriter}=await import('../src/organization.mjs');const f=fixture(),w=organizationWriter({dir:f.root,rolesDir:f.rolesDir});const before=readFileSync(f.file,'utf8'),view=w.read('acme');
|
|
const next=structuredClone(f.doc);next.launch.limits.total=99;const done=w.write('acme',next,{revision:view.revision,actor:'jason'});
|
|
assert.equal(JSON.parse(readFileSync(f.file,'utf8')).launch.limits.total,99);assert.notEqual(done.revision,view.revision);assert.equal(readFileSync(done.previous,'utf8'),before);
|
|
const events=readFileSync(w.logFile('acme'),'utf8').trim().split('\n').map(JSON.parse);const e=events.at(-1);assert.equal(e.actor,'jason');assert.equal(e.state,'committed');assert.ok(e.changes.some(c=>c.path==='launch.limits.total'&&c.new===99));
|
|
const bytes=readFileSync(f.file,'utf8');assert.throws(()=>w.write('acme',next,{revision:view.revision,actor:'jason'}),/revision/);next.launch.limits.total=-1;assert.throws(()=>w.write('acme',next,{revision:done.revision,actor:'jason'}));assert.equal(readFileSync(f.file,'utf8'),bytes);next.launch.limits.total=100;w.write('acme',next,{revision:done.revision,actor:'jason'});const last=JSON.parse(readFileSync(w.logFile('acme'),'utf8').trim().split('\n').at(-1));assert.ok(last.changes.some(c=>c.path==='launch.limits.total'&&c.old===99&&c.new===100));
|
|
});
|
|
test('REQ-ROLE-3 writer: pre-rename fault leaves old file, post-rename recovery records commit, conflicting recovery refuses',async()=>{
|
|
const {organizationWriter}=await import('../src/organization.mjs');const f=fixture(),w=organizationWriter({dir:f.root,rolesDir:f.rolesDir});const before=readFileSync(f.file,'utf8'),revision=w.read('acme').revision,next=structuredClone(f.doc);next.launch.limits.total=7;
|
|
const crash=organizationWriter({dir:f.root,rolesDir:f.rolesDir,barrier:stage=>{if(stage==='prepared')throw Error('fixture crash');}});assert.throws(()=>crash.write('acme',next,{revision,actor:'jason'}));assert.equal(readFileSync(f.file,'utf8'),before);
|
|
assert.equal(w.read('acme').revision,revision);w.recover('acme');assert.equal(w.read('acme').revision,revision);
|
|
const committed=organizationWriter({dir:f.root,rolesDir:f.rolesDir,barrier:stage=>{if(stage==='renamed')throw Error('fixture after rename');}});assert.throws(()=>committed.write('acme',next,{revision,actor:'jason'}));w.recover('acme');assert.equal(w.read('acme').document.launch.limits.total,7);assert.equal(JSON.parse(readFileSync(w.logFile('acme'),'utf8').trim().split('\n').at(-1)).state,'committed');
|
|
});
|
|
|
|
test('REQ-LIMIT-1 / decision145 migration prints only, preserves values, rejects ambiguity, and is idempotent',async()=>{
|
|
const {migrateLaunch}=await import('../src/migrate-launch.mjs');const f=fixture(),old=structuredClone(f.doc);old.roles.pm.vars.model='claude-opus-5-5';for(const [name,r] of Object.entries(old.roles))if(name!=='pm')r.vars.model='claude-sonnet-5-5';old.launch={by:'chief',instances:['coder','reviewer'],max:{opus:4,sonnet:4}};writeJson(f.file,old);const bytes=readFileSync(f.file,'utf8'),mtime=statSync(f.file).mtimeMs;
|
|
const mapping={models:f.doc.launch.models,families:{opus:'opus',sonnet:'sonnet'}};const next=migrateLaunch(old,mapping,f.file,{rolesDir:f.rolesDir});assert.deepEqual(next.launch.limits, f.doc.launch.limits);assert.equal(next.roles.pm.vars.model,'opus');assert.equal(next.roles.coder.vars.model,'sonnet');assert.deepEqual(migrateLaunch(next,mapping,f.file,{rolesDir:f.rolesDir}),next);
|
|
assert.throws(()=>migrateLaunch(old,{...mapping,families:{opus:'missing',sonnet:'sonnet'}},f.file,{rolesDir:f.rolesDir}));assert.throws(()=>migrateLaunch(old,{...mapping,families:{opus:'opus',sonnet:'opus'}},f.file,{rolesDir:f.rolesDir}),/ambiguous/);assert.throws(()=>migrateLaunch(old,{...mapping,models:{...mapping.models,proxy:{provider:'proxy',route:'anthropic',model:'claude-opus-5-5'}},instances:{pm:'proxy'}},f.file,{rolesDir:f.rolesDir}),/ambiguous/);const onlyOpus=structuredClone(old);onlyOpus.launch.max={opus:4};assert.throws(()=>migrateLaunch(onlyOpus,mapping,f.file,{rolesDir:f.rolesDir}),/unknown or ambiguous/);const allOpus=structuredClone(old);for(const r of Object.values(allOpus.roles))r.vars.model='claude-opus-5-5';assert.throws(()=>migrateLaunch(allOpus,{...mapping,families:{opus:'opus',sonnet:'opus'}},f.file,{rolesDir:f.rolesDir}),/ambiguous/);assert.equal(readFileSync(f.file,'utf8'),bytes);assert.equal(statSync(f.file).mtimeMs,mtime);
|
|
});
|
|
test('REQ-LIMIT-1 shared settings validates the complete proposed selection and logs exact actor',async()=>{
|
|
const {launchSettings}=await import('../src/launch-settings.mjs');const f=fixture(),system={environment:'development',dataRoot:join(f.root,'data'),'execution.backend':'docker','execution.provider':'anthropic','execution.model':'sonnet','execution.adapter':'pi'};const settings=launchSettings({dir:f.root,rolesDir:f.rolesDir,system,sessions:()=>[{business:'acme',provider:'anthropic',model:'claude-opus-5-5'}]});const view=settings.read('acme');assert.equal(view.usage.total,1);assert.equal(view.instances.pm.modelKey,'opus');
|
|
const result=settings.set('acme',{revision:view.revision,limits:{total:0},instances:{coder:'opus'}},'jason');assert.equal(result.limits.total,0);assert.equal(result.instances.coder.modelKey,'opus');const models=structuredClone(result.models);models.opus.provider='proxy';models.opus.route='anthropic';const updated=settings.set('acme',{revision:result.revision,models},'jason');assert.equal(updated.instances.pm.provider,'proxy');assert.throws(()=>settings.set('acme',{revision:result.revision,instances:{coder:'ghost'}},'jason'));assert.throws(()=>settings.set('acme',{revision:result.revision,roles:{}},'jason'));
|
|
});
|
|
test('REQ-LIMIT-1 provider is table data: another provider routes separately and aliases share exact counts',async()=>{
|
|
const {limitHit}=await import('../src/launch.mjs');const {sessionModel}=await import('../../harness/src/bundle.mjs');
|
|
const models={alias:{provider:'proxy-next',model:'claude-opus-5-5'}},selected=sessionModel({vars:{harness:'claude-code',model:'alias',thinking:'high'}},models);assert.equal(selected.provider,'proxy-next');assert.equal(selected.model,'claude-opus-5-5');
|
|
const sessions=[{business:'acme',provider:'anthropic',model:selected.model}];assert.equal(limitHit(sessions,'acme',{providers:{'proxy-next':1},models:{'proxy-next':{[selected.model]:1}}},selected),null);sessions.push({business:'acme',provider:'proxy-next',model:selected.model});assert.equal(limitHit(sessions,'acme',{providers:{'proxy-next':1}},selected).scope,'provider');assert.equal(limitHit(sessions,'acme',{models:{'proxy-next':{[selected.model]:1}}},selected).scope,'model');
|
|
assert.equal(limitHit([],'acme',{providers:{constructor:0}},{provider:'constructor',model:'toString'}).running,0);
|
|
});
|
|
test('REQ-ROLE-3 refuses live writer locks, symlink targets and uncertain recovery without changing bytes',async()=>{
|
|
const {organizationWriter}=await import('../src/organization.mjs');const f=fixture(),w=organizationWriter({dir:f.root,rolesDir:f.rolesDir}),revision=w.read('acme').revision,next=structuredClone(f.doc);next.launch.limits.total=8;
|
|
const crash=organizationWriter({dir:f.root,rolesDir:f.rolesDir,barrier:()=>{throw Error('fixture interruption');}});assert.throws(()=>crash.write('acme',next,{revision,actor:'jason'}));const external=structuredClone(f.doc);external.launch.limits.total=12;writeJson(f.file,external);const bytes=readFileSync(f.file,'utf8');assert.throws(()=>w.recover('acme'),/match prepared/);assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
const g=fixture(),gw=organizationWriter({dir:g.root,rolesDir:g.rolesDir}),gr=gw.read('acme').revision,lock=join(g.root,'businesses/.history/acme/write.lock');mkdirSync(lock,{recursive:true,mode:0o700});writeJson(join(lock,'owner.json'),{pid:process.pid});assert.throws(()=>gw.write('acme',g.doc,{revision:gr,actor:'jason'}),/busy/);rmSync(lock,{recursive:true});const target=join(g.root,'target.json');writeJson(target,g.doc);rmSync(g.file);symlinkSync(target,g.file);assert.throws(()=>gw.write('acme',g.doc,{revision:gr,actor:'jason'}));assert.equal(JSON.parse(readFileSync(target,'utf8')).id,'acme');
|
|
});
|
|
// Row 155 (#1649, Q169): the human and the chief of staff change an assistant instance's vars, tracker
|
|
// and definition through the same writer; holder and credentials stay hand edits.
|
|
function withAssistant(f,entry={}){const doc=structuredClone(f.doc);doc.roles.assistant={definition:'assistant',holder:'mos',vars:{model:'sonnet'},...entry};writeJson(f.file,doc);return doc;}
|
|
async function assistantSettings(f){const {launchSettings}=await import('../src/launch-settings.mjs');return launchSettings({dir:f.root,rolesDir:f.rolesDir,system:{environment:'development',dataRoot:join(f.root,'data'),'execution.backend':'docker','execution.provider':'anthropic','execution.model':'sonnet','execution.adapter':'pi'}});}
|
|
const code=c=>e=>e.code===c;
|
|
test('Q169 assistants: vars and definition change and log the actor; tracker null leaves no tracker',async()=>{
|
|
const f=fixture();withAssistant(f);const settings=await assistantSettings(f),view=settings.read('acme');
|
|
assert.deepEqual(view.assistants,{assistant:{definition:'assistant',vars:{model:'sonnet'},tracker:null}});
|
|
const done=settings.set('acme',{revision:view.revision,assistants:{assistant:{vars:{model:'opus',thinking:'high'},definition:'assistant',tracker:null}}},'jason');
|
|
assert.deepEqual(done.assistants.assistant,{definition:'assistant',vars:{model:'opus',thinking:'high'},tracker:null});assert.equal(done.instances.assistant.modelKey,'opus');
|
|
const e=JSON.parse(readFileSync(settings.writer.logFile('acme'),'utf8').trim().split('\n').at(-1));assert.equal(e.actor,'jason');assert.ok(e.changes.some(c=>c.path==='roles.assistant.vars.thinking'&&c.new==='high'));
|
|
const chief=settings.set('acme',{revision:done.revision,assistants:{assistant:{vars:{}}}},'chief-of-staff.chief');assert.deepEqual(chief.assistants.assistant.vars,{});assert.equal(chief.instances.assistant.modelKey,null,'row 65: no execution.model fallback');
|
|
const last=JSON.parse(readFileSync(settings.writer.logFile('acme'),'utf8').trim().split('\n').at(-1));assert.equal(last.actor,'chief-of-staff.chief');
|
|
});
|
|
test('Q169 assistants: refuses other instances, other definitions, holder and credentials, empty entries, a tracker the definition has no credential for and a stale revision',async()=>{
|
|
const f=fixture();withAssistant(f);const settings=await assistantSettings(f),{revision}=settings.read('acme'),bytes=readFileSync(f.file,'utf8'),set=assistants=>settings.set('acme',{revision,assistants},'jason');
|
|
for(const instance of ['coder','chief'])assert.throws(()=>set({[instance]:{definition:'assistant'}}),code('organization-not-assistant'),instance);
|
|
for(const definition of ['coder','chief-of-staff',null])assert.throws(()=>set({assistant:{definition}}),code('organization-definition-refused'),String(definition));
|
|
assert.throws(()=>set({ghost:{vars:{}}}),code('invalid-request'));assert.throws(()=>set({assistant:{}}),code('invalid-request'));
|
|
assert.throws(()=>set({assistant:{holder:'rex'}}),/unsupported assistant settings assistant key: "holder"/);assert.throws(()=>set({assistant:{credentials:{}}}),/unsupported assistant settings assistant key: "credentials"/);
|
|
assert.throws(()=>set({assistant:{vars:{harness:'cursor'}}}),code('harness-off'));assert.throws(()=>set({assistant:{vars:{model:'ghost'}}}),/unknown model/);
|
|
assert.throws(()=>set({assistant:{tracker:{bot:'bot-acme-assistant',botId:11}}}),/has "tracker" but assistant uses no vikunja credential/);
|
|
const moved=settings.set('acme',{revision,assistants:{assistant:{vars:{model:'opus'}}}},'jason');assert.notEqual(moved.revision,revision);const after=readFileSync(f.file,'utf8');
|
|
assert.throws(()=>set({assistant:{vars:{model:'sonnet'}}}),code('revision-conflict'));assert.equal(readFileSync(f.file,'utf8'),after);assert.notEqual(after,bytes);
|
|
});
|
|
test('Q169 assistants: a definition with a vikunja credential takes a tracker change and refuses its removal or a used bot',async()=>{
|
|
const f=fixture(),role=JSON.parse(readFileSync(join(f.rolesDir,'assistant.json'),'utf8'));role.credentials=[{service:'vikunja',scopes:{tasks:['read_one']}}];writeJson(join(f.rolesDir,'assistant.json'),role);
|
|
withAssistant(f,{tracker:{bot:'bot-acme-assistant',botId:11},credentials:{vikunja:{file:tokenFile(join(f.root,'secrets'),'assistant-vikunja.token'),expires:FAR_DATE}}});const settings=await assistantSettings(f),view=settings.read('acme'),set=assistants=>settings.set('acme',{revision:view.revision,assistants},'jason');
|
|
assert.deepEqual(view.assistants.assistant.tracker,{bot:'bot-acme-assistant',botId:11});
|
|
assert.throws(()=>set({assistant:{tracker:null}}),/needs "tracker"/);assert.throws(()=>set({assistant:{tracker:{bot:'bot-acme-pm',botId:12}}}),/used by another instance/);assert.throws(()=>set({assistant:{tracker:{bot:'bot-acme-x',botId:12,holder:'rex'}}}),/unsupported/);
|
|
const done=set({assistant:{tracker:{bot:'bot-acme-helper',botId:12}}});assert.deepEqual(done.assistants.assistant.tracker,{bot:'bot-acme-helper',botId:12});assert.equal(JSON.parse(readFileSync(f.file,'utf8')).roles.assistant.credentials.vikunja.file,join(f.root,'secrets','assistant-vikunja.token'));
|
|
});
|
|
// Row 65 (mos-0840): the routing table. routes() reads one row per instance with its six values, and
|
|
// setRoutes() saves a route and a whole fallback in one write, refusing what a launch would refuse.
|
|
const LEVELS={pi:['off','low','high'],'claude-code':['low','high']};
|
|
async function routeSettings(f,harness=['pi','claude-code']){const doc=structuredClone(f.doc);if(harness)doc.plugins={...doc.plugins,harness};else delete doc.plugins?.harness;for(const r of Object.values(doc.roles))r.vars={harness:'pi',thinking:'high',...r.vars};writeJson(f.file,doc);const {launchSettings}=await import('../src/launch-settings.mjs');return {doc,settings:launchSettings({dir:f.root,rolesDir:f.rolesDir,system:{environment:'development',dataRoot:join(f.root,'data'),'execution.backend':'docker','execution.provider':'anthropic','execution.model':'sonnet','execution.adapter':'pi'},levels:name=>LEVELS[name]??null})};}
|
|
const routeOf=v=>({harness:v.harness??null,model:v.model??null,thinking:v.thinking??null,fallback:null});
|
|
test('row 65 routes lists every instance with harness, model, thinking and fallback, the switched-on harnesses, the model keys and each harness\'s levels',async()=>{
|
|
const f=fixture();const {doc,settings}=await routeSettings(f);const view=settings.routes('acme');
|
|
assert.deepEqual(Object.keys(view.instances).sort(),Object.keys(doc.roles).sort());
|
|
for(const [instance,{vars}] of Object.entries(doc.roles))assert.deepEqual(view.instances[instance],routeOf(vars),instance);
|
|
assert.deepEqual([view.harnesses,view.models,view.levels],[['pi','claude-code'],['opus','sonnet'],{'claude-code':['low','high'],pi:['off','low','high']}]);
|
|
assert.match(view.revision,/^[a-f0-9]{64}$/);
|
|
});
|
|
test('row 65 setRoutes saves a harness, model, thinking and whole fallback, reads each back from the file, logs the actor, and a null fallback clears all three',async()=>{
|
|
const f=fixture();const {settings}=await routeSettings(f);const {revision}=settings.routes('acme'),{harness,model,thinking,...rest}=JSON.parse(readFileSync(f.file,'utf8')).roles.coder.vars;
|
|
const done=settings.setRoutes('acme',{revision,routes:{coder:{harness:'claude-code',model:'opus',thinking:'low',fallback:{harness:'pi',model:'sonnet',thinking:'off'}}}},'jason');
|
|
const vars=JSON.parse(readFileSync(f.file,'utf8')).roles.coder.vars;
|
|
// The coder's other vars stay as they were.
|
|
assert.deepEqual(vars,{...rest,harness:'claude-code',model:'opus',thinking:'low','fallback.harness':'pi','fallback.model':'sonnet','fallback.thinking':'off'});
|
|
assert.deepEqual(done.instances.coder,{harness:'claude-code',model:'opus',thinking:'low',fallback:{harness:'pi',model:'sonnet',thinking:'off'}});assert.match(done.transaction,/./);
|
|
const e=JSON.parse(readFileSync(settings.writer.logFile('acme'),'utf8').trim().split('\n').at(-1));assert.equal(e.actor,'jason');assert.ok(e.changes.some(c=>c.path==='roles.coder.vars.fallback.harness'||c.path==='roles.coder.vars'));
|
|
settings.setRoutes('acme',{revision:done.revision,routes:{coder:{harness:'claude-code',model:'opus',thinking:'low',fallback:null}}},'jason');
|
|
assert.deepEqual(JSON.parse(readFileSync(f.file,'utf8')).roles.coder.vars,{...rest,harness:'claude-code',model:'opus',thinking:'low'});
|
|
});
|
|
test('row 65 setRoutes refuses a partial fallback, a switched-off harness, a level the harness doesn\'t apply, an unloaded module, an unknown model or instance, a malformed route, no change and a stale revision, with the file unchanged',async()=>{
|
|
const f=fixture();const {settings}=await routeSettings(f,['pi']);const {revision}=settings.routes('acme'),bytes=readFileSync(f.file,'utf8');
|
|
const route=(more={})=>({harness:'pi',model:'sonnet',thinking:'low',fallback:null,...more}),set=(routes,rev=revision)=>settings.setRoutes('acme',{revision:rev,routes},'jason');
|
|
for(const [routes,c] of [
|
|
[{coder:route({fallback:{harness:'pi',model:'sonnet'}})},'organization-partial-fallback'],
|
|
[{coder:route({fallback:{harness:'pi',model:'sonnet',thinking:null}})},'organization-partial-fallback'],
|
|
[{coder:route({harness:'claude-code'})},'harness-off'],
|
|
[{coder:route({fallback:{harness:'claude-code',model:'sonnet',thinking:'low'}})},'harness-off'],
|
|
[{coder:route({thinking:'max'})},'thinking-unsupported'],
|
|
[{coder:route({fallback:{harness:'pi',model:'sonnet',thinking:'max'}})},'thinking-unsupported'],
|
|
// setRoutes names an unknown primary model before the file's own validation can refuse it uncoded.
|
|
[{coder:route({model:'ghost'})},'unknown-model'],
|
|
[{coder:route({fallback:{harness:'pi',model:'ghost',thinking:'low'}})},'unknown-model'],
|
|
[{ghost:route()},'organization-unknown-instance'],
|
|
[{coder:{harness:'pi',model:'sonnet',thinking:'low'}},'invalid-request'],
|
|
[{coder:route({extra:1})},'invalid-request'],
|
|
[{},'invalid-request'],
|
|
])assert.throws(()=>set(routes),typeof c==='string'?code(c):c,JSON.stringify(routes));
|
|
assert.throws(()=>settings.setRoutes('acme',{revision,routes:{coder:route()},instances:{}},'jason'),/unsupported routes update key/);
|
|
// A switched-on harness whose module didn't load has no known levels.
|
|
const g=fixture();const {settings:other}=await routeSettings(g,['pi','cursor']);assert.throws(()=>other.setRoutes('acme',{revision:other.routes('acme').revision,routes:{coder:route({harness:'cursor'})}},'jason'),code('harness-refused'));
|
|
const {coder}=JSON.parse(bytes).roles;assert.throws(()=>set({coder:{harness:coder.vars.harness,model:coder.vars.model,thinking:coder.vars.thinking,fallback:null}}),code('organization-no-change'));
|
|
assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
const moved=set({coder:route()});assert.notEqual(moved.revision,revision);const after=readFileSync(f.file,'utf8');
|
|
assert.throws(()=>set({coder:route({thinking:'high'})}),code('revision-conflict'));assert.equal(readFileSync(f.file,'utf8'),after);
|
|
});
|
|
test('row 65 a file with harness vars and no plugins.harness loads and its views read; a model change still saves, and a route change refuses harness-off',async()=>{
|
|
const f=fixture();const {doc,settings}=await routeSettings(f,null);
|
|
const view=settings.routes('acme');assert.deepEqual(view.harnesses,[]);assert.deepEqual(view.instances.coder,routeOf(doc.roles.coder.vars));assert.equal(settings.read('acme').instances.coder.modelKey,'sonnet');
|
|
const moved=settings.set('acme',{revision:view.revision,instances:{coder:'opus'}},'jason');assert.equal(moved.instances.coder.modelKey,'opus');
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:moved.revision,routes:{coder:{harness:'pi',model:'opus',thinking:'low',fallback:null}}},'jason'),code('harness-off'));
|
|
});
|
|
// Row 194 (D2.4 B7, webby-0094 N3): a kept instance's model is the human's, through its own key in
|
|
// instances or a models key it names, checked against the file. A key no kept instance names stays
|
|
// the chief's.
|
|
test('B7 kept instance: the models keys it names and the instances key are the human\'s',async()=>{
|
|
const f=fixture();const doc=structuredClone(f.doc);doc.launch.keep=['coder'];writeJson(f.file,doc);
|
|
const g={...f,doc};const {settings}=await routeSettings(g);const file=()=>readFileSync(f.file,'utf8');
|
|
const chief='chief-of-staff.chief';let {revision}=settings.read('acme');
|
|
const models=structuredClone(settings.read('acme').models);
|
|
const key=doc.roles.coder.vars.model;
|
|
const moved={...models,[key]:{provider:'proxy',route:'anthropic',model:'claude-sonnet-5-5'}};
|
|
for(const actor of [chief,'tester']){
|
|
const bytes=file();
|
|
assert.throws(()=>settings.set('acme',{revision,models:moved},actor),code('instance-kept'),actor);
|
|
assert.throws(()=>settings.set('acme',{revision,instances:{coder:'opus'}},actor),code('instance-kept'),actor);
|
|
assert.equal(file(),bytes,actor);
|
|
}
|
|
({revision}=settings.set('acme',{revision,models:{...models,spare:{provider:'proxy',route:'anthropic',model:'claude-opus-5-5'}}},chief));
|
|
({revision}=settings.set('acme',{revision,models:{...settings.read('acme').models,[key]:moved[key]}},'jason'));
|
|
settings.set('acme',{revision,instances:{coder:'opus'}},'jason');
|
|
assert.equal(JSON.parse(file()).roles.coder.vars.model,'opus');
|
|
});
|
|
// webby-0095 N1 (sage-2248): routes.set writes a kept instance's model too, so it's the human's.
|
|
test('B7 kept instance: its route is the human\'s',async()=>{
|
|
const f=fixture();const doc=structuredClone(f.doc);doc.launch.keep=['coder'];writeJson(f.file,doc);
|
|
const g={...f,doc};const {settings}=await routeSettings(g);const file=()=>readFileSync(f.file,'utf8');
|
|
const {revision}=settings.routes('acme');
|
|
const route={harness:'pi',model:'opus',thinking:'low',fallback:null};
|
|
for(const actor of ['chief-of-staff.chief','tester']){
|
|
const bytes=file();
|
|
assert.throws(()=>settings.setRoutes('acme',{revision,routes:{coder:route}},actor),code('instance-kept'),actor);
|
|
assert.equal(file(),bytes,actor);
|
|
}
|
|
settings.setRoutes('acme',{revision,routes:{coder:route}},'jason');
|
|
assert.equal(JSON.parse(file()).roles.coder.vars.model,'opus');
|
|
});
|
|
// Row 213 (C2-08, #1714, M10): a creative instance is the human's. The chief of staff (and the tester)
|
|
// can't reach its model through the models table, the instances key or a route; the human can,
|
|
// except through the instances key, which refuses a creative instance for everyone.
|
|
test('M10 creative instance: models keys it names, the instances key and its route are the human\'s',async()=>{
|
|
const f=fixture();const doc=structuredClone(f.doc);doc.roles.bean={definition:'companion',credentials:{gitea:{store:true,rotateBy:FAR_DATE}},vars:{model:'sonnet'}};writeJson(f.file,doc);
|
|
const g={...f,doc};const {settings}=await routeSettings(g);const file=()=>readFileSync(f.file,'utf8');
|
|
const chief='chief-of-staff.chief';let {revision}=settings.read('acme');
|
|
const models=structuredClone(settings.read('acme').models);
|
|
const sonnet={...models,sonnet:{provider:'proxy',route:'anthropic',model:'claude-sonnet-5-5'}};
|
|
for(const actor of [chief,'tester']){
|
|
const bytes=file();
|
|
assert.throws(()=>settings.set('acme',{revision,models:sonnet},actor),code('organization-creative-human-only'),actor);
|
|
const {sonnet:_,...gone}=models;assert.throws(()=>settings.set('acme',{revision,models:gone},actor),code('organization-creative-human-only'),`${actor} removal`);
|
|
assert.throws(()=>settings.set('acme',{revision,instances:{bean:'opus'}},actor),code('organization-creative-instance'),actor);
|
|
assert.throws(()=>settings.setRoutes('acme',{revision,routes:{bean:{harness:'pi',model:'opus',thinking:'high',fallback:null}}},actor),code('organization-creative-human-only'),actor);
|
|
assert.equal(file(),bytes,actor);
|
|
}
|
|
// A key no creative instance names stays the chief's.
|
|
({revision}=settings.set('acme',{revision,models:{...models,opus:{provider:'proxy',route:'anthropic',model:'claude-opus-5-5'}}},chief));
|
|
assert.throws(()=>settings.set('acme',{revision,instances:{bean:'opus'}},'jason'),code('organization-creative-instance'));
|
|
({revision}=settings.set('acme',{revision,models:sonnet},'jason'));
|
|
({revision}=settings.setRoutes('acme',{revision,routes:{bean:{harness:'pi',model:'sonnet',thinking:'high',fallback:{harness:'pi',model:'opus',thinking:'low'}}}},'jason'));
|
|
assert.equal(JSON.parse(file()).roles.bean.vars['fallback.model'],'opus');
|
|
// Now bean names opus as its fallback, so opus is the human's too.
|
|
const opus=structuredClone(settings.read('acme').models);opus.opus.provider='proxy-next';
|
|
assert.throws(()=>settings.set('acme',{revision,models:opus},chief),code('organization-creative-human-only'));
|
|
settings.set('acme',{revision,models:opus},'jason');
|
|
const e=JSON.parse(readFileSync(settings.writer.logFile('acme'),'utf8').trim().split('\n').at(-1));assert.equal(e.actor,'jason');
|
|
});
|
|
test('fixture catalogue materializes a new model and saves its route in one revision-checked write', async () => {
|
|
const f=fixture(); const {settings}=await routeSettings(f); const configured=JSON.parse(readFileSync(f.file,'utf8')); configured.plugins['model-provider']=['cliproxy']; writeJson(f.file,configured); const view=settings.routes('acme');
|
|
const catalogue={status:'ready',models:[{key:'zai-fixture-id',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-id',label:'zai / fixture-id',available:true,reason:null,harnesses:['pi']}],notices:[]};
|
|
assert.equal(view.models.includes('zai-fixture-id'),false);
|
|
const done=settings.setRoutes('acme',{revision:view.revision,routes:{coder:{harness:'pi',model:'zai-fixture-id',thinking:'low',fallback:null}}},'jason',catalogue);
|
|
const data=JSON.parse(readFileSync(f.file,'utf8'));
|
|
assert.deepEqual(data.launch.models['zai-fixture-id'],{provider:'zai',model:'fixture-id',module:'cliproxy',upstream:'zai'});
|
|
assert.equal(done.instances.coder.model,'zai-fixture-id');
|
|
});
|
|
test('catalogue selections refuse removed models, incompatible harnesses, collisions and indirect writes', async () => {
|
|
const f=fixture(); const {settings}=await routeSettings(f); const configured=JSON.parse(readFileSync(f.file,'utf8')); configured.plugins['model-provider']=['cliproxy']; writeJson(f.file,configured);
|
|
const view=settings.routes('acme'), offered={key:'zai-fixture-id',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-id',label:'Fixture',available:true,reason:null,harnesses:['pi']};
|
|
const route=(model='zai-fixture-id',harness='pi')=>({coder:{harness,model,thinking:'low',fallback:null}});
|
|
const catalog={status:'ready',models:[offered],notices:[]};
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:view.revision,routes:route('zai-fixture-id','claude-code')},'jason',catalog),code('model-harness-incompatible'));
|
|
const done=settings.setRoutes('acme',{revision:view.revision,routes:route()},'jason',catalog);
|
|
const file=JSON.parse(readFileSync(f.file,'utf8')); file.roles.coder.vars.model='sonnet'; writeJson(f.file,file);
|
|
const rev=settings.routes('acme').revision;
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:rev,routes:route()},'jason',{status:'ready',models:[],notices:[]}),code('model-provider-unavailable'));
|
|
assert.throws(()=>settings.set('acme',{revision:rev,instances:{coder:'zai-fixture-id'}},'jason'),code('model-provider-unavailable'));
|
|
const collision={...offered,key:'sonnet'};
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:rev,routes:{coder:{...route('sonnet').coder,thinking:'high'}}},'jason',{status:'ready',models:[collision],notices:[]}),code('model-key-collision'));
|
|
assert.ok(done.revision);
|
|
});
|
|
test('a changed legacy route refuses while the enabled catalogue is unavailable', async () => {
|
|
const f=fixture(); const {settings}=await routeSettings(f); const doc=JSON.parse(readFileSync(f.file,'utf8')); doc.plugins['model-provider']=['cliproxy']; doc.launch.models['zai-fixture-id']={provider:'zai',model:'fixture-id',module:'cliproxy',upstream:'zai'}; doc.roles.coder.vars.model='zai-fixture-id'; writeJson(f.file,doc);
|
|
const view=settings.routes('acme');
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:view.revision,routes:{coder:{harness:'pi',model:'sonnet',thinking:'low',fallback:null}}},'jason',{status:'unavailable',models:[],notices:[]}),code('model-provider-unavailable'));
|
|
});
|
|
|
|
test('indirect selection of an existing provider model uses fresh catalogue availability', async () => {
|
|
const f=fixture();const {settings}=await routeSettings(f);const doc=JSON.parse(readFileSync(f.file,'utf8'));doc.plugins['model-provider']=['cliproxy'];doc.launch.models['zai-fixture-id']={provider:'zai',model:'fixture-id',module:'cliproxy',upstream:'zai'};writeJson(f.file,doc);
|
|
const offered={key:'zai-fixture-id',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-id',label:'Fixture',available:true,reason:null,harnesses:['pi']};
|
|
const view=settings.routes('acme');
|
|
const saved=settings.set('acme',{revision:view.revision,instances:{coder:'zai-fixture-id'}},'jason',{status:'ready',models:[offered]});
|
|
assert.equal(saved.instances.coder.modelKey,'zai-fixture-id');
|
|
});
|
|
|
|
test('changed legacy primary and fallback refuse unavailable catalogue models', async () => {
|
|
const f=fixture();const {settings}=await routeSettings(f);const doc=JSON.parse(readFileSync(f.file,'utf8'));doc.plugins['model-provider']=['cliproxy'];writeJson(f.file,doc);
|
|
const view=settings.routes('acme');
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:view.revision,routes:{coder:{harness:'pi',model:'opus',thinking:'low',fallback:null}}},'jason',{status:'ready',models:[],legacy:{opus:{module:'cliproxy',upstream:'claude',model:doc.launch.models.opus.model}}}),code('model-provider-unavailable'));
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:view.revision,routes:{coder:{...routeOf(doc.roles.coder.vars),fallback:{harness:'pi',model:'opus',thinking:'low'}}}},'jason',{status:'ready',models:[],legacy:{opus:{module:'cliproxy',upstream:'claude',model:doc.launch.models.opus.model}}}),code('model-provider-unavailable'));
|
|
});
|
|
|
|
test('provider model materialization is human-only across indirect and Routing writers', async () => {
|
|
const f=fixture();const {settings}=await routeSettings(f);const doc=JSON.parse(readFileSync(f.file,'utf8'));doc.plugins['model-provider']=['cliproxy'];writeJson(f.file,doc);
|
|
const view=settings.routes('acme'),offered={key:'fixture-key',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-id',label:'Fixture',available:true,reason:null,harnesses:['pi']};const catalogue={status:'ready',models:[offered],legacy:{}};
|
|
const entry={provider:'zai',model:'fixture-id',module:'cliproxy',upstream:'zai'};
|
|
assert.throws(()=>settings.set('acme',{revision:view.revision,models:{...doc.launch.models,'fixture-key':entry}},'chief-of-staff.chief',catalogue),code('organization-model-provider-human-only'));
|
|
const legacyModels={...doc.launch.models,'fixture-key':{provider:'zai',model:'fixture-id'}};
|
|
assert.throws(()=>settings.set('acme',{revision:view.revision,models:legacyModels},'chief-of-staff.chief',catalogue),code('model-provider-unavailable'));
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:view.revision,routes:{coder:{harness:'pi',model:'fixture-key',thinking:'low',fallback:null}}},'tester',catalogue),code('organization-model-provider-human-only'));
|
|
});
|
|
|
|
test('a role can add a legacy model when the model-provider module is off', async () => {
|
|
const f=fixture();const {settings}=await routeSettings(f);const view=settings.read('acme');
|
|
const models={...view.models,'legacy-new':{provider:'zai',model:'fixture-id'}};
|
|
const saved=settings.set('acme',{revision:view.revision,models,instances:{coder:'legacy-new'}},'chief-of-staff.chief');
|
|
assert.equal(saved.instances.coder.modelKey,'legacy-new');
|
|
});
|