Files
msd-core/tests/config-get-raw-guard.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

92 lines
4.2 KiB
JavaScript

'use strict';
// ─────────────────────────────────────────────────────────────────────────────
// #3763 — every `config-get` command substitution in shipped content passes
// `--raw` (or is an exempt JSON consumer).
//
// `query config-get <key>` without `--raw` prints `JSON.stringify(value)`, so
// a STRING-typed value reaches a bash variable with embedded literal quotes
// (`RUNTIME='"claude"'`) and every downstream `[ "$X" = "y" ]` / `case "$X"`
// comparison silently never matches. Boolean and numeric values are identical
// either way, which is exactly why the string sites survived testing.
//
// Exempt shapes (consumers that WANT JSON — an object/array value):
// * the receiving variable's name ends in `_JSON`
// * the call carries a JSON-array default (`--default '[]'` / `--default "[]"`)
// Anything else that command-substitutes config-get must pass `--raw`.
// ─────────────────────────────────────────────────────────────────────────────
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const REPO_ROOT = path.join(__dirname, '..');
const SCAN_ROOTS = [
'msd-core/workflows',
'commands',
'agents',
'skills',
];
function walk(dir, out) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full, out);
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
}
return out;
}
// A config-get COMMAND SUBSTITUTION: any command substitution containing
// config-get — `$(msd_run query config-get ...)`, `$(msd-tools ... config-get
// ...)`, etc. Prose mentions (no `$(`) do not match; nested `$( )` inside the
// substitution is not a shape the shipped trees use for config-get calls.
const SUBSTITUTION_RE = /\$\([^)]*config-get[^)]*\)/g;
test('#3763: every config-get command substitution in shipped content passes --raw (or is an exempt JSON consumer)', () => {
const files = [];
for (const root of SCAN_ROOTS) walk(path.join(REPO_ROOT, root), files);
const offenders = [];
let scannedSubstitutions = 0;
for (const file of files) {
const rel = path.relative(REPO_ROOT, file);
const lines = fs.readFileSync(file, 'utf-8').split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.includes('config-get')) continue;
const subs = line.match(SUBSTITUTION_RE) || [];
for (const sub of subs) {
scannedSubstitutions++;
// --raw must sit in the config-get command itself, before any `||`
// fallback — an `echo "" --raw` fallback arg would otherwise
// false-pass the check while config-get still lacks the flag.
const cmd = sub.slice(0, sub.indexOf('||') >= 0 ? sub.indexOf('||') : sub.length);
if (cmd.includes('--raw')) continue;
// Exempt shapes: a JSON-consuming variable name, or an explicit
// JSON-array default — the caller wants JSON.stringify output.
const varMatch = /^\s*[A-Za-z_][A-Za-z0-9_]*=/.exec(line);
const varName = varMatch ? varMatch[0].trimEnd().slice(0, -1) : '';
if (varName.endsWith('_JSON')) continue;
if (/--default\s+('\[\]'|"\[\]")/.test(sub)) continue;
offenders.push(`${rel}:${i + 1}: ${line.trim()}`);
}
}
}
assert.ok(scannedSubstitutions > 20,
`guard self-check: expected to scan dozens of config-get substitutions, found ${scannedSubstitutions} — the scan roots or matcher rotted`);
assert.deepEqual(
offenders,
[],
`#3763: config-get command substitutions without --raw feed JSON.stringify output into bash string comparisons (they silently never match for string values). Add --raw, or rename the receiving variable to *_JSON / pass a '[]' default if the consumer parses JSON. Offenders:\n${offenders.join('\n')}`,
);
});