Files
stack/packages/bus/tests/p5-runtime.test.mjs
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
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.
2026-11-09 11:47:48 -06:00

111 lines
8.8 KiB
JavaScript

// P5 end-to-end within one process: only fresh fake config, credentials and loopback HTTP.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { createHash, randomBytes } from 'node:crypto';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { startBroker } from '../src/runtime.mjs';
import { Registry } from '../../seat/src/session.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../organization/tests/helpers.mjs';
const hash=s=>createHash('sha256').update(s).digest('hex').slice(0,12);
test('P5 real runtime binds stack usage to issued key, daily stops at stack count and human-only telemetry settings pass broker contract',async t=>{
const root=mkdtempSync(join(tmpdir(),'p5-runtime-')),config=join(root,'config'),data=join(root,'data');
mkdirSync(config,{mode:0o700});mkdirSync(data,{mode:0o700});const rolesDir=rolesCopy(config),doc=businessDoc(config,'demo');
doc.plugins={'usage-source':['transcript'],'limit-check':['daily-quota'],harness:['pi']};
writeJson(join(config,'businesses/demo.json'),doc);mkdirSync(join(config,'secrets'),{recursive:true,mode:0o700});
writeFileSync(join(config,'secrets/proxy-management.key'),'p5-fake-management',{mode:0o600});
new Registry(data);
let keys=[`msk-${hash(config)}-sentinel-${randomBytes(32).toString('base64url')}`],queue=[],calls=[];
const server=http.createServer(async(req,res)=>{
calls.push(req.url);
if(req.url==='/v8/management/config/access/api-keys') {
if(req.method==='PUT'){let b='';for await(const c of req)b+=c;keys=JSON.parse(b);res.end('{"status":"ok"}');}
else res.end(JSON.stringify(keys));return;
}
if(req.url==='/v8/management/observability/usage/queue?count=100'){res.end(JSON.stringify(queue));queue=[];return;}
res.writeHead(keys.includes(req.headers.authorization?.slice(7))?200:401);res.end('{}');
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));let bus;
t.after(async()=>{await bus?.close();await new Promise(r=>server.close(r));rmSync(root,{recursive:true,force:true});});
bus=await startBroker({dataRoot:data,businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
organization:{dir:config,rolesDir,system:systemFor(config)},proxy:{port:server.address().port,binary:'/fixture-unused'}});
assert.equal(calls.some(p=>p.includes('usage/queue')),false);
const model=await bus.modelPreflight({business:'demo',role:'coder'}),keyId=hash(model.key);
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
let limits=bus.broker.request(human,{verb:'limits'});
bus.broker.request(human,{verb:'limits.set',args:{revision:limits.revision,limits:{tokens:{keys:{[keyId]:{limit:2,reset:'00:00',timeZone:'UTC'}}}}}});
assert.equal((await bus.mayLaunch({business:'demo',instance:'coder',provider:'fixture',model:'fake'})).allow,true);
const dir=join(root,'transcript');mkdirSync(dir);
const {cap}=bus.bindLaunch({business:'demo',role:'coder',run:'p5-run',harness:'pi',modelKeyId:keyId,transcript:dir,pid:process.pid,startTime:startTimeOf(process.pid)});
bus.broker.request(cap,{verb:'role.claim'});
const {id}=bus.broker.request(human,{verb:'message.send',args:{to:'coder',body:'fake work'}});
bus.broker.request(cap,{verb:'message.receive'});
await bus.broker.request(cap,{verb:'usage.begin',args:{turn:id}});
writeFileSync(join(dir,'s.jsonl'),JSON.stringify({type:'message',id:'fake',message:{role:'assistant',usage:{input:2,output:0,cacheWrite:0,cacheRead:0}}})+'\n');
await bus.broker.request(cap,{verb:'usage.end',args:{turn:id}});
const view=bus.broker.request(human,{verb:'usage'});assert.equal(view.byKey[0].modelKeyId,keyId);assert.equal(view.byKey[0].input,2);
assert.equal((await bus.mayLaunch({business:'demo',instance:'coder',provider:'fixture',model:'fake'})).allow,false);
assert.throws(()=>bus.broker.request(cap,{verb:'message.receive'}),{code:'not-holder'});
let plugins=bus.broker.request(human,{verb:'plugins'});
plugins=bus.broker.request(human,{verb:'plugins.set',args:{revision:plugins.revision,plugins:{usage:'cliproxy-queue'},usage:{cliproxyQueue:{pollSeconds:7}}}});
assert.equal(plugins.plugins.usage,'cliproxy-queue');assert.equal(plugins.usage.cliproxyQueue.pollSeconds,7);
const history=readFileSync(join(config,'businesses/.history/demo/changes.jsonl'),'utf8');
for(const value of [model.key,'p5-fake-management'])assert.equal(JSON.stringify([view,history]).includes(value),false);
});
// Row 333 R4 (#1831): the writer the runtime builds keeps a moved quota's since through the runtime's
// own key lineage lookup. The runtime keeps its ModelKeys inside, so the move follows a re-issue: the
// fake proxy stops listing A and the next preflight issues B with A among its earlier ids.
test('R4 real runtime: a quota spent on A and moved to B with no reset still refuses the next launch', async t => {
const root=mkdtempSync(join(tmpdir(),'p5-runtime-')),config=join(root,'config'),data=join(root,'data');
mkdirSync(config,{mode:0o700});mkdirSync(data,{mode:0o700});const rolesDir=rolesCopy(config),doc=businessDoc(config,'demo');
doc.plugins={'usage-source':['transcript'],'limit-check':['daily-quota'],harness:['pi']};
writeJson(join(config,'businesses/demo.json'),doc);mkdirSync(join(config,'secrets'),{recursive:true,mode:0o700});
writeFileSync(join(config,'secrets/proxy-management.key'),'p5-fake-management',{mode:0o600});
new Registry(data);
let keys=[`msk-${hash(config)}-sentinel-${randomBytes(32).toString('base64url')}`];
const server=http.createServer(async(req,res)=>{
if(req.url==='/v8/management/config/access/api-keys') {
if(req.method==='PUT'){let b='';for await(const c of req)b+=c;keys=JSON.parse(b);res.end('{"status":"ok"}');}
else res.end(JSON.stringify(keys));return;
}
if(req.url==='/v8/management/observability/usage/queue?count=100'){res.end('[]');return;}
res.writeHead(keys.includes(req.headers.authorization?.slice(7))?200:401);res.end('{}');
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));let bus;
t.after(async()=>{await bus?.close();await new Promise(r=>server.close(r));rmSync(root,{recursive:true,force:true});});
bus=await startBroker({dataRoot:data,businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
organization:{dir:config,rolesDir,system:systemFor(config)},proxy:{port:server.address().port,binary:'/fixture-unused'}});
const stored=()=>JSON.parse(readFileSync(join(config,'businesses/demo.json'),'utf8')).launch.limits.tokens.keys;
const may=async()=>(await bus.mayLaunch({business:'demo',instance:'coder',provider:'fixture',model:'fake'})).allow;
const a=await bus.modelPreflight({business:'demo',role:'coder'}),A=hash(a.key);
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const leaf={limit:2,reset:'00:00',timeZone:'UTC'};
let limits=bus.broker.request(human,{verb:'limits'});
limits=bus.broker.request(human,{verb:'limits.set',args:{revision:limits.revision,limits:{tokens:{keys:{[A]:{...leaf}}}}}});
const since=stored()[A].since;assert.equal(await may(),true);
// The agent spends the whole allowance on A.
const dir=join(root,'transcript');mkdirSync(dir);
const {cap}=bus.bindLaunch({business:'demo',role:'coder',run:'r4-run',harness:'pi',modelKeyId:A,transcript:dir,pid:process.pid,startTime:startTimeOf(process.pid)});
bus.broker.request(cap,{verb:'role.claim'});
const {id}=bus.broker.request(human,{verb:'message.send',args:{to:'coder',body:'fake work'}});
bus.broker.request(cap,{verb:'message.receive'});
await bus.broker.request(cap,{verb:'usage.begin',args:{turn:id}});
writeFileSync(join(dir,'s.jsonl'),JSON.stringify({type:'message',id:'fake',message:{role:'assistant',usage:{input:2,output:0,cacheWrite:0,cacheRead:0}}})+'\n');
await bus.broker.request(cap,{verb:'usage.end',args:{turn:id}});
assert.equal(await may(),false);
// The proxy loses A, so the next preflight issues B; then the human moves the unchanged quota to B.
keys=keys.filter(k=>k!==a.key);
const b=await bus.modelPreflight({business:'demo',role:'coder'}),B=hash(b.key);assert.notEqual(B,A);
await new Promise(r=>setTimeout(r,20)); // a fresh `now` would fall after the turn's record
limits=bus.broker.request(human,{verb:'limits'});
bus.broker.request(human,{verb:'limits.set',args:{revision:limits.revision,limits:{tokens:{keys:{[B]:{...leaf}}}}}});
assert.deepEqual(stored(),{[B]:{...leaf,since}});
const report=bus.broker.request(human,{verb:'limits'});
assert.equal(JSON.stringify(report).includes(a.key)||JSON.stringify(report).includes(b.key),false);
assert.equal(await may(),false);
});