* fix(#3011): actionable SDK-not-on-PATH diagnostic with shim location and shell-specific commands The previous diagnostic was a generic 'GSD SDK files are present but gsd-sdk is not on your PATH' message with no concrete path or shell-specific PATH-export command. Windows users reported that they couldn't find where the shim was written and didn't know how to add it to PATH for each shell (PowerShell vs cmd.exe vs Git Bash vs WSL all read PATH from different sources). New formatSdkPathDiagnostic({ shimDir, platform, runDir }) helper returns a typed IR: - shimLocationLine: explicit 'Shim written to: <path>' - actionLines: platform-specific PATH-export commands - Windows: 3 lines (PowerShell, cmd.exe, Git Bash with backslash->/ translation for bash compatibility) - POSIX: 1 line (export PATH=...) - npxNoteLines: 'you're running via npx ... npm install -g instead' when runDir is under an _npx cache segment (where the shim may be written to a temp dir that won't persist for the user's interactive shell) - isNpx, isWin32: structured booleans for assertions Renderer in install.js just emits each line. Tests assert on the typed IR fields directly (no source-grep, no console-output parsing). Tests: 12 cases across 5 suites covering Windows shell flavors (PowerShell preserves backslashes, Git Bash translates to forward), POSIX exports, null-shimDir fallback to npm install -g advice, npx detection on both path-separator conventions, and IR shape contract. Closes #3011 * fix(#3011): cmd.exe guidance uses powershell -Command, not setx CodeRabbit flagged the cmd.exe action line as a Major Windows correctness bug: setx PATH "${shimDir}; %PATH%" Two failure modes: 1. setx silently truncates the registry value above 1024 chars, permanently storing the truncated PATH and breaking applications until restored from the registry backup or fixed manually. 2. %PATH% expands to its current literal value at the moment setx runs, and the result is written as REG_SZ instead of REG_EXPAND_SZ. Lazy references like %SystemRoot% are baked in as literals, so future changes to those variables stop propagating. Replace with the same SetEnvironmentVariable call already used for the PowerShell line, invoked through `powershell -Command` so cmd.exe users get a safe command without us recommending two different APIs. * fix(#3011): escape shimDir for PowerShell, bash, and POSIX export CodeRabbit (Minor): a Windows username with a single quote (e.g. "C:\Users\O'Neil\AppData\Roaming\npm") would interpolate raw into the suggested commands, producing unparseable shell input the user can't fix without understanding the bug. Each shell context needs a different escape: - PowerShell single-quoted strings: '' is the literal-quote escape. Apply to both the PowerShell line and the cmd.exe line (which delegates to PowerShell). - Git Bash, where the path lives inside an outer single-quoted echo: '\'' (close-quote, escaped-quote, reopen-quote) embeds a literal single quote. The slash-conversion (\\ → /) still applies first. - POSIX export (Linux/macOS) inside double quotes: escape \, $, ", and backtick so the path is copied verbatim. $PATH lives outside the escape and still expands at paste time. Regression test: bug-3011-sdk-path-diagnostic.test.cjs locks in the expected escape sequence for all three shell flavors.
192 lines
7.9 KiB
JavaScript
192 lines
7.9 KiB
JavaScript
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* Regression test for #3011: SDK not found.
|
|
*
|
|
* Reporter (Windows / PowerShell 7) ran `npx get-shit-done-cc@latest`,
|
|
* upgrade reported success, but `gsd-sdk` could not be resolved by Claude
|
|
* Code, Git Bash, PowerShell, or WSL. The previous diagnostic was a
|
|
* generic "not on your PATH" with no actionable info; the user couldn't
|
|
* find where the shim was written or how to add it to PATH for each shell.
|
|
*
|
|
* Fix: formatSdkPathDiagnostic() returns a typed IR with the shim
|
|
* location, platform-specific PATH-export commands, and an npx-note
|
|
* when running under an `_npx` cache. The console renderer in install.js
|
|
* just emits each line; tests assert on the IR fields directly.
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
|
|
const { formatSdkPathDiagnostic } = require(path.join(__dirname, '..', 'bin', 'install.js'));
|
|
|
|
describe('Bug #3011: formatSdkPathDiagnostic — Windows shim location and PATH commands', () => {
|
|
test('emits shim location line when shimDir is provided', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: 'C:\\Users\\me\\AppData\\Roaming\\npm',
|
|
platform: 'win32',
|
|
runDir: 'C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\get-shit-done-cc\\bin',
|
|
});
|
|
assert.equal(ir.shimLocationLine,
|
|
'Shim written to: C:\\Users\\me\\AppData\\Roaming\\npm');
|
|
});
|
|
|
|
test('Windows action lines include all three shell flavors (PowerShell, cmd.exe, Git Bash)', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: 'C:\\Users\\me\\AppData\\Roaming\\npm',
|
|
platform: 'win32',
|
|
runDir: 'C:\\some\\path',
|
|
});
|
|
const labels = ir.actionLines.map(l => l.split(':')[0].trim());
|
|
assert.ok(labels.some(l => l === 'PowerShell'), `expected PowerShell line, got: ${JSON.stringify(labels)}`);
|
|
assert.ok(labels.some(l => l === 'cmd.exe'), `expected cmd.exe line, got: ${JSON.stringify(labels)}`);
|
|
assert.ok(labels.some(l => l === 'Git Bash'), `expected Git Bash line, got: ${JSON.stringify(labels)}`);
|
|
});
|
|
|
|
test('Git Bash command translates Windows backslashes to forward slashes', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: 'C:\\Users\\me\\AppData\\Roaming\\npm',
|
|
platform: 'win32',
|
|
runDir: 'C:\\some\\path',
|
|
});
|
|
const gitBash = ir.actionLines.find(l => l.startsWith('Git Bash'));
|
|
assert.ok(gitBash);
|
|
// Git Bash uses POSIX path syntax; backslashes would not work in bash.
|
|
assert.equal(gitBash.includes('\\'), false,
|
|
`Git Bash line must not contain backslashes: ${gitBash}`);
|
|
assert.ok(gitBash.includes('C:/Users/me/AppData/Roaming/npm'),
|
|
`Git Bash line must contain forward-slash path: ${gitBash}`);
|
|
});
|
|
|
|
test('PowerShell command preserves Windows backslashes in the embedded path', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: 'C:\\Users\\me\\AppData\\Roaming\\npm',
|
|
platform: 'win32',
|
|
runDir: 'C:\\some\\path',
|
|
});
|
|
const ps = ir.actionLines.find(l => l.startsWith('PowerShell'));
|
|
assert.ok(ps);
|
|
// PowerShell uses native Windows paths.
|
|
assert.ok(ps.includes('C:\\Users\\me\\AppData\\Roaming\\npm'),
|
|
`PowerShell line must contain Windows-style path: ${ps}`);
|
|
});
|
|
|
|
test("paths containing a single quote are escaped for each shell (#3014 CR)", () => {
|
|
// CR finding: a real Windows username like "O'Neil" would generate
|
|
// unparseable commands. PowerShell single-quote escape is '' (doubled);
|
|
// bash within outer single-quotes uses '\'' to embed a literal quote;
|
|
// POSIX export within double-quotes leaves single quotes alone.
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: "C:\\Users\\O'Neil\\AppData\\Roaming\\npm",
|
|
platform: 'win32',
|
|
runDir: 'C:\\some\\path',
|
|
});
|
|
const ps = ir.actionLines.find(l => l.startsWith('PowerShell'));
|
|
const cmd = ir.actionLines.find(l => l.startsWith('cmd.exe'));
|
|
const gitBash = ir.actionLines.find(l => l.startsWith('Git Bash'));
|
|
// PowerShell: literal quote escape is doubled
|
|
assert.ok(ps.includes("C:\\Users\\O''Neil\\AppData\\Roaming\\npm"),
|
|
`PowerShell line must double single quotes: ${ps}`);
|
|
// cmd.exe (which delegates to powershell) uses the same PS-escape
|
|
assert.ok(cmd.includes("C:\\Users\\O''Neil\\AppData\\Roaming\\npm"),
|
|
`cmd.exe line must double single quotes (delegates to PowerShell): ${cmd}`);
|
|
// Git Bash: '\'' escape inside outer single-quoted echo
|
|
assert.ok(gitBash.includes("C:/Users/O'\\''Neil/AppData/Roaming/npm"),
|
|
`Git Bash line must escape single quote with '\\\\'': ${gitBash}`);
|
|
});
|
|
});
|
|
|
|
describe('Bug #3011: formatSdkPathDiagnostic — POSIX action lines', () => {
|
|
test('emits a single export PATH line on Linux', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: '/home/me/.local/bin',
|
|
platform: 'linux',
|
|
runDir: '/home/me/.local/lib/node_modules/get-shit-done-cc/bin',
|
|
});
|
|
const exports_ = ir.actionLines.filter(l => l.startsWith('export PATH='));
|
|
assert.equal(exports_.length, 1, `expected 1 export line, got: ${JSON.stringify(ir.actionLines)}`);
|
|
assert.equal(exports_[0], 'export PATH="/home/me/.local/bin:$PATH"');
|
|
});
|
|
|
|
test('emits a single export PATH line on macOS (darwin)', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: '/usr/local/bin',
|
|
platform: 'darwin',
|
|
runDir: '/usr/local/lib/node_modules/get-shit-done-cc/bin',
|
|
});
|
|
assert.ok(ir.actionLines.some(l => l === 'export PATH="/usr/local/bin:$PATH"'));
|
|
});
|
|
});
|
|
|
|
describe('Bug #3011: formatSdkPathDiagnostic — fallback when shimDir is null', () => {
|
|
test('shimLocationLine is empty', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: null,
|
|
platform: 'win32',
|
|
runDir: 'C:\\some\\path',
|
|
});
|
|
assert.equal(ir.shimLocationLine, '');
|
|
});
|
|
|
|
test('action lines fall back to npm install -g advice', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: null,
|
|
platform: 'win32',
|
|
runDir: 'C:\\some\\path',
|
|
});
|
|
assert.ok(ir.actionLines.some(l => l.includes('npm install -g get-shit-done-cc')));
|
|
});
|
|
});
|
|
|
|
describe('Bug #3011: formatSdkPathDiagnostic — npx-cache detection', () => {
|
|
test('detects POSIX npx cache path', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: '/home/me/.local/bin',
|
|
platform: 'linux',
|
|
runDir: '/home/me/.npm/_npx/abc123/node_modules/get-shit-done-cc/bin',
|
|
});
|
|
assert.equal(ir.isNpx, true);
|
|
assert.ok(ir.npxNoteLines.length >= 2,
|
|
`expected npx note lines, got: ${JSON.stringify(ir.npxNoteLines)}`);
|
|
assert.ok(ir.npxNoteLines.some(l => l.includes('npx')));
|
|
assert.ok(ir.npxNoteLines.some(l => l.includes('npm install -g')));
|
|
});
|
|
|
|
test('detects Windows npx cache path', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: 'C:\\Users\\me\\AppData\\Roaming\\npm',
|
|
platform: 'win32',
|
|
runDir: 'C:\\Users\\me\\AppData\\Local\\npm-cache\\_npx\\abc123\\node_modules\\get-shit-done-cc\\bin',
|
|
});
|
|
assert.equal(ir.isNpx, true);
|
|
});
|
|
|
|
test('non-npx invocation leaves npxNoteLines empty', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: 'C:\\Users\\me\\AppData\\Roaming\\npm',
|
|
platform: 'win32',
|
|
runDir: 'C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\get-shit-done-cc\\bin',
|
|
});
|
|
assert.equal(ir.isNpx, false);
|
|
assert.deepEqual(ir.npxNoteLines, []);
|
|
});
|
|
});
|
|
|
|
describe('Bug #3011: formatSdkPathDiagnostic — actionable shape contract', () => {
|
|
test('returns the documented IR shape for any input', () => {
|
|
const ir = formatSdkPathDiagnostic({
|
|
shimDir: '/x',
|
|
platform: 'linux',
|
|
runDir: '/y',
|
|
});
|
|
assert.equal(typeof ir.shimLocationLine, 'string');
|
|
assert.ok(Array.isArray(ir.actionLines));
|
|
assert.ok(Array.isArray(ir.npxNoteLines));
|
|
assert.equal(typeof ir.isNpx, 'boolean');
|
|
assert.equal(typeof ir.isWin32, 'boolean');
|
|
});
|
|
});
|