Files
stack/packages/bus/tests/dev-local-owner.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

76 lines
5.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { once } from 'node:events';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DEV_OWNER } from '../../cli/src/dev-local-owner.mjs';
import { fixture, notifyConfig } from '../../cli/tests/helpers.mjs';
import { initializeControl, transition } from '../../processing-control/src/state.mjs';
import { verifyPrincipal } from '../src/principal.mjs';
import { checkConsole } from '../../webui/src/sso-config.mjs';
import { reserveOrg, activateOrg } from '../../organization/src/org-registry.mjs';
const uid=process.getuid(), nonce='a'.repeat(64);
const paths={ console:'/tree/cli.mjs', consoleCli:'/tree/console-cli.mjs' };
function context(subject=DEV_OWNER, owners=[]) {
const table=new Map([[50,{pid:50,ppid:40,startTime:'100',uid,argv:['node',paths.consoleCli],env:{MOSAIC_BUS_CONSOLE_NONCE:nonce,MOSAIC_BUS_CONSOLE_BUSINESS:'demo',MOSAIC_BUS_CONSOLE_PROVIDER:subject.provider,MOSAIC_BUS_CONSOLE_ISS:subject.iss,MOSAIC_BUS_CONSOLE_SUB:subject.sub}}], [40,{pid:40,ppid:1,startTime:'90',uid,argv:['node',paths.console,'serve'],env:{}}]]);
return {peer:{pid:50,uid,gid:uid},paths,businesses:{demo:{human:'owner'}},consoleOwners:owners,readProcess:pid=>({...table.get(pid)}),table};
}
const proof = subject => ({console:{pid:50,startTime:'100',nonce,business:'demo',...subject}});
test('dev principal requires enabled owner membership and the Console wrapper, nonce and serve parent',()=>{
assert.throws(()=>verifyPrincipal(proof(DEV_OWNER),context()),/human-required/);
const c=context(DEV_OWNER,[DEV_OWNER]);
assert.deepEqual(verifyPrincipal(proof(DEV_OWNER),c).subject,DEV_OWNER);
for(const mutation of [t=>{t.get(50).argv=['node','/other'];},t=>{t.get(40).argv=['node',paths.console,'other'];},t=>{t.get(50).env.MOSAIC_BUS_CONSOLE_NONCE='bad';}]) {
const d=context(DEV_OWNER,[DEV_OWNER]); mutation(d.table); assert.throws(()=>verifyPrincipal(proof(DEV_OWNER),d),/human-required/);
}
const forged={...DEV_OWNER,sub:'another'};
assert.throws(()=>verifyPrincipal(proof(forged),context(forged,[DEV_OWNER])),/human-required/);
});
test('dev issuer cannot be a configured SSO issuer',()=>{
const sso={sso:{publicOrigin:'https://console.example',providers:[{id:DEV_OWNER.provider,issuer:DEV_OWNER.iss,clientId:'fake',clientSecret:{slot:'fake'},algs:['RS256'],subjects:[{sub:DEV_OWNER.sub,name:'operator'}]}]}};
assert.throws(()=>checkConsole(sso),/issuer.*https URL/);
});
test('bus dev flag refuses the default instance with or without --pm before config access',()=>{
for(const args of [['bus','start','demo','--dev-local-owner'], ['bus','start','demo','--dev-local-owner','--pm']]) {
const r=spawnSync(process.execPath,['packages/cli/src/cli.mjs',...args],{encoding:'utf8',env:{...process.env,MOSAIC_CONFIG:''}});
assert.notEqual(r.status,0); assert.match(r.stderr,/--dev-local-owner requires a named instance/);
}
});
for (const pm of [false, true]) {
test(`bus dev owner enables launches ${pm ? 'with --pm and prints the warning' : 'without --pm'}`, { timeout: 30000 }, async t => {
const root=mkdtempSync(join(tmpdir(),'dev-owner-launch-'));
const f=fixture(root), home=join(root,'home'), base=join(home,'.config');
mkdirSync(base,{recursive:true}); renameSync(join(root,'config'),join(base,'mosaic-test'));
chmodSync(join(base,'mosaic-test'),0o700);
writeFileSync(join(f.dataRoot,'instance.json'),JSON.stringify({instanceVersion:1,name:'test'}));
const ctx={ configDir:join(base,'mosaic-test'), dataRoot:f.dataRoot, instance:'test' };
const org=await reserveOrg(ctx,'acme'); await activateOrg(ctx,'acme');
notifyConfig(org.root,'acme',null);
// A halted instance exercises the real --pm path without launching a harness.
initializeControl(org.root);
transition(org.root,{business:'acme',human:'jason',halted:true});
const env={...f.env,HOME:home,MOSAIC_CONFIG:join(base,'mosaic-test','config.json')};
const child=spawn(process.execPath,['packages/cli/src/cli.mjs','bus','start','acme','--dev-local-owner',...(pm ? ['--pm'] : [])],{env,stdio:['ignore','pipe','pipe']});
const exited=once(child,'exit'); let stdout='', stderr='';
child.stdout.on('data',b=>stdout+=b); child.stderr.on('data',b=>stderr+=b);
t.after(async()=>{if(child.exitCode===null && child.signalCode===null){const stop=spawn(process.execPath,['packages/cli/src/cli.mjs','bus','stop','acme'],{env,stdio:'ignore'}); const [[code]]=await Promise.all([once(stop,'exit'),exited]); assert.equal(code,0,'bus stops cleanly');} rmSync(root,{recursive:true,force:true});});
const log=join(org.root,'bus-host','host.log');
const hostOutput=()=>existsSync(log)?readFileSync(log,'utf8'):'';
const deadline=Date.now()+15000;
while(Date.now()<deadline && child.exitCode===null && !(pm ? hostOutput().includes('processing halted: chief launch skipped') : hostOutput().includes('bus host up:'))) await new Promise(r=>setTimeout(r,50));
stdout=hostOutput();
assert.match(stdout,/DEV LOCAL OWNER MODE: console owner local-dev accepted \(named instance test\)\n/,stderr);
const warning='DEV LOCAL OWNER MODE: launched agents run as this OS user and can reach the loopback Console as owner; use this instance for UI testing only';
if(pm) {
assert.ok(stdout.includes(`DEV LOCAL OWNER MODE: console owner local-dev accepted (named instance test)\n${warning}\n`),stdout);
assert.match(stdout,/processing halted: chief launch skipped/);
} else assert.ok(!stdout.includes(warning));
assert.match(stdout,/bus host up: business acme, socket \S+, notifier off\n/);
assert.equal(child.exitCode,null,stderr);
});
}