extractCommandTokens regexes matched the /gsd-core substring inside the org/repo path open-gsd/gsd-core#22. Add a negative lookbehind so only actual invocations (BOL / space / backtick / paren) match, not path or word-embedded segments. Add a regression test covering the repo-path false positive while proving real broken command refs are still caught. Refs #489 Co-authored-by: CI Rebase Check <ci@gsd-redux>
This commit is contained in:
@@ -197,9 +197,14 @@ function extractCommandTokens(content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allSlash = (stripped.match(/\/gsd-[a-z0-9][a-z0-9-]*/g) || []);
|
||||
const allColon = (stripped.match(/\/gsd:[a-z0-9][a-z0-9-]*/g) || []);
|
||||
const allDollar = (stripped.match(/\$gsd-[a-z0-9][a-z0-9-]*/g) || []);
|
||||
// Negative lookbehind: only match tokens NOT preceded by a letter, digit,
|
||||
// `/`, `_`, or `-`. This prevents matching the `/gsd-core` substring inside
|
||||
// the org/repo path `open-gsd/gsd-core` (and similar path-embedded segments)
|
||||
// while still matching real invocations preceded by BOL, space, backtick, or
|
||||
// `(`. Fixes false-positive class identified in #489.
|
||||
const allSlash = (stripped.match(/(?<![A-Za-z0-9/_-])\/gsd-[a-z0-9][a-z0-9-]*/g) || []);
|
||||
const allColon = (stripped.match(/(?<![A-Za-z0-9/_-])\/gsd:[a-z0-9][a-z0-9-]*/g) || []);
|
||||
const allDollar = (stripped.match(/(?<![A-Za-z0-9/_-])\$gsd-[a-z0-9][a-z0-9-]*/g) || []);
|
||||
|
||||
const slash = new Set(allSlash.filter(t => !isInternal(t)));
|
||||
const colon = new Set(allColon.filter(t => !isInternal(t)));
|
||||
@@ -483,3 +488,44 @@ describe('adversarial: polarity inversion catches drift deny-list misses', () =>
|
||||
assert.ok(!registry.has('/gsd-research-phase'), '/gsd-research-phase must not be in the live registry');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tokenizer regression tests (#489) ───────────────────────────────────────
|
||||
|
||||
describe('extractCommandTokens() — repo-path false-positive regression (#489)', () => {
|
||||
test('open-gsd/gsd-core#22 repo path does NOT produce a /gsd-core token', () => {
|
||||
// Before the lookbehind fix, /gsd-core inside `open-gsd/gsd-core#22`
|
||||
// would be matched by the slash regex — a false positive.
|
||||
const { slash, colon, dollar } = extractCommandTokens(
|
||||
'see open-gsd/gsd-core#22 for details'
|
||||
);
|
||||
const all = [...slash, ...colon, ...dollar];
|
||||
assert.ok(
|
||||
!all.includes('/gsd-core'),
|
||||
'repo path open-gsd/gsd-core#22 must not produce a /gsd-core token; got: ' + all.join(', ')
|
||||
);
|
||||
assert.strictEqual(all.length, 0, 'expected zero tokens from a bare repo-path string; got: ' + all.join(', '));
|
||||
});
|
||||
|
||||
test('space-preceded /gsd-totally-not-a-real-command is still extracted (real invocation)', () => {
|
||||
// A genuine (but unregistered) command reference after whitespace must be
|
||||
// captured so the live-registry check can flag it as unknown.
|
||||
const { slash } = extractCommandTokens(
|
||||
'run /gsd-totally-not-a-real-command here'
|
||||
);
|
||||
assert.ok(
|
||||
slash.has('/gsd-totally-not-a-real-command'),
|
||||
'invocation after whitespace must be extracted; slash set: ' + [...slash].join(', ')
|
||||
);
|
||||
});
|
||||
|
||||
test('backtick-wrapped `/gsd-plan` is still extracted (real invocation)', () => {
|
||||
// Backtick-wrapped commands (common in markdown) must still be captured.
|
||||
const { slash } = extractCommandTokens(
|
||||
'use `/gsd-plan` to plan'
|
||||
);
|
||||
assert.ok(
|
||||
slash.has('/gsd-plan'),
|
||||
'backtick-wrapped invocation must be extracted; slash set: ' + [...slash].join(', ')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user