Files
stack/packages/bus/tests/provider-materialization-guard.test.mjs

44 lines
3.9 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {join} from 'node:path';
import {tmpdir} from 'node:os';
import {DatabaseSync} from 'node:sqlite';
import {startBroker} from '../src/runtime.mjs';
import {businessDoc,rolesCopy,systemFor,writeJson} from '../../organization/tests/helpers.mjs';
test('role must not materialize a new catalogue key by submitting legacy entry shape',async t=>{
const root=fs.mkdtempSync(join(tmpdir(),'materialize-'));t.after(()=>fs.rmSync(root,{recursive:true,force:true}));
const rolesDir=rolesCopy(root),doc=businessDoc(root,'demo'),file=join(root,'businesses/demo.json');
doc.plugins={...doc.plugins,harness:['pi'],'model-provider':['cliproxy']};doc.roles.coder.vars={...doc.roles.coder.vars,harness:'pi',thinking:'low'};
writeJson(file,doc);fs.mkdirSync(join(root,'data'),{mode:0o700});
const authority=JSON.parse(fs.readFileSync('./roles/chief-of-staff.json','utf8')).authority;
const offered={key:'zai-fixture-new',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-new',label:'Fixture',available:true,reason:null,harnesses:['pi']};
let status='ready';
const modelCatalogue={invalidate(){},read:async()=>({status,models:[offered],notices:[]})};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{coder:{authority:{withinRole:[],crossRole:[]}},chief:{definition:'chief-of-staff',authority}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue});t.after(()=>bus.close());
const db=new DatabaseSync(join(root,'data/bus/bus.sqlite'),{readOnly:true});t.after(()=>db.close());
const allowed=()=>db.prepare("SELECT count(*) AS n FROM events WHERE kind='action.allowed' AND json_extract(body,'$.action')='organization.change'").get().n;
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const cap=bus.broker.bindLaunch({business:'demo',role:'chief',run:'materialize-chief',harness:'pi'});bus.broker.request(cap,{verb:'role.claim'});
const view=bus.broker.request(human,{verb:'limits'});assert.equal(Object.hasOwn(view.models,offered.key),false);
const models={...view.models,[offered.key]:{provider:offered.provider,model:offered.model,module:offered.module,upstream:offered.upstream}};
const args={revision:view.revision,models,instances:{coder:offered.key}}, bytes=fs.readFileSync(file,'utf8');
await assert.rejects(bus.broker.request(cap,{verb:'organization.set',args}),{code:'organization-model-provider-human-only'});
assert.equal(allowed(),0);assert.equal(fs.readFileSync(file,'utf8'),bytes);
delete models[offered.key].module;delete models[offered.key].upstream;
await assert.rejects(bus.broker.request(cap,{verb:'organization.set',args}),{code:'organization-model-provider-human-only'});
assert.equal(allowed(),0);assert.equal(fs.readFileSync(file,'utf8'),bytes);
// A different, unselected key is still materialization of the trusted identity.
const aliasModels={...view.models,'new-alias':{provider:offered.provider,model:offered.model}};
await assert.rejects(bus.broker.request(cap,{verb:'organization.set',args:{revision:view.revision,models:aliasModels}}),{code:'organization-model-provider-human-only'});
status='unavailable';
await assert.rejects(bus.broker.request(cap,{verb:'organization.set',args:{revision:view.revision,models:aliasModels}}),{code:'model-provider-unavailable'});
assert.equal(allowed(),0);assert.equal(fs.readFileSync(file,'utf8'),bytes);
// A human may add the entry; a role may then select that existing key.
status='ready';
const saved=await bus.broker.request(human,{verb:'limits.set',args:{revision:view.revision,models:aliasModels}});
await bus.broker.request(cap,{verb:'organization.set',args:{revision:saved.revision,instances:{coder:'new-alias'}}});
assert.equal(JSON.parse(fs.readFileSync(file,'utf8')).roles.coder.vars.model,'new-alias');
assert.equal(allowed(),1);
});