Files
stack/packages/cli/tests/launch-limit-checks.test.mjs

93 lines
5.3 KiB
JavaScript

// Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): the launcher asks the bus's limit checks before
// every launch, after the agent limits. A stub host answers the mayLaunch op; no sandbox, Docker
// or session process. Any answer that isn't an allow refuses (fail closed, lead decision 150).
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {fileURLToPath} from 'node:url';
import {createLauncher} from '../src/launcher.mjs';
import {initializeControl} from '../../processing-control/src/state.mjs';
import {fixture,tmp,withoutHostCredentials} from './helpers.mjs';
import {launchLogFile} from '../../seat/src/session.mjs';
const HIT={path:['tokens','instances','coder'],limit:100,count:120,unknownTurns:1};
// answer is what the mayLaunch op returns, or an Error it throws.
function setup(t,answer,{initialize=true}={}){
const root=tmp(t),f=fixture(root,'acme',doc=>{doc.roles.coder.vars.model='claude-sonnet-5-5';return doc;}),ops=[];
withoutHostCredentials(f.env,{piKey:true});
if(initialize)initializeControl(f.dataRoot);
const host={business:'acme',beforeClose:()=>{},op:async x=>{
ops.push(x);
if(x.op==='identity')return {business:'acme',role:'pm'};
if(x.op==='mayLaunch'){if(answer instanceof Error)throw answer;return answer;}
if(x.op==='authorizeLaunch')throw Object.assign(Error('fixture authorization stop'),{code:'fixture-policy'});
return {};
}};
const launcher=createLauncher({host,system:JSON.parse(readFileSync(f.config,'utf8')),env:f.env,bwrap:'/usr/bin/true',log:()=>{}});
const last=()=>JSON.parse(readFileSync(launchLogFile(f.dataRoot,'acme'),'utf8').trim().split('\n').at(-1));
return {launcher,ops,last};
}
test('REQ-LIMIT-4 the launcher asks mayLaunch with the instance, provider, model and account (row 126), and goes on to authorization on an allow',async t=>{
const {launcher,ops}=setup(t,{allow:true});
assert.equal((await launcher.fromSession('fixture-cap','coder')).error,'fixture-policy');
assert.deepEqual(ops.find(x=>x.op==='mayLaunch'),{op:'mayLaunch',business:'acme',instance:'coder',provider:'zai',model:'claude-sonnet-5-5',account:null});
assert.ok(ops.findIndex(x=>x.op==='mayLaunch')<ops.findIndex(x=>x.op==='authorizeLaunch'));
});
test('REQ-LIMIT-3 REQ-LIMIT-4 a launch whose subject is at its limit is refused limit-reached and logged with the check, limit and count',async t=>{
const {launcher,ops,last}=setup(t,{allow:false,check:'token-limit',hit:HIT});
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'limit-reached'});
const event=last();
assert.equal(event.event,'refused');
assert.equal(event.code,'limit-reached');
assert.deepEqual(event.limit,{check:'token-limit',...HIT});
assert.deepEqual(JSON.parse(event.why),HIT);
assert.ok(ops.find(x=>x.op==='refuse'&&x.code==='limit-reached'));
assert.equal(ops.find(x=>x.op==='authorizeLaunch'),undefined);
assert.deepEqual(launcher.registry.sessions,[]);
});
test('REQ-LIMIT-4 fail closed: a refused op, a check failure or a malformed answer refuses limit-check-failed, naming the check and the code',async t=>{
for(const [answer,why] of [
[Object.assign(Error('op refused'),{code:'quota-gone'}),'bus: quota-gone'],
[Error('no code'),'bus: launch-op-refused'],
[{allow:false,check:'token-limit',code:'not-loaded'},'token-limit: not-loaded'],
[{allow:false,check:'token-limit',code:'check-threw'},'token-limit: check-threw'],
[{allow:false,check:null,code:'organization-unreadable'},'bus: organization-unreadable'],
// Row 333 R3 (#1831) ruling 2: an agent with no model key under a key quota, by its own code.
[{allow:false,check:null,code:'key-quota-no-model-key'},'bus: key-quota-no-model-key'],
[{allow:'yes'},'bus: malformed-answer'],
[{},'bus: malformed-answer'],
[null,'bus: malformed-answer'],
[undefined,'bus: malformed-answer'],
]){
const {launcher,ops,last}=setup(t,answer);
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'limit-check-failed'},why);
const event=last();
assert.equal(event.code,'limit-check-failed',why);
assert.equal(event.why,why);
assert.equal(event.limit,undefined,why);
assert.equal(ops.find(x=>x.op==='authorizeLaunch'),undefined,why);
}
});
test('REQ-LIMIT-4 the launcher reaches token-limit only through the bus: launcher.mjs imports no limit check',()=>{
const src=readFileSync(fileURLToPath(new URL('../src/launcher.mjs',import.meta.url)),'utf8');
assert.doesNotMatch(src,/token-limit/);
assert.doesNotMatch(src,/limit-check\//);
// The launcher loads model providers and harnesses, but never a limit check.
assert.deepEqual([...src.matchAll(/\.load\(([^)]*)\)/g)].map(m=>m[1]),["{ 'model-provider': providerNames }",'{ harness: [m.harness] }']);
assert.doesNotMatch(src,/modules\(\s*["'](?!harness["'])/);
assert.doesNotMatch(src,/\bimport\s*\(/);
});
// Row 191: only trusted bus startup initializes control; an early seat launch refuses.
test('a launcher before trusted control initialization refuses processing-uninitialized',async t=>{
const {launcher,ops}=setup(t,{allow:true},{initialize:false});
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'processing-uninitialized'});
assert.equal(ops.some(x=>x.op==='mayLaunch'||x.op==='authorizeLaunch'),false);
assert.deepEqual(launcher.registry.sessions,[]);
});