fix(#1615): normalize Windows backslash paths in workflow content
computePathPrefix returned a Windows-style path (with backslashes from path.join) into markdown @-references. Workflow file content on Windows ended up with mixed separators, breaking substring checks in install/install-runtime-artifacts tests on windows-latest CI only. Normalize resolvedTarget and homeDir to forward slashes inside computePathPrefix. The prefix is always substituted into markdown body text, which uses POSIX paths universally. Idempotent on POSIX. Also normalizes the two test assertions to forward-slash form so they pass on Windows. Adds a regression test for backslash-style input. Documents DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT + RULESET.CONTENT-PATH-NORMALIZATION in CONTEXT.md so this anti-pattern stops recurring.
This commit is contained in:
@@ -722,6 +722,14 @@ A legal deferred state of an Execute step (`external_job_waiting`): the executor
|
||||
`DEFECT.WINDOWS-TEST-PORTABILITY.fix-forward=gate platform-specific execution with if (process.platform !== 'win32'); normalize path expectations to forward slashes with .replace(/\\/g, '/'); invoke scripts via explicit interpreter (sh <path>) rather than relying on exec-bit; annotate // windows-portability-ok: <reason> when a bypass is intentional`
|
||||
`DEFECT.WINDOWS-TEST-PORTABILITY.prevention=run lint:ci before opening a PR; treat the CI windows lane as the only true Windows signal — gsd-test (Mac/Linux only) cannot substitute for it`
|
||||
|
||||
`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.symptom=path.join() result on Windows (backslashes) substituted verbatim into markdown body (@-references, workflow files, generated docs); content gains mixed separators; cross-platform substring assertions fail on windows-latest CI lane only; macOS/Linux CI green so defect ships undetected`
|
||||
`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.examples=PR #1622 computePathPrefix returned ${resolvedTarget}/ verbatim — rewrites of @~/.claude/gsd-core/commands/gsd/X.md wrote @C:\...\gsd-ial-windsurf-XXX\gsd-core/commands/gsd/help.md (trailing forward slashes from the original literal survived, prefix backslashes did not); tests/install-runtime-artifacts.test.cjs:318 + tests/install.test.cjs:1323 failed on windows-latest only`
|
||||
`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.detect=any function returning a filesystem path that flows into markdown/text body substitution; grep for path.join/raw resolvedTarget/${configDir}/ in code paths writing workflow .md, agent .md, or generated docs; smoke pattern is ${resolvedTarget}/ or ${configDir}/... templates that bypass normalization`
|
||||
`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.fix-forward=normalize at the SOURCE not the test: posixTarget=String(resolvedTarget).replace(/\\/g,'/'), posixHome=homeDir?String(homeDir).replace(/\\/g,'/'):homeDir; markdown body is POSIX-only; .replace(/\\/g,'/') is idempotent on POSIX (no backslashes present) so safe to apply unconditionally; isWindowsHost arg is a no-op tripwire (enh-1511) — do NOT branch on it, normalize always`
|
||||
`DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.prevention=RULESET.CONTENT-PATH-NORMALIZATION; tests are downstream signal, never the fix; ref DEFECT.WINDOWS-TEST-PORTABILITY for test-side parity (normalize expected substrings too: ${configDir}/foo.replace(/\\/g,'/'))`
|
||||
|
||||
`RULESET.CONTENT-PATH-NORMALIZATION=filesystem paths substituted into markdown body text (@-references, workflow .md, agent .md, generated docs, command bodies) MUST be normalized to POSIX forward slashes via .replace(/\\/g,'/') at the production source BEFORE substitution; never push normalization to tests; cross-platform content is POSIX-only; applies to: computePathPrefix output, install-path rewrites, generated shim paths emitted into .md bodies; idempotent on POSIX so unconditional`
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2169,10 +2169,18 @@ function convertClaudeCommandToKiloSkill(content, skillName) {
|
||||
* @private — exported as `_computePathPrefix` for tests.
|
||||
*/
|
||||
function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget, homeDir }) {
|
||||
if (isGlobal && resolvedTarget.startsWith(homeDir) && !isOpencode) {
|
||||
return '$HOME' + resolvedTarget.slice(homeDir.length) + '/';
|
||||
// #1615: normalize Windows backslashes to forward slashes. This prefix is
|
||||
// substituted into markdown @-references (e.g. Windsurf workflow files),
|
||||
// which use POSIX paths universally. Idempotent on POSIX (no backslashes).
|
||||
// Without this, path.join on Windows produces a backslash prefix that
|
||||
// leaks into markdown content and breaks cross-platform substring checks.
|
||||
// See DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT in CONTEXT.md.
|
||||
const posixTarget = String(resolvedTarget).replace(/\\/g, '/');
|
||||
const posixHome = homeDir ? String(homeDir).replace(/\\/g, '/') : homeDir;
|
||||
if (isGlobal && posixTarget.startsWith(posixHome) && !isOpencode) {
|
||||
return '$HOME' + posixTarget.slice(posixHome.length) + '/';
|
||||
}
|
||||
return `${resolvedTarget}/`;
|
||||
return `${posixTarget}/`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,6 +91,22 @@ describe('_computePathPrefix', () => {
|
||||
assert.equal(withWindows, '$HOME/.cursor/');
|
||||
assert.strictEqual(withWindows, withoutWindows);
|
||||
});
|
||||
|
||||
test('backslash-style resolvedTarget is normalized to forward slashes (#1615 regression)', () => {
|
||||
// path.join on Windows produces backslashes; the returned prefix is
|
||||
// substituted into markdown @-references which must use POSIX paths.
|
||||
// Without normalization the backslashes leak into workflow file content
|
||||
// and break substring checks on Windows CI.
|
||||
const prefix = conversion._computePathPrefix({
|
||||
isGlobal: false,
|
||||
isOpencode: false,
|
||||
isWindowsHost: true,
|
||||
resolvedTarget: 'C:\\Users\\runner\\AppData\\Local\\Temp\\gsd-1615-windsurf',
|
||||
homeDir: 'C:\\Users\\runner',
|
||||
});
|
||||
assert.strictEqual(prefix, 'C:/Users/runner/AppData/Local/Temp/gsd-1615-windsurf/');
|
||||
assert.ok(!prefix.includes('\\'), `prefix must not contain backslashes: ${prefix}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -315,7 +315,7 @@ describe('installRuntimeArtifacts — windsurf workflows layout (#1615)', () =>
|
||||
const helpContent = fs.readFileSync(path.join(workflowsDir, 'gsd-help.md'), 'utf8');
|
||||
assert.ok(!helpContent.startsWith('---'), 'Windsurf workflows must be plain markdown, not SKILL.md frontmatter');
|
||||
assert.match(helpContent, /# gsd-help/, 'workflow should identify the slash command it backs');
|
||||
assert.ok(helpContent.includes(`${configDir}/gsd-core/commands/gsd/help.md`),
|
||||
assert.ok(helpContent.includes(`${configDir}/gsd-core/commands/gsd/help.md`.replace(/\\/g, '/')),
|
||||
'workflow should reference the installed command body using the actual install target');
|
||||
|
||||
for (const fileName of fs.readdirSync(workflowsDir)) {
|
||||
|
||||
@@ -1320,7 +1320,7 @@ describe('windsurf local install writes workflow slash commands (#1615)', () =>
|
||||
const workflowFile = path.join(workflowsDir, workflowEntry.name);
|
||||
const content = fs.readFileSync(workflowFile, 'utf8');
|
||||
assert.ok(
|
||||
content.includes(`${tmpDir}/.windsurf/gsd-core/commands/gsd/`),
|
||||
content.includes(`${tmpDir}/.windsurf/gsd-core/commands/gsd/`.replace(/\\/g, '/')),
|
||||
`${workflowEntry.name} must reference the installed canonical command body`,
|
||||
);
|
||||
assert.ok(
|
||||
|
||||
Reference in New Issue
Block a user