Files
stack/packages/webui/tests/org-selector-cli.test.mjs
jason.woltje 259302a368 selector: --org and --business in the Console CLI and service parsers
cli.mjs serve and webui-service.sh each take --org as --business in their own
scope through the shared selector rules; the service unit still carries the
canonical --business. A flag-shaped or invalid value keeps each script's old
wording and exit code; a repeated or conflicting selector is a usage error.
2026-11-08 21:20:09 -06:00

68 lines
4.3 KiB
JavaScript

// 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]);
});