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()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); }); }