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.
139 lines
7.7 KiB
JavaScript
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');
|
|
});
|