Fresh windsurf/devin-desktop workspace installs write skills under .devin/ (legacy .windsurf/ recognized); global ~/.codeium/windsurf/ unchanged. Also threads real isGlobal through _applyRuntimeRewrites so global skill content references the codeium path. Closes #1085.
339 lines
14 KiB
JavaScript
339 lines
14 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* Regression tests for issue #983 — Trae and Windsurf converters leak
|
|
* unreplaced bare `~/.claude` / `$HOME/.claude` references.
|
|
*
|
|
* Both converters rewrote only trailing-slash `.claude/` forms, so bare
|
|
* home-path references (configDir = ~/.claude, $HOME/.claude) survived
|
|
* conversion and pointed users at the wrong config dir.
|
|
*
|
|
* Fix: add bare word-boundary replacements mirroring Cline (#782) and
|
|
* Codex (#570) precedent, with a negative lookahead to preserve `.claude-plugin`.
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const {
|
|
convertClaudeToWindsurfMarkdown,
|
|
convertClaudeToTraeMarkdown,
|
|
_applyRuntimeRewrites,
|
|
} = require('../bin/install.js');
|
|
|
|
// ─── Windsurf converter bare-form tests ─────────────────────────────────────
|
|
|
|
describe('convertClaudeToWindsurfMarkdown — bare ~/.claude and CLAUDE_CONFIG_DIR (#983)', () => {
|
|
test('bare ~/.claude rewritten to ~/.devin (#1085: workspace dir is now .devin)', () => {
|
|
const input = 'Config dir: (~/.claude), skills at ~/.claude/skills';
|
|
const result = convertClaudeToWindsurfMarkdown(input);
|
|
assert.ok(
|
|
!/~\/\.claude(?![\w-])/.test(result),
|
|
`bare ~/.claude must be rewritten; got: ${result}`,
|
|
);
|
|
assert.ok(result.includes('~/.devin'), 'must rewrite to ~/.devin');
|
|
});
|
|
|
|
test('$HOME/.claude rewritten to $HOME/.devin (#1085: workspace dir is now .devin)', () => {
|
|
const input = 'RUNTIME_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"';
|
|
const result = convertClaudeToWindsurfMarkdown(input);
|
|
assert.ok(
|
|
!/\$HOME\/\.claude(?![\w-])/.test(result),
|
|
`bare $HOME/.claude must be rewritten; got: ${result}`,
|
|
);
|
|
assert.ok(result.includes('$HOME/.devin'), 'must rewrite to $HOME/.devin');
|
|
});
|
|
|
|
test('CLAUDE_CONFIG_DIR rewritten to WINDSURF_CONFIG_DIR', () => {
|
|
const input = 'Use CLAUDE_CONFIG_DIR or $HOME/.claude to configure';
|
|
const result = convertClaudeToWindsurfMarkdown(input);
|
|
assert.ok(
|
|
result.includes('WINDSURF_CONFIG_DIR'),
|
|
'CLAUDE_CONFIG_DIR must become WINDSURF_CONFIG_DIR',
|
|
);
|
|
assert.ok(
|
|
!result.includes('CLAUDE_CONFIG_DIR'),
|
|
'CLAUDE_CONFIG_DIR must be gone',
|
|
);
|
|
});
|
|
|
|
test('.claude-plugin is NOT corrupted (preserved as-is)', () => {
|
|
const input = 'The .claude-plugin/plugin.json manifest enables plugin install.';
|
|
const result = convertClaudeToWindsurfMarkdown(input);
|
|
assert.ok(
|
|
result.includes('.claude-plugin'),
|
|
`.claude-plugin must be preserved; got: ${result}`,
|
|
);
|
|
assert.ok(
|
|
!result.includes('.windsurf-plugin'),
|
|
`.windsurf-plugin must not appear; got: ${result}`,
|
|
);
|
|
});
|
|
|
|
test('no bare ~/.claude in converted surface.md', () => {
|
|
const surfacePath = path.join(__dirname, '..', 'commands', 'gsd', 'surface.md');
|
|
const raw = fs.readFileSync(surfacePath, 'utf8');
|
|
const result = convertClaudeToWindsurfMarkdown(raw);
|
|
assert.ok(
|
|
!/~\/\.claude(?![\w-])/.test(result),
|
|
'converted surface.md must not contain bare ~/.claude',
|
|
);
|
|
});
|
|
|
|
test('no $HOME/.claude in converted surface.md', () => {
|
|
const surfacePath = path.join(__dirname, '..', 'commands', 'gsd', 'surface.md');
|
|
const raw = fs.readFileSync(surfacePath, 'utf8');
|
|
const result = convertClaudeToWindsurfMarkdown(raw);
|
|
assert.ok(
|
|
!/\$HOME\/\.claude(?![\w-])/.test(result),
|
|
'converted surface.md must not contain bare $HOME/.claude',
|
|
);
|
|
});
|
|
|
|
test('no CLAUDE_CONFIG_DIR in converted surface.md', () => {
|
|
const surfacePath = path.join(__dirname, '..', 'commands', 'gsd', 'surface.md');
|
|
const raw = fs.readFileSync(surfacePath, 'utf8');
|
|
const result = convertClaudeToWindsurfMarkdown(raw);
|
|
assert.ok(
|
|
!result.includes('CLAUDE_CONFIG_DIR'),
|
|
'converted surface.md must not contain CLAUDE_CONFIG_DIR',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── Trae converter bare-form tests ─────────────────────────────────────────
|
|
|
|
describe('convertClaudeToTraeMarkdown — bare ~/.claude and CLAUDE_CONFIG_DIR (#983)', () => {
|
|
test('bare ~/.claude rewritten to ~/.trae', () => {
|
|
const input = 'Config dir: (~/.claude), skills at ~/.claude/skills';
|
|
const result = convertClaudeToTraeMarkdown(input);
|
|
assert.ok(
|
|
!/~\/\.claude(?![\w-])/.test(result),
|
|
`bare ~/.claude must be rewritten; got: ${result}`,
|
|
);
|
|
assert.ok(result.includes('~/.trae'), 'must rewrite to ~/.trae');
|
|
});
|
|
|
|
test('$HOME/.claude rewritten to $HOME/.trae', () => {
|
|
const input = 'RUNTIME_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"';
|
|
const result = convertClaudeToTraeMarkdown(input);
|
|
assert.ok(
|
|
!/\$HOME\/\.claude(?![\w-])/.test(result),
|
|
`bare $HOME/.claude must be rewritten; got: ${result}`,
|
|
);
|
|
assert.ok(result.includes('$HOME/.trae'), 'must rewrite to $HOME/.trae');
|
|
});
|
|
|
|
test('CLAUDE_CONFIG_DIR rewritten to TRAE_CONFIG_DIR', () => {
|
|
const input = 'Use CLAUDE_CONFIG_DIR or $HOME/.claude to configure';
|
|
const result = convertClaudeToTraeMarkdown(input);
|
|
assert.ok(
|
|
result.includes('TRAE_CONFIG_DIR'),
|
|
'CLAUDE_CONFIG_DIR must become TRAE_CONFIG_DIR',
|
|
);
|
|
assert.ok(
|
|
!result.includes('CLAUDE_CONFIG_DIR'),
|
|
'CLAUDE_CONFIG_DIR must be gone',
|
|
);
|
|
});
|
|
|
|
test('.claude-plugin is NOT corrupted (preserved as-is)', () => {
|
|
const input = 'The .claude-plugin/plugin.json manifest enables plugin install.';
|
|
const result = convertClaudeToTraeMarkdown(input);
|
|
assert.ok(
|
|
result.includes('.claude-plugin'),
|
|
`.claude-plugin must be preserved; got: ${result}`,
|
|
);
|
|
assert.ok(
|
|
!result.includes('.trae-plugin'),
|
|
`.trae-plugin must not appear; got: ${result}`,
|
|
);
|
|
});
|
|
|
|
test('no bare ~/.claude in converted surface.md', () => {
|
|
const surfacePath = path.join(__dirname, '..', 'commands', 'gsd', 'surface.md');
|
|
const raw = fs.readFileSync(surfacePath, 'utf8');
|
|
const result = convertClaudeToTraeMarkdown(raw);
|
|
assert.ok(
|
|
!/~\/\.claude(?![\w-])/.test(result),
|
|
'converted surface.md must not contain bare ~/.claude',
|
|
);
|
|
});
|
|
|
|
test('no $HOME/.claude in converted surface.md', () => {
|
|
const surfacePath = path.join(__dirname, '..', 'commands', 'gsd', 'surface.md');
|
|
const raw = fs.readFileSync(surfacePath, 'utf8');
|
|
const result = convertClaudeToTraeMarkdown(raw);
|
|
assert.ok(
|
|
!/\$HOME\/\.claude(?![\w-])/.test(result),
|
|
'converted surface.md must not contain bare $HOME/.claude',
|
|
);
|
|
});
|
|
|
|
test('no CLAUDE_CONFIG_DIR in converted surface.md', () => {
|
|
const surfacePath = path.join(__dirname, '..', 'commands', 'gsd', 'surface.md');
|
|
const raw = fs.readFileSync(surfacePath, 'utf8');
|
|
const result = convertClaudeToTraeMarkdown(raw);
|
|
assert.ok(
|
|
!result.includes('CLAUDE_CONFIG_DIR'),
|
|
'converted surface.md must not contain CLAUDE_CONFIG_DIR',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── _applyRuntimeRewrites install-path tests (windsurf) ────────────────────
|
|
//
|
|
// These tests exercise the ACTUAL install path that causes the user-facing leak.
|
|
// The converter functions are called at stage time to produce a Windsurf-branded
|
|
// copy, but _applyRuntimeRewrites is the path that runs at INSTALL time and
|
|
// rewrites any surviving ~/.claude / $HOME/.claude refs in the staged files.
|
|
//
|
|
// FAIL-BEFORE proof: prior to this PR, windsurf used /~\/\.claude\b/ which
|
|
// fires on "~/.claude-plugin" because \b matches between 'e' and '-'. Running
|
|
// the test below against the old regex (`\b`) would:
|
|
// - let bare $HOME/.claude survive (it used only /~\/\.claude\b/, missing $HOME form), AND
|
|
// - corrupt "~/.claude-plugin" → "~/.windsurf-plugin".
|
|
// Both assertions in the test below would fail on the old code.
|
|
//
|
|
// PASS-AFTER: the fix changes to (?![\w-]) so:
|
|
// - bare ~/.claude / $HOME/.claude (not followed by word-char or hyphen) → rewritten
|
|
// - ~/.claude-plugin preserved (the '-' after 'e' is in [\w-])
|
|
//
|
|
// NOTE on pathPrefix choice: we use '~/.windsurf/' (a simple home-relative
|
|
// prefix) rather than '$HOME/.codeium/windsurf/' so that the corruption of
|
|
// '~/.claude-plugin' → '~/.windsurf-plugin' is directly detectable via
|
|
// result.includes('.windsurf-plugin').
|
|
describe('_applyRuntimeRewrites(windsurf) — install-path bare-form + .claude-plugin (#983)', () => {
|
|
// Use ~/ prefix (local-style) so that the .windsurf-plugin corruption is
|
|
// directly detectable as a substring of the result.
|
|
const WINDSURF_PATH_PREFIX = '~/.windsurf/';
|
|
|
|
// Compound content: covers every form the fix must handle.
|
|
// IMPORTANT: we use ~/.claude-plugin (home-relative form) to exercise the
|
|
// corruption that the old \b regex caused. The \b fires between 'e' and '-',
|
|
// so ~/.claude-plugin → ~/.windsurf-plugin under the old code. That would
|
|
// break the preservation assertion below. The (?![\w-]) fix prevents this.
|
|
const COMPOUND_INPUT = [
|
|
'Config dir: ~/.claude',
|
|
'Also: $HOME/.claude',
|
|
'Slash form: ~/.claude/skills/foo.md',
|
|
'Plugin installed at: ~/.claude-plugin/plugin.json',
|
|
'Env var: CLAUDE_CONFIG_DIR',
|
|
].join('\n');
|
|
|
|
test('bare ~/.claude rewritten to ~/.windsurf (no trailing slash)', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'windsurf', WINDSURF_PATH_PREFIX);
|
|
assert.ok(
|
|
!/~\/\.claude(?![\w-])/.test(result),
|
|
`bare ~/.claude must be gone; got:\n${result}`,
|
|
);
|
|
assert.ok(
|
|
result.includes('~/.windsurf'),
|
|
`must contain normalized pathPrefix; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('bare $HOME/.claude rewritten to ~/.windsurf (install-path normalizes both home forms)', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'windsurf', WINDSURF_PATH_PREFIX);
|
|
assert.ok(
|
|
!/\$HOME\/\.claude(?![\w-])/.test(result),
|
|
`bare $HOME/.claude must be gone; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('zero surviving bare ~/.claude or $HOME/.claude refs in compound input', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'windsurf', WINDSURF_PATH_PREFIX);
|
|
const bareClaudePattern = /(?:~|\$HOME)\/\.claude(?![\w-])/;
|
|
assert.ok(
|
|
!bareClaudePattern.test(result),
|
|
`no bare ~/.claude / $HOME/.claude must survive; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('~/.claude-plugin is NOT corrupted to ~/.windsurf-plugin — was the \\b corruption', () => {
|
|
// FAIL-BEFORE: old /~\/\.claude\b/ rewrote ~/.claude-plugin → ~/.windsurf-plugin
|
|
// because \b fires between 'e' and '-'.
|
|
// PASS-AFTER: (?![\w-]) sees '-' and skips the match, preserving ~/.claude-plugin.
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'windsurf', WINDSURF_PATH_PREFIX);
|
|
assert.ok(
|
|
result.includes('~/.claude-plugin'),
|
|
`~/.claude-plugin must be preserved; got:\n${result}`,
|
|
);
|
|
assert.ok(
|
|
!result.includes('~/.windsurf-plugin'),
|
|
`~/.windsurf-plugin must NOT appear (was the \\b corruption); got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('slash form ~/.claude/ is also rewritten (pre-existing coverage)', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'windsurf', WINDSURF_PATH_PREFIX);
|
|
assert.ok(
|
|
!result.includes('~/.claude/'),
|
|
`slash form ~/.claude/ must be gone; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('CLAUDE_CONFIG_DIR is NOT rewritten by _applyRuntimeRewrites (converter responsibility)', () => {
|
|
// _applyRuntimeRewrites does NOT handle CLAUDE_CONFIG_DIR for windsurf;
|
|
// that rewrite is done by convertClaudeToWindsurfMarkdown at stage time.
|
|
// This test documents the boundary and guards against scope creep.
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'windsurf', WINDSURF_PATH_PREFIX);
|
|
assert.ok(
|
|
result.includes('CLAUDE_CONFIG_DIR'),
|
|
'CLAUDE_CONFIG_DIR is not rewritten by _applyRuntimeRewrites — that is converter scope',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── _applyRuntimeRewrites install-path tests (trae) ────────────────────────
|
|
//
|
|
// Trae had bare-form handling before this PR (via \b) and the converter uses
|
|
// (?![\w-]). The pre-existing \b in _applyRuntimeRewrites DOES corrupt
|
|
// .claude-plugin → .trae-plugin (known limitation, out of scope for #983).
|
|
// We document this here but do NOT assert preservation for trae, and we do NOT
|
|
// fix the pre-existing trae \b lines (that would be a separate concern).
|
|
//
|
|
// What we DO assert: trae bare ~/.claude / $HOME/.claude refs are rewritten
|
|
// (the install path cleans them), which is the core #983 fix for trae.
|
|
describe('_applyRuntimeRewrites(trae) — install-path bare-form (#983)', () => {
|
|
const TRAE_PATH_PREFIX = '$HOME/.trae/';
|
|
|
|
const TRAE_INPUT = [
|
|
'Config dir: ~/.claude',
|
|
'Also: $HOME/.claude',
|
|
'Slash form: ~/.claude/skills/foo.md',
|
|
// Note: .claude-plugin is intentionally omitted from assertions here because
|
|
// the pre-existing trae case uses \b which corrupts it (known limitation,
|
|
// out of scope for #983 — do not fix here).
|
|
].join('\n');
|
|
|
|
test('bare ~/.claude rewritten to $HOME/.trae (trae install path)', () => {
|
|
const result = _applyRuntimeRewrites(TRAE_INPUT, 'trae', TRAE_PATH_PREFIX);
|
|
assert.ok(
|
|
!/~\/\.claude(?![\w-])/.test(result),
|
|
`bare ~/.claude must be gone; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('bare $HOME/.claude rewritten to $HOME/.trae (trae install path)', () => {
|
|
const result = _applyRuntimeRewrites(TRAE_INPUT, 'trae', TRAE_PATH_PREFIX);
|
|
assert.ok(
|
|
!/\$HOME\/\.claude(?![\w-])/.test(result),
|
|
`bare $HOME/.claude must be gone; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('slash form ~/.claude/ also rewritten (trae install path)', () => {
|
|
const result = _applyRuntimeRewrites(TRAE_INPUT, 'trae', TRAE_PATH_PREFIX);
|
|
assert.ok(
|
|
!result.includes('~/.claude/'),
|
|
`slash form ~/.claude/ must be gone; got:\n${result}`,
|
|
);
|
|
});
|
|
});
|