Files
stack/packages/bus/tests/model-catalogue.test.mjs

98 lines
8.9 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createModelCatalogue } from '../src/model-catalogue.mjs';
test('catalogue caches per business, refreshes on expiry and invalidation, and forces a fresh launch read', async () => {
let clock = 1000, calls = 0;
const client = {
getCredentials: async () => ({ files: [] }),
getCredentialModels: async () => ({ models: [] }),
getUpstreamGroups: async () => { calls++; return [{ 'base-url': 'https://api.z.ai/api/anthropic', keys: [{}], models: [{ name: 'fixture-id' }] }]; },
};
const service = createModelCatalogue({ client: () => client, settings: () => ({ 'model-provider': ['cliproxy'] }), now: () => clock });
const [first, second] = await Promise.all([service.read('one'), service.read('one')]);
assert.equal(calls, 1); assert.deepEqual(first, second);
assert.equal(first.models[0].key, 'zai-fixture-id');
await service.read('one'); assert.equal(calls, 1);
await service.read('two'); assert.equal(calls, 2);
await service.read('one', { fresh: true }); assert.equal(calls, 3);
clock += 30001; await service.read('one'); assert.equal(calls, 4);
service.invalidate(); await service.read('one'); assert.equal(calls, 5);
});
test('disabled, refused, and failed providers never retain authorization', async () => {
let enabled = true, failed = false;
const client = { getCredentials: async () => ({ files: [] }), getCredentialModels: async () => ({ models: [] }),
getUpstreamGroups: async () => { if (failed) throw Error('fixture secret'); return [{ 'base-url': 'https://api.z.ai/api/anthropic', keys: [{}], models: [{ name: 'fixture-id' }] }]; } };
const service = createModelCatalogue({ client: () => client, settings: () => ({ 'model-provider': enabled ? ['cliproxy'] : [] }) });
assert.equal((await service.read('one')).models[0].available, true);
enabled = false; assert.equal((await service.read('one')).status, 'disabled');
enabled = true; failed = true;
const out = await service.read('one', { fresh: true });
assert.equal(out.status, 'unavailable');
assert.ok(out.models.every(m => !m.available));
assert.equal(JSON.stringify(out).includes('fixture secret'), false);
});
test('launch admission refreshes selection and refuses a removed model and incompatible harness', async t => {
const { startBroker } = await import('../src/runtime.mjs');
const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../organization/tests/helpers.mjs');
const { mkdtempSync, mkdirSync, rmSync } = await import('node:fs');
const { tmpdir } = await import('node:os'); const { join } = await import('node:path');
const root=mkdtempSync(join(tmpdir(),'model-admission-')); t.after(()=>rmSync(root,{recursive:true,force:true}));
const rolesDir=rolesCopy(root), doc=businessDoc(root,'demo'); doc.plugins={...doc.plugins,harness:['pi'],'model-provider':['cliproxy']};
doc.launch.models['zai-fixture-id']={provider:'zai',model:'fixture-id',module:'cliproxy',upstream:'zai'};
doc.roles.coder.vars={...doc.roles.coder.vars,harness:'pi',model:'zai-fixture-id',thinking:'low'};
writeJson(join(root,'businesses/demo.json'),doc); mkdirSync(join(root,'data'),{mode:0o700});
let present=true, reads=0;
const service={invalidate(){},read:async(_,{fresh})=>{assert.equal(fresh,true);reads++;return {status:'ready',models:present?[{module:'cliproxy',upstream:'zai',model:'fixture-id',available:true,harnesses:['pi']}]:[]};}};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue:service});t.after(()=>bus.close());
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const revision=bus.broker.request(human,{verb:'limits'}).revision;
const {checkedTransport}=await import('../../harness/src/bundle.mjs');
const entry=doc.launch.models['zai-fixture-id'];
const request={business:'demo',instance:'coder',modelKey:'zai-fixture-id',harness:'pi',revision,entry,provider:entry.provider,model:entry.model,account:null,route:'primary',transport:checkedTransport(entry)};
assert.deepEqual(await bus.modelAdmission(request),{allow:true});
present=false;
assert.deepEqual(await bus.modelAdmission(request),{allow:false,code:'model-provider-unavailable'});
assert.equal(reads,2);
});
test('an account mutation fences reads begun before or during the write', async () => {
let release;const gate=new Promise(r=>{release=r;});
const client={getCredentials:async()=>({files:[]}),getCredentialModels:async()=>({models:[]}),getUpstreamGroups:async()=>{await gate;return [{'base-url':'https://api.z.ai/api/anthropic',keys:[{}],models:[{name:'fixture-id'}]}];}};
const service=createModelCatalogue({client:()=>client,settings:()=>({'model-provider':['cliproxy']})});
const read=service.read('one',{fresh:true});service.begin();
assert.equal((await service.read('one',{fresh:true})).status,'unavailable');
release();assert.equal((await read).status,'unavailable');service.end();
assert.equal((await service.read('one',{fresh:true})).status,'ready');
});
test('a different enabled fixture provider supplies legacy mapping and admission without a core case', async t => {
const {startBroker}=await import('../src/runtime.mjs');const {businessDoc,rolesCopy,systemFor,writeJson}=await import('../../organization/tests/helpers.mjs');
const {mkdtempSync,mkdirSync,rmSync}=await import('node:fs');const {tmpdir}=await import('node:os');const {join}=await import('node:path');
const root=mkdtempSync(join(tmpdir(),'second-provider-'));t.after(()=>rmSync(root,{recursive:true,force:true}));const rolesDir=rolesCopy(root),doc=businessDoc(root,'demo');
doc.plugins={...doc.plugins,harness:['pi'],'model-provider':['second-fixture']};doc.launch.models['fixture-legacy']={provider:'fixture',model:'fixture-model'};doc.roles.coder.vars={...doc.roles.coder.vars,harness:'pi',model:'fixture-legacy',thinking:'low'};
writeJson(join(root,'businesses/demo.json'),doc);mkdirSync(join(root,'data'),{mode:0o700});
const transport={routeId:'fixture',endpoint:'v1',api:'openai-completions',piProvider:'fixture',compat:{thinkingFormat:'openai',zaiToolStream:false}};
const module={route:({selection})=>selection.provider==='fixture'?{provider:selection.provider,model:selection.model,upstream:'fixture',...transport}:{code:'unsupported'},preflight:({selection,catalogue})=>({allow:catalogue.models.some(m=>m.upstream===selection.upstream&&m.model===selection.model)})};
const makeProviderLoader=()=>({load:async()=>({refused:[]}),snapshot:()=>[{name:'second-fixture',module}]});
const offered={key:'fixture-model',module:'second-fixture',upstream:'fixture',provider:'fixture',model:'fixture-model',label:'Fixture',available:true,reason:null,harnesses:['pi']};
const modelCatalogue={read:async()=>({status:'ready',models:[offered],notices:[]}),invalidate(){}};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue,makeProviderLoader});t.after(()=>bus.close());
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});const view=await bus.broker.request(human,{verb:'routes'});
assert.equal(view.catalogue.legacy['fixture-legacy'].module,'second-fixture');
const answer=await bus.modelAdmission({business:'demo',instance:'coder',modelKey:'fixture-legacy',harness:'pi',revision:view.revision,entry:doc.launch.models['fixture-legacy'],provider:'fixture',model:'fixture-model',account:null,route:'primary',transport});
assert.deepEqual(answer,{allow:true});
});
test('shared generation invalidates cached catalogues and an uncertain fence makes them unavailable', async () => {
let version = 0, uncertain = false, calls = 0;
const lock = { generation: () => version, assertCertain: () => { if (uncertain) throw Error('uncertain'); } };
const client = { getCredentials: async () => ({ files: [] }), getCredentialModels: async () => ({ models: [] }),
getUpstreamGroups: async () => { calls++; return [{ 'base-url': 'https://api.z.ai/api/anthropic', keys: [{}], models: [{ name: 'fixture-id' }] }]; } };
const service = createModelCatalogue({ client: () => client, settings: () => ({ 'model-provider': ['cliproxy'] }), proxyLock: lock });
assert.equal((await service.read('one')).status, 'ready');
version++; assert.equal((await service.read('one')).status, 'ready'); assert.equal(calls, 2);
uncertain = true; assert.equal((await service.read('one')).status, 'unavailable');
});