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.
227 lines
18 KiB
JavaScript
227 lines
18 KiB
JavaScript
// Row 125 (#1619, REQ-LIMIT-3): token limits in the organization file. The schema, the writer's
|
|
// `since` stamp, and the reset, which moves `since` to now and is logged with its actor.
|
|
import test,{after} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFileSync,rmSync} from 'node:fs';
|
|
import {join} from 'node:path';
|
|
import {businessDoc,rolesCopy,tmp as makeRoot,writeJson} from './helpers.mjs';
|
|
import {validateBusinessDocument} from '../src/organization-config.mjs';
|
|
import {launchSettings} from '../src/launch-settings.mjs';
|
|
|
|
const roots=[];after(()=>roots.forEach(root=>rmSync(root,{recursive:true,force:true})));
|
|
const T0='2026-10-18T07:00:00.000Z',T1='2026-10-18T08:00:00.000Z',T2='2026-10-18T09:00:00.000Z';
|
|
// keyLineage, when given, is the writer's lineage lookup (row 333 R4); without it the writer's default refuses.
|
|
function fixture(keyLineage){
|
|
const root=makeRoot();roots.push(root);const rolesDir=rolesCopy(root),doc=businessDoc(root,'acme');
|
|
for(const r of Object.values(doc.roles))r.vars={...r.vars,model:'sonnet'};
|
|
doc.launch={by:'chief',instances:['coder','reviewer'],models:{sonnet:{provider:'anthropic',model:'claude-sonnet-5-5'}},limits:{}};
|
|
const file=join(root,'businesses/acme.json');writeJson(file,doc);
|
|
const system={environment:'development',dataRoot:join(root,'data'),'execution.backend':'docker','execution.provider':'anthropic','execution.model':'sonnet','execution.adapter':'pi'};
|
|
let clock=T0;const settings=launchSettings({dir:root,rolesDir,system,now:()=>clock,...(keyLineage?{keyLineage}:{})});
|
|
return {root,rolesDir,doc,file,settings,tick:at=>{clock=at;}};
|
|
}
|
|
const stored=f=>JSON.parse(readFileSync(f.file,'utf8')).launch.limits.tokens;
|
|
const lastLog=f=>JSON.parse(readFileSync(f.settings.writer.logFile('acme'),'utf8').trim().split('\n').at(-1));
|
|
|
|
test('REQ-LIMIT-3 schema: each subject kind, limit 0 and no ceiling are accepted; bad limits, times, keys and undeclared instances refuse',()=>{
|
|
const f=fixture(),check=tokens=>{const d=structuredClone(f.doc);d.launch.limits={tokens};return validateBusinessDocument(d,f.file,{rolesDir:f.rolesDir}).launch.limits.tokens;};
|
|
const leaf={limit:0,since:T0},big={limit:Number.MAX_SAFE_INTEGER,since:T0};
|
|
const all={instances:{coder:leaf},providers:{anthropic:big},models:{anthropic:{'claude-sonnet-5-5':leaf}},keys:{k1:leaf}};
|
|
assert.deepEqual(check(all),all);
|
|
assert.deepEqual(check({}),{});
|
|
for(const bad of [
|
|
{instances:{coder:{limit:-1,since:T0}}},{instances:{coder:{limit:1.5,since:T0}}},{instances:{coder:{limit:'5',since:T0}}},
|
|
{instances:{coder:{limit:5}}},{instances:{coder:{limit:5,since:'2026-10-18'}}},{instances:{coder:{limit:5,since:'2026-10-18T07:00:00Z'}}},
|
|
{instances:{coder:{limit:5,since:'2026-02-30T07:00:00.000Z'}}},{instances:{coder:{limit:5,since:T0,extra:1}}},
|
|
{accounts:{}},{models:{anthropic:{limit:5,since:T0}}},{instances:[]},{instances:{ghost:{limit:5,since:T0}}},
|
|
])assert.throws(()=>check(bad),JSON.stringify(bad));
|
|
});
|
|
|
|
test('REQ-LIMIT-3 the writer stamps since: a new limit gets now, a changed one keeps its time, a different time is refused',()=>{
|
|
const f=fixture();
|
|
let view=f.settings.read('acme');
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{instances:{coder:{limit:100}}}}},'jason');
|
|
assert.deepEqual(stored(f),{instances:{coder:{limit:100,since:T0}}});
|
|
// A changed limit keeps its since, and a new one beside it gets the new now.
|
|
f.tick(T1);
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{instances:{coder:{limit:50}},providers:{anthropic:{limit:9}}}}},'jason');
|
|
assert.deepEqual(stored(f),{instances:{coder:{limit:50,since:T0}},providers:{anthropic:{limit:9,since:T1}}});
|
|
// A since equal to the stored one is accepted; any other is refused and changes nothing.
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{instances:{coder:{limit:50,since:T0}},providers:{anthropic:{limit:9}}}}},'jason');
|
|
const bytes=readFileSync(f.file,'utf8');
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,limits:{tokens:{instances:{coder:{limit:50,since:T2}}}}},'jason'),{code:'invalid-request'});
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{k1:{limit:1,since:T2}}}}},'jason'),{code:'invalid-request'});
|
|
assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
// A limit removed and set again starts from now.
|
|
f.tick(T2);
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{providers:{anthropic:{limit:9}}}}},'jason');
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{instances:{coder:{limit:50}},providers:{anthropic:{limit:9}}}}},'jason');
|
|
assert.equal(stored(f).instances.coder.since,T2);
|
|
});
|
|
|
|
test('REQ-LIMIT-3 only tokens.* at schema depth holds token limits: agent limits named `limit` save as sent, and a token limit named `limit` is stamped and reset',()=>{
|
|
const f=fixture(),full=()=>JSON.parse(readFileSync(f.file,'utf8')).launch.limits;
|
|
let view=f.settings.read('acme');
|
|
// Agent (concurrency) limits that 19fe85c3e saves (Filbert, row 125 r1 change 1).
|
|
for(const limits of [{providers:{anthropic:3}},{providers:{limit:3}},{models:{limit:{m:2}}},{models:{anthropic:{limit:2}}}]){
|
|
view=f.settings.set('acme',{revision:view.revision,limits},'jason');
|
|
assert.deepEqual(full(),limits,JSON.stringify(limits));
|
|
}
|
|
// Token limits whose provider, model or key is named `limit`, beside agent limits named the same.
|
|
const agents={providers:{limit:3},models:{limit:{limit:2}}};
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{...agents,tokens:{providers:{limit:{limit:5}},models:{limit:{limit:{limit:6}}},keys:{limit:{limit:7}}}}},'jason');
|
|
assert.deepEqual(full(),{...agents,tokens:{providers:{limit:{limit:5,since:T0}},models:{limit:{limit:{limit:6,since:T0}}},keys:{limit:{limit:7,since:T0}}}});
|
|
f.tick(T1);
|
|
view=f.settings.set('acme',{revision:view.revision,reset:[['tokens','providers','limit'],['tokens','models','limit','limit']]},'chief');
|
|
assert.deepEqual(full(),{...agents,tokens:{providers:{limit:{limit:5,since:T1}},models:{limit:{limit:{limit:6,since:T1}}},keys:{limit:{limit:7,since:T0}}}});
|
|
// An agent limit, or a path above a token limit, is never reset.
|
|
const bytes=readFileSync(f.file,'utf8');
|
|
for(const reset of [[['providers','limit']],[['models','limit','limit']],[['tokens','models','limit']],[['tokens','providers']]])
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,reset},'jason'),{code:'invalid-request'},JSON.stringify(reset));
|
|
assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
});
|
|
|
|
test('REQ-LIMIT-3 a reset moves since to now, is logged with its actor, and refuses a path that is not a limit',()=>{
|
|
const f=fixture();
|
|
let view=f.settings.read('acme');
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{instances:{coder:{limit:100}},models:{anthropic:{'claude-sonnet-5-5':{limit:7}}}}}},'jason');
|
|
f.tick(T1);
|
|
view=f.settings.set('acme',{revision:view.revision,reset:[['tokens','models','anthropic','claude-sonnet-5-5']]},'chief');
|
|
assert.deepEqual(stored(f),{instances:{coder:{limit:100,since:T0}},models:{anthropic:{'claude-sonnet-5-5':{limit:7,since:T1}}}});
|
|
const log=lastLog(f);
|
|
assert.equal(log.actor,'chief');
|
|
assert.ok(log.changes.some(c=>c.path==='launch.limits.tokens.models.anthropic.claude-sonnet-5-5.since'&&c.old===T0&&c.new===T1),JSON.stringify(log.changes));
|
|
// A user reset of two limits at once, both to the same now.
|
|
f.tick(T2);
|
|
view=f.settings.set('acme',{revision:view.revision,reset:[['tokens','instances','coder'],['tokens','models','anthropic','claude-sonnet-5-5']]},'jason');
|
|
assert.equal(stored(f).instances.coder.since,T2);
|
|
assert.equal(stored(f).models.anthropic['claude-sonnet-5-5'].since,T2);
|
|
assert.equal(lastLog(f).actor,'jason');
|
|
const bytes=readFileSync(f.file,'utf8');
|
|
for(const reset of [[],[[]],[['tokens','instances']],[['tokens','instances','ghost']],[['tokens','instances',1]],'tokens',[['tokens','instances','coder','limit']]])
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,reset},'jason'),{code:'invalid-request'},JSON.stringify(reset));
|
|
assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
});
|
|
|
|
// Row 126 (#1620, REQ-LIMIT-2): launch.account and tokens.daily.<account>.
|
|
const ACCOUNT={name:'max',credential:'CLAUDE_CODE_OAUTH_TOKEN'};
|
|
const DAILY={limit:40,since:T0,reset:'05:00',timeZone:'America/Chicago'};
|
|
test('REQ-LIMIT-2 schema: launch.account names a credential set; a daily quota needs a reset and an IANA zone and names the account',()=>{
|
|
const f=fixture(),read=(launch)=>{const d=structuredClone(f.doc);Object.assign(d.launch,launch);return validateBusinessDocument(d,f.file,{rolesDir:f.rolesDir}).launch;};
|
|
// No account: the launch reads as before, with no account key.
|
|
assert.equal(Object.hasOwn(read({}),'account'),false);
|
|
for(const credential of ['CLAUDE_CODE_OAUTH_TOKEN','ANTHROPIC_API_KEY','ANTHROPIC_AUTH_TOKEN+ANTHROPIC_BASE_URL'])
|
|
assert.deepEqual(read({account:{name:'max',credential}}).account,{name:'max',credential});
|
|
const ok=read({account:ACCOUNT,limits:{tokens:{daily:{max:DAILY}}}});
|
|
assert.deepEqual(ok.limits.tokens.daily,{max:DAILY});
|
|
for(const timeZone of ['UTC','Etc/GMT+5','America/Argentina/Buenos_Aires'])
|
|
assert.equal(read({account:ACCOUNT,limits:{tokens:{daily:{max:{...DAILY,timeZone}}}}}).limits.tokens.daily.max.timeZone,timeZone);
|
|
const bad=[
|
|
{account:{name:'max'}},{account:{credential:'ANTHROPIC_API_KEY'}},{account:{name:'Max Account',credential:'ANTHROPIC_API_KEY'}},
|
|
{account:{name:'max',credential:'anthropic_api_key'}},{account:{name:'max',credential:'sk-ant-api03-abc'}},
|
|
{account:{name:'max',credential:'ANTHROPIC_API_KEY+'}},{account:{name:'max',credential:'ANTHROPIC_API_KEY',key:'x'}},{account:'max'},
|
|
];
|
|
for(const reset of ['5:00','24:00','05:60','05:00:00','',undefined,500])bad.push({account:ACCOUNT,limits:{tokens:{daily:{max:{...DAILY,reset}}}}});
|
|
for(const timeZone of ['Chicago time','+05:00','-0600','Mars/Phobos','America/Chicago ','',undefined,5])bad.push({account:ACCOUNT,limits:{tokens:{daily:{max:{...DAILY,timeZone}}}}});
|
|
bad.push({account:ACCOUNT,limits:{tokens:{daily:{max:{...DAILY,limit:-1}}}}},{account:ACCOUNT,limits:{tokens:{daily:{max:{...DAILY,since:'2026-10-18'}}}}},
|
|
{account:ACCOUNT,limits:{tokens:{daily:{max:{...DAILY,extra:1}}}}},{account:ACCOUNT,limits:{tokens:{daily:{max:{limit:40,since:T0}}}}});
|
|
// A daily quota for another account, or with no account at all, would count nothing.
|
|
bad.push({account:ACCOUNT,limits:{tokens:{daily:{other:DAILY}}}},{limits:{tokens:{daily:{max:DAILY}}}});
|
|
for(const launch of bad)assert.throws(()=>read(launch),JSON.stringify(launch));
|
|
// The credential check's refusal names the field, never the value given.
|
|
assert.throws(()=>read({account:{name:'max',credential:'sk-ant-api03-abc'}}),e=>/launch\.account\.credential/.test(e.message)&&!e.message.includes('sk-ant'));
|
|
assert.throws(()=>read({account:ACCOUNT,limits:{tokens:{daily:{other:DAILY}}}}),/daily names other/);
|
|
});
|
|
|
|
test('REQ-LIMIT-2 the writer stamps a daily quota\'s since, keeps its reset and zone, and a reset moves since',()=>{
|
|
const f=fixture();
|
|
f.doc.launch.account=ACCOUNT;writeJson(f.file,f.doc);
|
|
let view=f.settings.read('acme');
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{daily:{max:{limit:40,reset:'05:00',timeZone:'America/Chicago'}}}}},'jason');
|
|
assert.deepEqual(stored(f),{daily:{max:{limit:40,reset:'05:00',timeZone:'America/Chicago',since:T0}}});
|
|
f.tick(T1);
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{daily:{max:{limit:30,reset:'06:00',timeZone:'UTC'}}}}},'jason');
|
|
assert.deepEqual(stored(f),{daily:{max:{limit:30,reset:'06:00',timeZone:'UTC',since:T0}}});
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,limits:{tokens:{daily:{max:{limit:30,reset:'06:00',timeZone:'UTC',since:T2}}}}},'jason'),{code:'invalid-request'});
|
|
f.tick(T2);
|
|
view=f.settings.set('acme',{revision:view.revision,reset:[['tokens','daily','max']]},'chief');
|
|
assert.equal(stored(f).daily.max.since,T2);
|
|
assert.equal(lastLog(f).actor,'chief');
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,reset:[['tokens','daily']]},'jason'),{code:'invalid-request'});
|
|
// The account itself stays a hand edit: the file's account is untouched by the writes.
|
|
assert.deepEqual(JSON.parse(readFileSync(f.file,'utf8')).launch.account,ACCOUNT);
|
|
});
|
|
|
|
test('P5 per-model-key quotas are unset by default, accept digit-leading IDs and only the human changes or resets them', () => {
|
|
const f=fixture(()=>[]),key='012345abcdef'; let view=f.settings.read('acme');
|
|
assert.equal(view.limits.tokens,undefined);
|
|
const limits={tokens:{keys:{[key]:{limit:20,reset:'05:00',timeZone:'America/Chicago'}}}};
|
|
view=f.settings.set('acme',{revision:view.revision,limits},'jason');
|
|
assert.deepEqual(stored(f).keys[key],{...limits.tokens.keys[key],since:T0});
|
|
const bytes=readFileSync(f.file,'utf8');f.tick(T1);
|
|
for(const actor of ['chief','tester','coder']) {
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{}}}},actor),{code:'organization-model-quota-human-only'});
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,reset:[['tokens','keys',key]]},actor),{code:'organization-model-quota-human-only'});
|
|
}
|
|
assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
f.tick(T1);view=f.settings.set('acme',{revision:view.revision,reset:[['tokens','keys',key]]},'jason');
|
|
assert.equal(stored(f).keys[key].since,T1);
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{}},'jason');
|
|
assert.equal(view.limits.tokens,undefined);
|
|
});
|
|
|
|
// Row 333 R4 (#1831): a new daily key quota takes the earliest since of the old document's daily key
|
|
// quotas in its key's lineage. A, B and C are one agent's keys, oldest first; X is no agent's.
|
|
const A='aaaaaaaaaaa1',B='bbbbbbbbbbb2',C='ccccccccccc3',X='ddddddddddd4',T3='2026-10-18T10:00:00.000Z';
|
|
const daily=(limit=20)=>({limit,reset:'05:00',timeZone:'America/Chicago'});
|
|
const lineageOf=(business,id)=>{assert.equal(business,'acme');return [C,B,A].includes(id)?[C,B,A]:[];};
|
|
test('R4 a moved daily key quota keeps the earliest since in its lineage; a reset, a new key and a re-add get now; a caller time refuses',()=>{
|
|
const f=fixture(lineageOf);let view=f.settings.read('acme');
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[A]:daily()}}}},'jason');
|
|
assert.equal(stored(f).keys[A].since,T0);
|
|
// B joins while A stays: it carries A's time. Then B alone is reset, so A and B differ.
|
|
f.tick(T1);view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[A]:daily(),[B]:daily(30)}}}},'jason');
|
|
assert.equal(stored(f).keys[B].since,T0);
|
|
view=f.settings.set('acme',{revision:view.revision,reset:[['tokens','keys',B]]},'jason');
|
|
assert.deepEqual([stored(f).keys[A].since,stored(f).keys[B].since],[T0,T1]);
|
|
// The move to C drops both: C takes the earlier of the two, not the later. X matches no agent: now.
|
|
f.tick(T2);view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[C]:daily(),[X]:daily()}}}},'jason');
|
|
assert.deepEqual(stored(f).keys,{[C]:{...daily(),since:T0},[X]:{...daily(),since:T2}});
|
|
// A caller-supplied since refuses, on a moved leaf too, even when it is the time the writer would carry.
|
|
const bytes=readFileSync(f.file,'utf8');
|
|
for(const keys of [{[C]:daily(),[X]:daily(),[B]:{...daily(),since:T0}},{[C]:daily(),[X]:daily(),[A]:{...daily(),since:T2}},{[C]:{...daily(),since:T1},[X]:daily()}])
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys}}},'jason'),{code:'invalid-request'},JSON.stringify(keys));
|
|
assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
// An explicit reset after the move sets now, in the same write as a move or in its own.
|
|
f.tick(T3);view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[B]:daily(),[X]:daily()}}},reset:[['tokens','keys',B]]},'jason');
|
|
assert.equal(stored(f).keys[B].since,T3);
|
|
// Removed in one write and added back in a later one: a new quota, so now.
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[X]:daily()}}}},'jason');
|
|
f.tick('2026-10-18T11:00:00.000Z');view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[X]:daily(),[A]:daily()}}}},'jason');
|
|
assert.equal(stored(f).keys[A].since,'2026-10-18T11:00:00.000Z');
|
|
});
|
|
|
|
test('R4 the writer asks the lineage only for a new daily key quota, and a lookup that fails or answers malformed refuses',()=>{
|
|
let asked=[];const answer={value:null};
|
|
const f=fixture((business,id)=>{asked.push(id);if(answer.value instanceof Error)throw answer.value;return answer.value??lineageOf(business,id);});
|
|
let view=f.settings.read('acme');
|
|
// Writes adding no new daily key quota never ask: account limits, a hex-named legacy limit, a changed quota.
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{instances:{coder:{limit:5}},keys:{k1:{limit:5},[X]:{limit:5}}}}},'jason');
|
|
assert.deepEqual(asked,[]);
|
|
view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[A]:daily()}}}},'jason');
|
|
assert.deepEqual(asked,[A]);
|
|
f.tick(T1);view=f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[A]:daily(99)}}}},'jason');
|
|
view=f.settings.set('acme',{revision:view.revision,reset:[['tokens','keys',A]]},'jason');
|
|
assert.deepEqual(asked,[A]);
|
|
const bytes=readFileSync(f.file,'utf8');
|
|
for(const bad of [Object.assign(new Error('records unreadable'),{code:'credential-format'}),new Error('plain'),'nope',[42],['ABCDEFABCDEF'],[A],{0:B}]){
|
|
answer.value=bad;
|
|
assert.throws(()=>f.settings.set('acme',{revision:view.revision,limits:{tokens:{keys:{[A]:daily(99),[B]:daily()}}}},'jason'),{code:'proxy-key-refused'},String(bad));
|
|
}
|
|
assert.equal(readFileSync(f.file,'utf8'),bytes);
|
|
// Without a lookup, the default refuses a new daily key quota and nothing else.
|
|
const g=fixture();let gv=g.settings.read('acme');
|
|
gv=g.settings.set('acme',{revision:gv.revision,limits:{tokens:{instances:{coder:{limit:5}},keys:{[X]:{limit:5}}}}},'jason');
|
|
assert.throws(()=>g.settings.set('acme',{revision:gv.revision,limits:{tokens:{keys:{[X]:{limit:5},[A]:daily()}}}},'jason'),{code:'proxy-key-refused'});
|
|
});
|