The catalogue refusals no longer tell every user to press Disable model proxy catalogue. The server text names the proxy setup step. The Routing page adds the Disable step only when cliproxy is listed and every moved model comes from another listed provider, and sends a tester to the owner. The switch mismatch message no longer claims a setting changed. Tests: a fallback thinking-only change saves with no catalogue; a move between model-proxy models refuses before and after cliproxy is removed.
302 lines
37 KiB
JavaScript
302 lines
37 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');
|
|
});
|
|
|
|
test('D3 P0-4: a thinking-only change on a provider model saves with no catalogue, and a model or harness change still refuses', async () => {
|
|
const f=fixture();const {settings}=await routeSettings(f);const doc=JSON.parse(readFileSync(f.file,'utf8'));
|
|
doc.plugins['model-provider']=['fixture','cliproxy'];doc.launch.models['fixture-deterministic']={provider:'proxy',model:'fixture-deterministic',module:'fixture',upstream:'fixture'};
|
|
doc.roles.coder.vars={...doc.roles.coder.vars,harness:'pi',model:'fixture-deterministic',thinking:'off'};writeJson(f.file,doc);
|
|
const pick=(thinking,extra={})=>({coder:{harness:'pi',model:'fixture-deterministic',thinking,fallback:null,...extra}});
|
|
for(const catalogue of [null,{status:'unavailable',models:[],notices:[]}]){
|
|
const view=settings.routes('acme'),next=view.instances.coder.thinking==='low'?'high':'low';
|
|
settings.setRoutes('acme',{revision:view.revision,routes:pick(next)},'jason',catalogue);
|
|
assert.equal(JSON.parse(readFileSync(f.file,'utf8')).roles.coder.vars.thinking,next);
|
|
}
|
|
// A level the harness doesn't apply still refuses on a thinking-only change.
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:settings.routes('acme').revision,routes:pick('medium')},'jason',null),code('thinking-unsupported'));
|
|
// A harness or model change truly needs the catalogue.
|
|
const rev=settings.routes('acme').revision,before=readFileSync(f.file,'utf8');
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:rev,routes:pick('low',{harness:'claude-code'})},'jason',{status:'unavailable',models:[],notices:[]}),code('model-catalogue-unavailable'));
|
|
assert.throws(()=>settings.set('acme',{revision:rev,instances:{reviewer:'fixture-deterministic'}},'jason',{status:'unavailable',models:[],notices:[]}),code('model-provider-unavailable'));
|
|
assert.equal(readFileSync(f.file,'utf8'),before);
|
|
});
|
|
|
|
test('D3 review: a fallback thinking-only change on a provider model saves with no catalogue, and a fallback harness change still refuses', async () => {
|
|
const f=fixture();const {settings}=await routeSettings(f);const doc=JSON.parse(readFileSync(f.file,'utf8'));
|
|
doc.plugins['model-provider']=['fixture','cliproxy'];doc.launch.models['fixture-deterministic']={provider:'proxy',model:'fixture-deterministic',module:'fixture',upstream:'fixture'};
|
|
doc.roles.coder.vars={...doc.roles.coder.vars,harness:'pi',model:'fixture-deterministic',thinking:'off','fallback.harness':'pi','fallback.model':'fixture-deterministic','fallback.thinking':'off'};writeJson(f.file,doc);
|
|
const unavailable={status:'unavailable',models:[],notices:[]};
|
|
const pick=fallback=>({coder:{harness:'pi',model:'fixture-deterministic',thinking:'off',fallback:{harness:'pi',model:'fixture-deterministic',...fallback}}});
|
|
settings.setRoutes('acme',{revision:settings.routes('acme').revision,routes:pick({thinking:'low'})},'jason',unavailable);
|
|
assert.equal(JSON.parse(readFileSync(f.file,'utf8')).roles.coder.vars['fallback.thinking'],'low');
|
|
const before=readFileSync(f.file,'utf8');
|
|
assert.throws(()=>settings.setRoutes('acme',{revision:settings.routes('acme').revision,routes:pick({harness:'claude-code',thinking:'low'})},'jason',unavailable),code('model-catalogue-unavailable'));
|
|
assert.equal(readFileSync(f.file,'utf8'),before);
|
|
});
|