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.
92 lines
4.2 KiB
JavaScript
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')}`,
|
|
);
|
|
});
|