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.
111 lines
8.8 KiB
JavaScript
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);
|
|
});
|