organization: keep the old stderr prefix on config diagnostics in both spellings
Sol's stage B review (P2): mosaic org config validate|resolve printed 'mosaic org config: ' on refusals and warnings, so a caller moving from mosaic business validate|resolve lost the byte-level diagnostic contract in rename design r2. Now only the usage line follows the spelling; every other stderr line keeps 'mosaic business: '. The B2 test compares a refusal, a warning and a system config problem unnormalized; it fails 4 of 9 on the old source.
This commit is contained in:
1 parent
ee7dbd2332
commit
e19db4009c
5 files changed
+36
-22
No files matched your search
+4
-4
@@ -263,10 +263,10 @@ scripts/mosaic org instance set <org> <instance> [--definition <def>] [--vars <f
|
||||
|
||||
`scripts/mosaic business validate|resolve|instances|instance|veto` is the old
|
||||
spelling of these commands, with the same arguments, and it still works. Both
|
||||
spellings run the same code and give the same status and stdout. The
|
||||
validate/resolve stderr lines start with the spelling you used
|
||||
(`mosaic org config: ` or `mosaic business: `), and a usage error prints that
|
||||
spelling's usage. `scripts/mosaic org validate FILE` is a different command: it
|
||||
spellings run the same code and give the same status, stdout and stderr.
|
||||
Only a validate/resolve usage error differs: it prints the usage of the
|
||||
spelling you used. Every other validate/resolve stderr line, refusals and
|
||||
warnings included, starts `mosaic business: ` in both spellings. `scripts/mosaic org validate FILE` is a different command: it
|
||||
checks a draft file by path and prints `valid`.
|
||||
|
||||
Reads `businesses/<business>.json` next to the system config
|
||||
|
||||
@@ -688,8 +688,9 @@ async function app(io, deps) {
|
||||
io.stdout.write(`${quitLine(result)}\n`);
|
||||
}
|
||||
|
||||
// `mosaic org config validate|resolve`: the service writes its own stdout and stderr lines, prefixed
|
||||
// `mosaic org config: `, and returns the exit code; run() turns a CliExit into that code.
|
||||
// `mosaic org config validate|resolve`: the service writes its own stdout and stderr lines (the
|
||||
// usage line names `mosaic org config`; every other line keeps `mosaic business: `) and returns
|
||||
// the exit code; run() turns a CliExit into that code.
|
||||
function orgConfig(args, io) {
|
||||
const code = runConfigCommand(args, { stdout: io.stdout, stderr: io.stderr, env: io.env, spelling: ORG_CONFIG });
|
||||
if (code !== 0) throw new CliExit(code);
|
||||
|
||||
@@ -34,8 +34,10 @@ const OLD_CLI = path("../../business/src/cli.mjs");
|
||||
const CONFIG_COMMAND = path("../../organization/src/config-command.mjs");
|
||||
const golden = (name) => readFileSync(path(`../../business/tests/fixtures/rename-a/${name}`), "utf8");
|
||||
|
||||
// The old spelling's stderr as the new spelling prints it: each line's prefix and the usage text.
|
||||
const asOrgConfig = (stderr) => stderr.replaceAll(BUSINESS_CONFIG.usage, ORG_CONFIG.usage).replace(/^mosaic business: /gm, "mosaic org config: ");
|
||||
// The old spelling's stderr as the new spelling prints it: only a usage line changes. Every other
|
||||
// line, refusals and warnings included, is byte for byte the same.
|
||||
const OLD_USAGE_LINE = `mosaic business: ${BUSINESS_CONFIG.usage}`;
|
||||
const asOrgConfig = (stderr) => stderr.replaceAll(OLD_USAGE_LINE, `mosaic org config: ${ORG_CONFIG.usage}`);
|
||||
|
||||
const spawn = (command, args, env, cwd) => {
|
||||
const p = spawnSync(command, args, { env, cwd, encoding: "utf8" });
|
||||
@@ -108,7 +110,8 @@ test("rename r2 B2: org config validate|resolve has the status, stdout and stder
|
||||
test("rename r2 B2: a credential problem, an expiry warning and a system config problem read the same in both spellings", async (t) => {
|
||||
const bad = installed(t, ({ root }) => chmodSync(join(root, "secrets", "coder-gitea.token"), 0o644));
|
||||
for (const args of [["validate", "acme"], ["resolve", "acme", "coder"]]) {
|
||||
const { old } = await sameAcrossSpellings(args, bad.env, bad.root, 2);
|
||||
const { old, now } = await sameAcrossSpellings(args, bad.env, bad.root, 2);
|
||||
assert.equal(now.stderr, old.stderr, "a refusal is byte for byte the same, unnormalized");
|
||||
assert.match(old.stderr, /^mosaic business: .*coder-gitea\.token.*mode 644/m);
|
||||
assert.match(old.stderr, /credential reference problem\(s\)\n$/);
|
||||
}
|
||||
@@ -117,11 +120,13 @@ test("rename r2 B2: a credential problem, an expiry warning and a system config
|
||||
doc.roles.coder.credentials.vikunja.expires = soon;
|
||||
writeJson(join(root, "config", "businesses", "acme.json"), doc);
|
||||
});
|
||||
const { old } = await sameAcrossSpellings(["resolve", "acme", "coder"], warned.env, warned.root, 0);
|
||||
const { old, now } = await sameAcrossSpellings(["resolve", "acme", "coder"], warned.env, warned.root, 0);
|
||||
assert.equal(now.stderr, old.stderr, "a warning is byte for byte the same, unnormalized");
|
||||
assert.match(old.stderr, new RegExp(`^mosaic business: warning: .*expires on ${soon}\\n$`, "m"));
|
||||
const missing = installed(t, ({ root, env }) => { env.MOSAIC_CONFIG = join(root, "nowhere", "config.json"); });
|
||||
const sys = await sameAcrossSpellings(["validate", "acme"], missing.env, missing.root, 3);
|
||||
assert.match(sys.old.stderr, /^mosaic business: system config problem \(mosaic-config exit 3\): /);
|
||||
assert.equal(sys.now.stderr, sys.old.stderr, "a system config problem is byte for byte the same, unnormalized");
|
||||
});
|
||||
|
||||
test("rename r2 B2: org config prints the stage A golden v1 bytes, preimage and digest", async (t) => {
|
||||
@@ -170,7 +175,7 @@ test("rename r2 B2: a draft file named like an installed ID: org validate reads
|
||||
const noInstalled = spawn(MOSAIC, ["org", "config", "validate", "acme"], env, cwd);
|
||||
assert.equal(noInstalled.status, 4);
|
||||
assert.equal(noInstalled.stdout, "");
|
||||
assert.match(noInstalled.stderr, /^mosaic org config: .*acme/);
|
||||
assert.match(noInstalled.stderr, /^mosaic business: .*acme/);
|
||||
assert.equal(spawn(MOSAIC, ["org", "validate", "acme"], env, cwd).stdout, "valid\n");
|
||||
});
|
||||
|
||||
@@ -309,7 +314,7 @@ test("rename r2 B2: importing the config service runs nothing and never exits; i
|
||||
assert.equal(runConfigCommand(["validate", "acme"], { ...streams, env, spelling: ORG_CONFIG }), 3);
|
||||
assert.equal(runConfigCommand([], { ...streams, env }), 4);
|
||||
assert.equal(out.text, "");
|
||||
assert.match(err.text, /^mosaic org config: system config problem /);
|
||||
assert.match(err.text, /^mosaic business: system config problem /);
|
||||
assert.match(err.text, /\nmosaic business: usage: mosaic business validate <business> \| resolve /);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@ node --test packages/organization/tests/
|
||||
|
||||
`mosaic business validate|resolve` is the old spelling, with the same
|
||||
arguments. Both spellings run `runConfigCommand` in `src/config-command.mjs`,
|
||||
so status and stdout are the same. Only stderr differs: each line starts
|
||||
`mosaic org config: ` or `mosaic business: `, and the usage line names the
|
||||
spelling you used. Importing `src/config-command.mjs` runs nothing and never
|
||||
so status, stdout and stderr are the same. The one exception is a usage
|
||||
error: its line names the spelling you used (`mosaic org config: usage: ...`).
|
||||
Every other stderr line, refusals and warnings included, starts
|
||||
`mosaic business: ` in both spellings, so the diagnostics stay byte for byte
|
||||
what the old command printed. Importing `src/config-command.mjs` runs nothing and never
|
||||
exits the process; `runConfigCommand(argv, {stdout, stderr, env, spelling})`
|
||||
returns the exit code. `src/cli.mjs` is the thin executable that the old
|
||||
spelling runs. `mosaic org validate FILE` is different: it checks a draft file
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
// packages/business/src/cli.mjs path that forwards to it) and `mosaic org config validate|resolve`
|
||||
// (packages/cli/src/cli.mjs) both call runConfigCommand. It never exits the process or runs on
|
||||
// import: it writes to the streams it is given and returns the exit code. A spelling sets only the
|
||||
// prefix of each stderr line and the usage text; status, stdout and every message after the prefix
|
||||
// are the same for both.
|
||||
// usage line (its prefix and text). Status, stdout and every other stderr line, refusals and
|
||||
// warnings included, are byte for byte the same for both: they keep the `mosaic business: ` prefix
|
||||
// so a caller moving to the new spelling keeps the diagnostic contract.
|
||||
//
|
||||
// Exit codes: 0 ok; 2 invalid (organization, role, project file or a credential reference);
|
||||
// 3 system config problem; 4 usage, or a required file missing.
|
||||
@@ -49,7 +50,12 @@ class Exit {
|
||||
// argv is [verb, ...args]. Returns the exit code. An error that is not an OrganizationConfigError
|
||||
// (a bug, not a refusal) is thrown to the caller, as the old executable let it escape.
|
||||
export function runConfigCommand(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, spelling = BUSINESS_CONFIG } = {}) {
|
||||
const say = (message) => stderr.write(`${spelling.prefix}: ${message}\n`);
|
||||
// Diagnostics keep the old prefix in both spellings; only the usage line follows the spelling.
|
||||
const say = (message) => stderr.write(`${BUSINESS_CONFIG.prefix}: ${message}\n`);
|
||||
const usage = () => {
|
||||
stderr.write(`${spelling.prefix}: ${spelling.usage}\n`);
|
||||
throw new Exit(4);
|
||||
};
|
||||
const fail = (code, message) => {
|
||||
say(message);
|
||||
throw new Exit(code);
|
||||
@@ -85,7 +91,7 @@ export function runConfigCommand(argv, { stdout = process.stdout, stderr = proce
|
||||
[REPO, config.dataRoot, ...Object.values(organization.projects).filter((p) => p.kind !== "library").map((p) => p.root)];
|
||||
|
||||
const validate = (args) => {
|
||||
if (args.length !== 1) fail(4, spelling.usage);
|
||||
if (args.length !== 1) usage();
|
||||
const config = loadSystem();
|
||||
const system = systemVars(config);
|
||||
const organization = load(args[0]);
|
||||
@@ -128,11 +134,11 @@ export function runConfigCommand(argv, { stdout = process.stdout, stderr = proce
|
||||
const rest = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "--project") {
|
||||
if (!args[i + 1] || project !== null) fail(4, spelling.usage);
|
||||
if (!args[i + 1] || project !== null) usage();
|
||||
project = args[++i];
|
||||
} else rest.push(args[i]);
|
||||
}
|
||||
if (rest.length !== 2) fail(4, spelling.usage);
|
||||
if (rest.length !== 2) usage();
|
||||
const [organizationId, instance] = rest;
|
||||
const config = loadSystem();
|
||||
const organization = load(organizationId);
|
||||
@@ -156,7 +162,7 @@ export function runConfigCommand(argv, { stdout = process.stdout, stderr = proce
|
||||
try {
|
||||
if (verb === "validate") validate(args);
|
||||
else if (verb === "resolve") resolveCommand(args);
|
||||
else fail(4, spelling.usage);
|
||||
else usage();
|
||||
return 0;
|
||||
} catch (error) {
|
||||
if (error instanceof Exit) return error.code;
|
||||
|
||||
Reference in new issue
Block a user