Files
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

139 lines
7.7 KiB
JavaScript

// The organization selector helpers: one table, run against packages/business/src/selector.mjs and
// scripts/selector.sh through the same small parser, so the two halves keep one set of rules.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
import { REPO } from './helpers.mjs';
import { ID_PATTERN } from '../src/vocabulary.mjs';
import { SELECTOR_FAMILIES, isSelector, selectorScope } from '../src/selector.mjs';
// A parser with one other value option (--note), positional text and a `--` separator.
function viaJs(family, args) {
const scope = selectorScope({ family, valid: family === 'id' ? id => ID_PATTERN.test(id) : null, refuse: kind => { throw Object.assign(new Error(kind), { kind }); } });
const out = { value: null, note: null, positional: [] };
try {
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (scope.owns(a)) { scope.take(a, args[i + 1]); i++; }
else if (a === '--note') out.note = args[++i] ?? null;
else if (a === '--') { out.positional.push(...args.slice(i + 1)); break; }
else out.positional.push(a);
}
} catch (e) { if (e.kind) return { refused: e.kind }; throw e; }
out.value = scope.value ?? null;
return out;
}
const DRIVER = `
set -euo pipefail
. "$1"; family="$2"; shift 2
refuse() { printf 'refused %s\\n' "$1"; exit 3; }
valid() { [[ "$1" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]]; }
v=-; [ "$family" = id ] && v=valid
note=""; has_note=0; pos=()
while [ $# -gt 0 ]; do
case "$family:$1" in
id:--business|id:--org|file:--business-file|file:--org-file) selector_take s "$family" refuse "$v" "$1" "\${@:2:1}"; shift 2 ;;
*:--note) has_note=1; note="\${2-}"; shift; [ $# -gt 0 ] && shift ;;
*:--) shift; pos+=("$@"); break ;;
*) pos+=("$1"); shift ;;
esac
done
printf 'value %s\\n' "$(selector_value s)"
[ "$has_note" = 1 ] && printf 'note %s\\n' "$note"
for p in "\${pos[@]+"\${pos[@]}"}"; do printf 'pos %s\\n' "$p"; done
`;
function viaShell(family, args) {
const r = spawnSync('bash', ['-c', DRIVER, 'driver', join(REPO, 'scripts/selector.sh'), family, ...args], { encoding: 'utf8' });
if (r.status === 3) return { refused: /^refused (\S+)$/.exec(r.stdout.trim())[1] };
assert.equal(r.status, 0, r.stderr);
const out = { value: null, note: null, positional: [] };
for (const line of r.stdout.split('\n').filter(Boolean)) {
const [, key, rest] = /^(\S+) ?(.*)$/.exec(line);
if (key === 'value') out.value = rest || null;
else if (key === 'note') out.note = rest;
else out.positional.push(rest);
}
return out;
}
const ok = (value, more = {}) => ({ value, note: null, positional: [], ...more });
const ID_TABLE = [
['old alone', ['--business', 'acme'], ok('acme')],
['new alone', ['--org', 'acme'], ok('acme')],
['matching, old first', ['--business', 'acme', '--org', 'acme'], ok('acme')],
['matching, new first', ['--org', 'acme', '--business', 'acme'], ok('acme')],
['unequal, old first', ['--business', 'acme', '--org', 'beta'], { refused: 'differ' }],
['unequal, new first', ['--org', 'acme', '--business', 'beta'], { refused: 'differ' }],
['old repeated', ['--business', 'acme', '--business', 'acme'], { refused: 'repeated' }],
['new repeated', ['--org', 'acme', '--org', 'acme'], { refused: 'repeated' }],
['new repeated after a match', ['--org', 'acme', '--business', 'acme', '--org', 'acme'], { refused: 'repeated' }],
['old missing', ['--business'], { refused: 'missing' }],
['new missing', ['--org'], { refused: 'missing' }],
['new empty', ['--org', ''], { refused: 'missing' }],
['old flag-shaped', ['--business', '--org', 'acme'], { refused: 'flag' }],
['new flag-shaped', ['--org', '-x'], { refused: 'flag' }],
['old invalid', ['--business', 'Acme'], { refused: 'invalid' }],
['new invalid', ['--org', 'acme/../x'], { refused: 'invalid' }],
['equal but invalid', ['--org', 'A', '--business', 'A'], { refused: 'invalid' }],
['option-like value of another option', ['--note', '--org', '--business', 'acme'], ok('acme', { note: '--org' })],
['option-like value, new spelling', ['--note', '--business', '--org', 'acme'], ok('acme', { note: '--business' })],
['text after the separator', ['--org', 'acme', '--', '--business', 'beta', '--org'], ok('acme', { positional: ['--business', 'beta', '--org'] })],
['neither', ['word'], ok(null, { positional: ['word'] })],
];
const FILE_TABLE = [
['old alone', ['--business-file', 'a.json'], ok('a.json')],
['new alone', ['--org-file', 'a.json'], ok('a.json')],
['identical, old first', ['--business-file', 'a.json', '--org-file', 'a.json'], ok('a.json')],
['identical, new first', ['--org-file', 'a.json', '--business-file', 'a.json'], ok('a.json')],
['different paths, old first', ['--business-file', 'a.json', '--org-file', 'b.json'], { refused: 'differ' }],
['different paths, new first', ['--org-file', 'a.json', '--business-file', 'b.json'], { refused: 'differ' }],
// Strings are compared as given, so two spellings of one path differ.
['same file, other spelling', ['--org-file', 'a.json', '--business-file', './a.json'], { refused: 'differ' }],
['new repeated', ['--org-file', 'a.json', '--org-file', 'a.json'], { refused: 'repeated' }],
['old repeated', ['--business-file', 'a.json', '--business-file', 'a.json'], { refused: 'repeated' }],
['new missing', ['--org-file'], { refused: 'missing' }],
['new flag-shaped', ['--org-file', '--business-file', 'a.json'], { refused: 'flag' }],
['an id spelling is data here', ['--org', 'x', '--org-file', 'a.json'], ok('a.json', { positional: ['--org', 'x'] })],
];
for (const [family, table] of [['id', ID_TABLE], ['file', FILE_TABLE]]) {
for (const [name, args, expected] of table) {
test(`${family} selector: ${name}`, () => {
assert.deepEqual(viaJs(family, args), expected, 'selector.mjs');
assert.deepEqual(viaShell(family, args), expected, 'selector.sh');
});
}
}
test('selector families and spellings are fixed', () => {
assert.deepEqual(SELECTOR_FAMILIES.id.spellings, ['--business', '--org']);
assert.deepEqual(SELECTOR_FAMILIES.file.spellings, ['--business-file', '--org-file']);
assert.equal(isSelector('--org'), true);
assert.equal(isSelector('--org-file'), false);
assert.equal(isSelector('--org-file', 'file'), true);
assert.equal(isSelector('--organization'), false);
});
test('a refuse function that returns still refuses, and scopes are separate', () => {
const quiet = selectorScope({ refuse: () => {} });
assert.throws(() => quiet.take('--org', ''), /selector refusal returned/);
assert.throws(() => quiet.take('--org-file', 'x'), TypeError);
const refuse = kind => { throw new Error(kind); };
const outer = selectorScope({ refuse }), child = selectorScope({ refuse });
outer.take('--org', 'acme'); child.take('--org', 'beta');
assert.deepEqual([outer.value, child.value, outer.given], ['acme', 'beta', true]);
assert.equal(selectorScope({ refuse }).given, false);
});
test('the shell helper refuses a misuse and keeps scopes separate', () => {
const run = body => spawnSync('bash', ['-c', `set -u; . "$1"; refuse() { echo "refused $1"; exit 3; }; quiet() { :; }; ${body}`, 'x', join(REPO, 'scripts/selector.sh')], { encoding: 'utf8' });
assert.equal(run('selector_take s id refuse - --org-file a').status, 70);
assert.equal(run('selector_take bad-name id refuse - --org a').status, 70);
assert.equal(run('selector_take s id quiet - --org ""').status, 70);
const two = run('selector_take outer id refuse - --org acme; selector_take child id refuse - --business beta; echo "$(selector_value outer) $(selector_value child) [$(selector_value none)]"');
assert.equal(two.status, 0, two.stderr);
assert.equal(two.stdout, 'acme beta []\n');
});