// The organization selector on `cli.mjs serve` (rename design r2, "Selector scopes and parser // matrix"): --org and --business are one option in serve's scope. serve.mjs and bus.mjs are // stand-ins loaded through a hook, so nothing listens and no bus is called; the bus stand-in // records the organization the Console would bind. import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmp, fixture } from '../../cli/tests/helpers.mjs'; const CLI = new URL('../src/cli.mjs', import.meta.url).pathname; function serve(t, args) { const root = tmp(t, 'sel-'), f = fixture(root), log = join(root, 'bound.jsonl'), hook = join(root, 'hooks.mjs'); writeFileSync(log, ''); const serveSource = "export const DEFAULT_BOARD='http://127.0.0.1:9';export const previewOptions=()=>null;export const proofOptions=()=>null;export async function startServer(){return {address:()=>({port:0}),close:()=>{}};}"; const busSource = `import {appendFileSync} from 'node:fs';export class BusReadError extends Error{};export const busCaller=o=>{appendFileSync(${JSON.stringify(log)},JSON.stringify({business:o.business})+'\\n');return async()=>[];};export const organizationConsole=()=>{throw Error('organizationConsole outside dev-owner')};export const busBusiness=()=>{throw Error('busBusiness outside --sso')};export const userCaller=()=>{throw Error('userCaller outside --sso')};export const consoleCall=()=>{throw Error('consoleCall outside --sso')};`; writeFileSync(hook, `import {registerHooks} from 'node:module';registerHooks({load(url,context,next){ if(url.endsWith('/webui/src/serve.mjs'))return {format:'module',shortCircuit:true,source:${JSON.stringify(serveSource)}}; if(url.endsWith('/webui/src/bus.mjs'))return {format:'module',shortCircuit:true,source:${JSON.stringify(busSource)}}; return next(url,context); }});`); const r = spawnSync(process.execPath, [CLI, 'serve', '--port', '0', ...args], { env: { ...f.env, NODE_OPTIONS: `--import=${hook}` }, encoding: 'utf8', timeout: 10000 }); return { ...r, bound: readFileSync(log, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l).business) }; } const accepted = (r, name) => { assert.equal(r.status, 0, `${name}: ${r.stderr}`); assert.deepEqual(r.bound, ['acme'], name); assert.match(r.stdout, /^Bus: acme$/m, name); }; const refused = (r, pattern, name) => { assert.equal(r.status, 2, `${name}: ${r.stdout}${r.stderr}`); assert.match(r.stderr, pattern, name); assert.deepEqual(r.bound, [], `${name} bound nothing`); }; test('serve takes --org or --business, alone or matching in either order', t => { for (const args of [['--business', 'acme'], ['--org', 'acme'], ['--business', 'acme', '--org', 'acme'], ['--org', 'acme', '--business', 'acme']]) accepted(serve(t, args), args.join(' ')); }); test('serve refuses unequal, repeated, missing, flag-shaped and invalid selectors before any read', t => { for (const [args, pattern] of [ [['--business', 'acme', '--org', 'beta'], /^refused: --business acme and --org beta conflict/], [['--org', 'acme', '--business', 'beta'], /^refused: --org acme and --business beta conflict/], [['--business', 'acme', '--business', 'acme'], /^refused: --business given twice/], [['--org', 'acme', '--org', 'acme'], /^refused: --org given twice/], [['--org'], /^refused: --org needs one value/], [['--business'], /^refused: --business needs one value/], [['--org', ''], /^refused: --org needs one value/], [['--org', '--send'], /^refused: business must be a business id/], [['--business', '-x'], /^refused: business must be a business id/], [['--org', 'Acme'], /^refused: business must be a business id/], [['--org', 'acme/../x'], /^refused: business must be a business id/], ]) refused(serve(t, args), pattern, args.join(' ')); }); test('a selector spelling taken as another option\'s value is that option\'s text', t => { const r = serve(t, ['--board', '--org', '--business', 'acme']); accepted(r, 'board'); assert.match(r.stdout, /^Board: --org$/m); // Without a selector the Console keeps the running host's business. const none = serve(t, []); assert.equal(none.status, 0, none.stderr); assert.deepEqual(none.bound, [null]); });