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.
76 lines
5.7 KiB
JavaScript
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);
|
|
});
|
|
}
|