* test(#4324): prove colon tokens the installer cannot convert leak Failing-first regression coverage for #4324. The install rewrite (transformContentToHyphen) is gated on an exact match against the commands/gsd stem list, so any /gsd:<token> whose token is not a registered stem survives the install and reaches the user as the deprecated colon form. The gate is load-bearing -- it is the only thing protecting the workflow DSL marker family (gsd:section, gsd:protected, gsd:loop-host, gsd:guard, gsd:dispatch, gsd:plan-revision-conflicts), which workflow-fragments parses as a literal. So this suite asserts the shipped text is convertible rather than asserting the transform is broad, and pins the marker family as explicit negative space. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#4324): stop unconvertible colon tokens reaching the user The install rewrite is gated on an exact match against the commands/gsd stem list, so a /gsd:<token> whose token is not a registered stem survives the install and reaches the user as the deprecated colon form. That gate is load-bearing -- it protects the gsd:section / gsd:protected / gsd:loop-host marker family -- so the fix is in the shipped text, and the source stays colon per CONTEXT.md's two-tier rule. - quick-batch command + skill description: close the command token at a boundary so `/gsd:quick`-shaped converts instead of being skipped. - gsd-code-fixer (both variants): execute-plan and diagnose-issues are workflows, not commands, so they never converted and rendered beside two hyphenated siblings on the same line. Name them as workflows. - help topic-mode: the extraction rule hard-coded a colon prefix that the converted full.md never ships, so --brief could never match a signature line and silently fell back on every topic. Describe the signature line without a literal prefix. - update.md: drop the prefix from prose describing a stale command. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#4324): add changeset fragment pr:0 placeholder is backfilled with the real number once the PR exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#4324): locate the help summary per reference variant Adversarial review finding. Restoring the signature-line match (the #4324 fix) activated a latent defect in the clause next to it: compact scope emitted "the single non-blank line immediately after" the signature, and that clause is only correct for full.md. full.compact.md puts the summary on the signature line itself, after an em-dash, and its next non-blank line is an unrelated "Usage:" line. Both variants ship and both are served, so before this commit the compact variant would have emitted the wrong line as the summary. It was masked until now only because the stale colon prefix meant no signature line ever matched at all. Name the two placements and pick per line, and say explicitly that a Usage: line is never a summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#4324): de-vacuum the help parity check, narrow the marker waiver Two adversarial review findings against the #4324 coverage. The help-parity assertion went vacuous the moment the fix landed: once topic.md stops spelling a literal prefix, the matched set is empty and the assertion holds for any rewording, correct or not. It now also asserts across BOTH served reference variants that each ships signature lines under the hyphen prefix, that the two genuinely disagree about where the summary sits, and that topic.md still names both placements and the Usage: guard. The marker waiver keyed on "sits inside an HTML comment", which waves through a real broken reference that happens to be commented out -- `<!-- see /gsd:typo-cmd -->` scored clean. Enumerate the six marker families instead. Verified the narrowed rule catches that probe and still passes over the tree; it also surfaced a seventh family, write-continue, that the broad rule was hiding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#4324): normalize the namespace in skill descriptions Both hyphen-namespace skill converters ran the hyphen transform over the body but rebuilt the frontmatter description from the raw field, so a /gsd:<cmd> mention in a command description survived into the installed SKILL.md -- the exact field the host's skill picker renders, which is the surface this issue was filed about. The local flat-command path was already correct because it rewrites the whole file; only the skills path, used by a global install, was affected. Confirmed by installing into a fake HOME before and after. Fixed in both copies: bin/install.js and the src/ source of truth that compiles into gsd-core/bin/lib. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#4324): assert descriptions through the real converters The previous version of this check called transformContentToHyphen on the description line itself and passed, while a real install still shipped the colon form -- the converter never calls that transform on the description. It asserted a proxy for the behaviour instead of the behaviour. Drive convertClaudeCommandToClaudeSkill and convertClaudeCommandToClineSkill over every registered command and assert on the emitted description. Verified it fails against the pre-fix converters and passes against the fixed ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#4324): regenerate skills after the description change skills/<name>/SKILL.md is generated by gen-plugin-skills, not hand-maintained, and lint:generated-sync caught the hand edit. The regenerated file emits the hyphen form, which also corrects the assumption behind the scan comment in the namespace test: skills/ is runtime-emitter output, not colon source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#4324): re-sanction normalizeKimiSkillName's real end line The description-normalisation fix inserted five lines above normalizeKimiSkillName in src/runtime-artifact-conversion.cts, moving its closing brace from 635 to 640. MAJOR-1 pins that line deliberately, so the planted violation landed INSIDE the exempted body and went unflagged -- 0 !== 1. Re-sanction the value rather than derive it: the array is named sanctionedRealEndLines, and a pinned line that fails loudly on drift is the design. Deriving it would remove the human check the name asks for. Verified by executing all four MAJOR-1 rows against the real tree: each planted violation is flagged at realEndLine+1 and each unmodified file stays exempt. Emitted-Drift-Ack-Growth: gsd-code-fixer.md — names execute-plan and diagnose-issues as workflows rather than as slash commands that do not exist Emitted-Drift-Ack-Growth: gsd-code-fixer.compact.md — same rewording as its full sibling, kept byte-consistent with it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#4324): backfill the changeset PR number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1099,3 +1099,375 @@ describe('bug #3683 — workflow/reference colon-namespace leak (Claude local in
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// #4324 — colon tokens the install transform CANNOT convert leak to users
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Companion to the `#3443` invariant above, and deliberately its mirror image.
|
||||
// That one asserts the source stays COLON. This one asserts every colon token
|
||||
// in the source is one the installer can actually turn into hyphen form.
|
||||
//
|
||||
// The install rewrite (`transformContentToHyphen`) is gated on an exact match
|
||||
// against the `commands/gsd/*.md` stem list, so a `/gsd:<token>` whose token is
|
||||
// not a registered stem survives the install untouched and reaches the user as
|
||||
// the deprecated colon form. #4324 reported this as "all auto-suggestions are
|
||||
// still using the outdated /gsd:".
|
||||
//
|
||||
// That gate is load-bearing and must NOT be widened: it is the only thing
|
||||
// protecting the workflow DSL marker family (`gsd:section`, `gsd:protected`,
|
||||
// `gsd:loop-host`, `gsd:guard`, `gsd:dispatch`, `gsd:plan-revision-conflicts`),
|
||||
// which is parsed as a literal — `src/workflow-fragments.cts` pins
|
||||
// `CLOSE_TAG = '/gsd:section'`. The fix therefore belongs in the shipped text,
|
||||
// and this suite is what keeps it there.
|
||||
{
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const FIXER = path.join(ROOT, 'scripts', 'fix-slash-commands.cjs');
|
||||
// Drive the REAL production transform with the REAL roster. A roster invented
|
||||
// here could only confirm what this test's author already believed about the
|
||||
// gate (fixture-provenance rule, #2371).
|
||||
const {
|
||||
transformContentToHyphen,
|
||||
buildColonPattern,
|
||||
readCmdNames,
|
||||
SKIP_DIRS,
|
||||
} = require(FIXER);
|
||||
|
||||
const cmdNames = readCmdNames();
|
||||
|
||||
// Shipped surfaces whose text the runtime loads and shows the user.
|
||||
//
|
||||
// `skills/` is included even though the #3443 colon-invariant scan omits it —
|
||||
// but not for the reason that scan omits it. skills/<name>/SKILL.md is
|
||||
// GENERATED by scripts/gen-plugin-skills.cjs and is already emitted in hyphen
|
||||
// form, so it is runtime-emitter output rather than colon source. Scanning it
|
||||
// is a cheap belt-and-braces check that the generator never emits a colon
|
||||
// token the installer could not convert; it currently contributes zero
|
||||
// tokens, and that is the expected steady state.
|
||||
const SCAN_DIRS = [
|
||||
path.join(ROOT, 'commands', 'gsd'),
|
||||
path.join(ROOT, 'agents'),
|
||||
path.join(ROOT, 'gsd-core', 'workflows'),
|
||||
path.join(ROOT, 'gsd-core', 'references'),
|
||||
path.join(ROOT, 'gsd-core', 'templates'),
|
||||
path.join(ROOT, 'skills'),
|
||||
];
|
||||
|
||||
// Structural markers that legitimately use `gsd:` and are NOT slash commands.
|
||||
// Enumerated BY FAMILY, never by "it sits in a comment": a blanket
|
||||
// comment-context waiver would also wave through a genuinely broken command
|
||||
// reference that merely happens to be commented out, e.g.
|
||||
// `<!-- see /gsd:typo-cmd -->`, which is exactly the leak this guard exists
|
||||
// to catch.
|
||||
const COMMENT_MARKER_TOKENS = new Set([
|
||||
'section', // <!-- gsd:section id="…" when="…" --> / <!-- /gsd:section -->
|
||||
'protected', // <!-- gsd:protected:start --> / :end
|
||||
'loop-host', // <!-- gsd:loop-host … -->
|
||||
'plan-revision-conflicts', // <!-- gsd:plan-revision-conflicts:begin --> / :end
|
||||
'live-dom-families', // <!-- gsd:live-dom-families -->
|
||||
'write-continue', // <!-- gsd:write-continue … -->
|
||||
]);
|
||||
const BARE_MARKER_TOKENS = new Set([
|
||||
'guard', // `# gsd:guard=orchestrator-cwd-drift`
|
||||
'dispatch', // `[gsd:dispatch phase="…" plan="…"]`
|
||||
]);
|
||||
const IN_HTML_COMMENT = /<!--[^>]*gsd:/;
|
||||
// `<!-- gsd: no compact sibling … -->` — the compact-content disclosure
|
||||
// banner. Its token is empty, so it is matched by shape rather than by name.
|
||||
const COMPACT_DISCLOSURE = /<!--\s*gsd:\s/;
|
||||
|
||||
function collect(dir, out = []) {
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
if (SKIP_DIRS.has(e.name)) continue;
|
||||
collect(full, out);
|
||||
} else if (e.name.endsWith('.md')) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const shippedFiles = SCAN_DIRS.flatMap((d) => collect(d));
|
||||
|
||||
// Same lookbehind as buildColonPattern, so match indices from the two regexes
|
||||
// are directly comparable.
|
||||
const ANY_COLON_TOKEN = /(?<![a-zA-Z0-9_-])gsd:([a-zA-Z0-9_-]*)/g;
|
||||
|
||||
describe('install-convertibility of colon tokens (#4324)', () => {
|
||||
test('the scan corpus and the real roster are both populated', () => {
|
||||
assert.ok(cmdNames.length > 0, 'commands/gsd/ must yield a non-empty roster');
|
||||
assert.ok(shippedFiles.length > 0, 'SCAN_DIRS must yield shipped .md files');
|
||||
assert.ok(cmdNames.includes('quick'), 'quick must be a registered command stem');
|
||||
});
|
||||
|
||||
// ROW 1 — the failing-first regression test for #4324.
|
||||
test('every gsd: token in user-facing source is convertible or a declared structural marker', () => {
|
||||
const colonPattern = buildColonPattern(cmdNames);
|
||||
assert.ok(colonPattern, 'buildColonPattern must compile for a non-empty roster');
|
||||
|
||||
const violations = [];
|
||||
for (const file of shippedFiles) {
|
||||
// allow-test-rule: source-text-is-the-product (#4324)
|
||||
// These are shipped command/agent/workflow/skill bodies — their text IS
|
||||
// what the runtime loads and renders, so the deployed text is the contract.
|
||||
const src = fs.readFileSync(file, 'utf-8');
|
||||
if (!src.includes('gsd:')) continue;
|
||||
|
||||
const convertibleAt = new Set(
|
||||
[...src.matchAll(new RegExp(colonPattern.source, 'g'))].map((m) => m.index),
|
||||
);
|
||||
const lines = src.split(/\r?\n/);
|
||||
|
||||
for (const m of src.matchAll(ANY_COLON_TOKEN)) {
|
||||
if (convertibleAt.has(m.index)) continue; // installer handles it
|
||||
const lineNo = src.slice(0, m.index).split(/\r?\n/).length;
|
||||
const line = lines[lineNo - 1] || '';
|
||||
if (BARE_MARKER_TOKENS.has(m[1])) continue; // structural marker
|
||||
const inComment = IN_HTML_COMMENT.test(line);
|
||||
if (inComment && COMMENT_MARKER_TOKENS.has(m[1])) continue;
|
||||
if (inComment && m[1] === '' && COMPACT_DISCLOSURE.test(line)) continue;
|
||||
violations.push(
|
||||
`${path.relative(ROOT, file)}:${lineNo}: "${m[0]}" in "${line.trim().slice(0, 90)}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
violations,
|
||||
[],
|
||||
`Found ${violations.length} colon token(s) the install transform cannot convert — ` +
|
||||
`they reach the user as the deprecated /gsd: form (#4324).\n` +
|
||||
`Fix the SHIPPED TEXT, not the transform: the command-stem gate protects the ` +
|
||||
`gsd:section / gsd:protected / gsd:loop-host marker family.\n` +
|
||||
`Either close the command token at a boundary ("\`/gsd:quick\`-shaped", not ` +
|
||||
`"/gsd:quick-shaped"), stop rendering a non-command as a slash command, or ` +
|
||||
`declare a new structural marker in BARE_MARKER_TOKENS.\n` +
|
||||
`Offenders:\n ${violations.join('\n ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ROW 2 — the reported symptom, asserted at the surface the user sees AND
|
||||
// through the code that actually produces it.
|
||||
//
|
||||
// The first version of this test called `transformContentToHyphen` on the
|
||||
// description line directly and passed — while a real install still shipped
|
||||
// the colon form, because both hyphen-namespace skill converters rebuild
|
||||
// `description:` from the raw frontmatter field and never run the transform
|
||||
// on it. That is the difference between asserting the identity and asserting
|
||||
// a proxy for it: drive the converter, or the next converter that forgets to
|
||||
// normalise a field ships the same bug again (#4324).
|
||||
test('installed skill descriptions carry no colon form', () => {
|
||||
// allow-test-rule: integration-test-input (#4324)
|
||||
// The command files are passed to the converters as DATA — real fixture
|
||||
// input to the transformation under test — and the assertion is on the
|
||||
// converter's emitted frontmatter, which is the text the host's skill
|
||||
// picker renders and therefore the deployed contract itself.
|
||||
const {
|
||||
convertClaudeCommandToClaudeSkill,
|
||||
convertClaudeCommandToClineSkill,
|
||||
} = require(path.join(ROOT, 'bin', 'install.js'));
|
||||
|
||||
const converters = [
|
||||
['claude', convertClaudeCommandToClaudeSkill],
|
||||
['cline', convertClaudeCommandToClineSkill],
|
||||
];
|
||||
|
||||
const offenders = [];
|
||||
let checked = 0;
|
||||
for (const stem of cmdNames) {
|
||||
const file = path.join(ROOT, 'commands', 'gsd', `${stem}.md`);
|
||||
if (!fs.existsSync(file)) continue;
|
||||
const src = fs.readFileSync(file, 'utf-8');
|
||||
for (const [label, convert] of converters) {
|
||||
const emitted = convert(src, `gsd-${stem}`, null, cmdNames);
|
||||
const descLine = emitted
|
||||
.split(/\r?\n/)
|
||||
.find((l) => l.startsWith('description:'));
|
||||
if (!descLine) continue;
|
||||
checked += 1;
|
||||
if (/gsd:/.test(descLine)) {
|
||||
offenders.push(
|
||||
`${label} · commands/gsd/${stem}.md → ${descLine.trim().slice(0, 110)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against the whole loop silently doing nothing.
|
||||
assert.ok(
|
||||
checked >= cmdNames.length,
|
||||
`expected at least one emitted description per command per converter; checked ${checked}`,
|
||||
);
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`A converter emitted a skill description still carrying the /gsd: colon ` +
|
||||
`form. This is the picker text #4324 reported, and it reaches the user on ` +
|
||||
`every install.\n ${offenders.join('\n ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ROW 3 — cross-surface parity, across BOTH served reference variants.
|
||||
// topic.md tells the model which bold signature line to extract AND where
|
||||
// that line's one-line summary sits. Two things can go wrong, and both did:
|
||||
// 1. a literal prefix baked into the rule can never match the installed
|
||||
// reference, which is converted to hyphen form at install time; and
|
||||
// 2. the two served variants put the summary in DIFFERENT places —
|
||||
// full.md on the following line, full.compact.md trailing an em-dash on
|
||||
// the signature line itself. A rule that knows only one shape emits the
|
||||
// following `Usage:` line as if it were the summary.
|
||||
test('help topic-mode rule covers every served reference variant', () => {
|
||||
const modes = path.join(ROOT, 'gsd-core', 'workflows', 'help', 'modes');
|
||||
const topicPath = path.join(modes, 'topic.md');
|
||||
assert.ok(fs.existsSync(topicPath), 'help/modes/topic.md must exist');
|
||||
|
||||
const variants = ['full.md', 'full.compact.md']
|
||||
.map((name) => ({ name, file: path.join(modes, name) }))
|
||||
.filter((v) => fs.existsSync(v.file));
|
||||
assert.equal(variants.length, 2, 'both full.md and full.compact.md must ship');
|
||||
|
||||
const shapes = new Set();
|
||||
for (const variant of variants) {
|
||||
// allow-test-rule: source-text-is-the-product (#4324)
|
||||
// The installed reference text IS what the model reads; its rendered
|
||||
// shape is the contract topic.md's extraction rule is written against.
|
||||
const converted = transformContentToHyphen(
|
||||
fs.readFileSync(variant.file, 'utf-8'), cmdNames,
|
||||
);
|
||||
const lines = converted.split(/\r?\n/);
|
||||
const signatureLines = lines.filter((l) => /^\*\*`\/gsd[-:]/.test(l));
|
||||
assert.ok(
|
||||
signatureLines.length > 0,
|
||||
`${variant.name} must contain command-signature bold lines`,
|
||||
);
|
||||
|
||||
const prefixes = new Set(
|
||||
signatureLines.map((l) => l.match(/^\*\*`(\/gsd[-:])/)[1]),
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...prefixes], ['/gsd-'],
|
||||
`${variant.name} must ship only the hyphen signature prefix after install conversion`,
|
||||
);
|
||||
|
||||
for (const l of signatureLines) {
|
||||
shapes.add(/\*\*\s+—\s+\S/.test(l) ? 'same-line' : 'next-line');
|
||||
}
|
||||
}
|
||||
|
||||
// The variants genuinely disagree. This is WHY topic.md's rule must branch,
|
||||
// and it is asserted rather than assumed: if the corpus ever collapses to a
|
||||
// single placement, the two-case rule can be simplified — deliberately.
|
||||
assert.deepEqual(
|
||||
[...shapes].sort(), ['next-line', 'same-line'],
|
||||
'the served variants must between them use both summary placements',
|
||||
);
|
||||
|
||||
// allow-test-rule: source-text-is-the-product (#4324)
|
||||
// topic.md is shipped workflow text the runtime loads; its wording is the
|
||||
// deployed contract, so the wording is what must be asserted.
|
||||
const topic = fs.readFileSync(topicPath, 'utf-8');
|
||||
const topicConverted = transformContentToHyphen(topic, cmdNames);
|
||||
|
||||
// (a) no literal command prefix baked into a bold-signature instruction —
|
||||
// such a prefix can never match the converted reference.
|
||||
const instructed = [...topicConverted.matchAll(/\*\*`(\/gsd[-:])/g)].map((m) => m[1]);
|
||||
assert.deepEqual(
|
||||
instructed, [],
|
||||
`topic.md bakes the literal prefix(es) ${JSON.stringify(instructed)} into a ` +
|
||||
`bold-signature instruction, but the installed reference ships '/gsd-' after ` +
|
||||
`conversion — the match can never succeed and --brief silently falls back to ` +
|
||||
`"heading + first paragraph" on every topic (#4324).`,
|
||||
);
|
||||
|
||||
// (b) the rule must actually COVER both placements. Without this, (a) is
|
||||
// vacuous: any rewording that merely avoids spelling a literal prefix
|
||||
// would pass, including one that handles only a single variant.
|
||||
for (const [needle, why] of [
|
||||
[/Locating the summary/, 'a named sub-rule that says where the summary sits'],
|
||||
[/em-dash/, 'the full.compact.md case — summary trailing an em-dash on the signature line'],
|
||||
[/single non-blank line immediately after/, 'the full.md case — summary on the following line'],
|
||||
[/`Usage:` line is never a summary/, 'the guard against emitting a Usage: line as the summary'],
|
||||
]) {
|
||||
assert.match(
|
||||
topic, needle,
|
||||
`topic.md must retain ${why}; without it the compact variant emits the ` +
|
||||
`following "Usage:" line as if it were the one-line summary (#4324).`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// The gate's own boundary behaviour. These characterize WHY the residuals above
|
||||
// escape, so a future editor cannot "fix" #4324 by widening the gate without
|
||||
// reding the marker guard directly below.
|
||||
describe('colon-gate boundary behaviour (#4324)', () => {
|
||||
test('a command stem at a token boundary converts', () => {
|
||||
for (const input of ['/gsd:quick ', '`/gsd:quick`', '/gsd:quick.', '/gsd:quick']) {
|
||||
assert.match(
|
||||
transformContentToHyphen(input, cmdNames),
|
||||
/gsd-quick/,
|
||||
`expected ${JSON.stringify(input)} to convert`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('a command stem followed by a hyphen is not a command token', () => {
|
||||
// `quick` is a stem, but `quick-shaped` is not, and the right-hand lookahead
|
||||
// rejects the following `-`. Nothing matches — this is the #4324 mechanism.
|
||||
assert.equal(transformContentToHyphen('/gsd:quick-shaped', cmdNames), '/gsd:quick-shaped');
|
||||
});
|
||||
|
||||
test('longest-stem-first ordering is preserved', () => {
|
||||
assert.equal(transformContentToHyphen('/gsd:quick-batch', cmdNames), '/gsd-quick-batch');
|
||||
});
|
||||
|
||||
test('an empty roster converts nothing', () => {
|
||||
assert.equal(buildColonPattern([]), null);
|
||||
assert.equal(transformContentToHyphen('/gsd:quick', []), '/gsd:quick');
|
||||
});
|
||||
|
||||
test('non-command gsd- identifiers are left alone', () => {
|
||||
for (const input of ['/gsd-sdk', '/gsd-tools']) {
|
||||
assert.equal(transformContentToHyphen(input, cmdNames), input);
|
||||
}
|
||||
});
|
||||
|
||||
// NEGATIVE SPACE — the guard that makes "just un-gate the regex" fail loudly.
|
||||
test('structural markers survive the install transform unchanged', () => {
|
||||
const markers = [
|
||||
'<!-- gsd:section id="converge-loop" when="state:plan-strategy-converge" -->',
|
||||
'<!-- /gsd:section -->',
|
||||
'<!-- gsd:protected:start -->',
|
||||
'<!-- gsd:protected:end -->',
|
||||
'<!-- gsd:loop-host',
|
||||
'<!-- gsd:plan-revision-conflicts:begin -->',
|
||||
'<!-- gsd:plan-revision-conflicts:end -->',
|
||||
'# gsd:guard=orchestrator-cwd-drift',
|
||||
'[gsd:dispatch phase="{phase_number}" plan="{plan_id}"]',
|
||||
'<!-- gsd:live-dom-families -->',
|
||||
];
|
||||
for (const marker of markers) {
|
||||
assert.equal(
|
||||
transformContentToHyphen(marker, cmdNames),
|
||||
marker,
|
||||
`structural marker must survive byte-identical: ${marker}`,
|
||||
);
|
||||
// CRLF variant — same verdict.
|
||||
assert.equal(
|
||||
transformContentToHyphen(`${marker}\r\n`, cmdNames),
|
||||
`${marker}\r\n`,
|
||||
`structural marker must survive byte-identical under CRLF: ${marker}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('findSlugDerivationDrift — MAJOR-1: allowlist exemption is scoped to
|
||||
const sanctionedRealEndLines = [
|
||||
{ file: path.join('src', 'core-utils.cts'), fn: 'generateSlugInternal', realEndLine: 199 },
|
||||
{ file: path.join('src', 'gsd2-import.cts'), fn: 'slugify', realEndLine: 103 },
|
||||
{ file: path.join('src', 'runtime-artifact-conversion.cts'), fn: 'normalizeKimiSkillName', realEndLine: 635 },
|
||||
{ file: path.join('src', 'runtime-artifact-conversion.cts'), fn: 'normalizeKimiSkillName', realEndLine: 640 },
|
||||
{ file: path.join('scripts', 'generate-package-identity.cjs'), fn: 'slugifyPackageName', realEndLine: 42 },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user