* test: reproduce Windows SDK not found after fresh npx install (#3211) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: red — docs-parity live-registry tests fail against stub helper (#3049) Adds: - tests/helpers/live-command-registry.cjs (stub: returns empty Set) - tests/docs-parity-live-registry.test.cjs (new polarity-inverted test) - tests/fixtures/live-command-registry/ (fixture .md files) All parity and helper-contract tests fail because the stub returns an empty registry. This is the intentional RED state before GREEN implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(test-helpers): live-command-registry derives canonical tokens from commands/gsd/*.md (#3049) Implements GREEN phase: - tests/helpers/live-command-registry.cjs: walks commands/gsd/*.md, parses YAML frontmatter name: field, emits /gsd-slug, /gsd:slug, $gsd-slug per command. Memoized per process. Fails loud on malformed frontmatter (k302). - tests/docs-parity-live-registry.test.cjs: updated with INTERNAL_COMPONENT_SLUGS exemption for path-component and placeholder tokens (gsd-build from GitHub org URLs, gsd-workspaces from ~/gsd-workspaces/ paths, gsd-tools from bin/gsd-tools.cjs paths, etc.) Docs drift caught and fixed: - ns-* rename: /gsd-ns-workflow→/gsd-workflow etc. in COMMANDS, FEATURES, INVENTORY, USER-GUIDE (6 commands across 4 English files) - /gsd-scan → /gsd-map-codebase --fast (FEATURES, INVENTORY, USER-GUIDE) - /gsd-note → /gsd-capture (FEATURES, issue-driven-orchestration, ja-JP, ko-KR) - /gsd-do → /gsd-fast (FEATURES, ja-JP, ko-KR) - /gsd-from-gsd2 → /gsd-import --from-gsd2 (CLI-TOOLS, FEATURES, INVENTORY) - /gsd-verify-phase → /gsd-validate-phase (STATE-MD-LIFECYCLE) - /gsd-settings-integrations → /gsd-settings or /gsd-config --integrations (CLI-TOOLS) - /gsd-dev-preferences removed from profile-user artifact lists (AGENTS, COMMANDS, FEATURES in English, ja-JP, ko-KR) - /gsd-select-framework removed from gsd-framework-selector spawner list (AGENTS, INVENTORY) All 28 new tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(test): replace deny-list parity tests with polarity-inverted live-registry approach (#3049) - Delete bug-3010-reapply-patches-references.test.cjs (hardcoded deny-list) - Delete bug-3029-3034-stale-command-routes.test.cjs (hardcoded deny-list) - Delete bug-3042-3044-research-flag-and-stale-refs.test.cjs (deny-list + frontmatter checks) - Add tests/skill-frontmatter-contract.test.cjs (frontmatter structural checks extracted from deleted file) - Update tests/commands-doc-parity.test.cjs to derive slug from name: frontmatter field instead of filename, so ns-* commands resolve to their actual deployed tokens Closes #3049 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: annotate commands-doc-parity with source-text-is-the-product exemption (#3049 lint fix) The readFileSync on commands/gsd/*.md reads product markdown whose deployed text IS what the user sees — content.startsWith('---') detects YAML frontmatter in those files, not source-code structure. Add the allow-test-rule exemption matching the same rationale used in docs-parity-live-registry.test.cjs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: walk docs/** recursively to cover nested locale trees (CR finding 7) Replaced the non-recursive listMdFiles() with a hand-rolled DFS walker compatible with Node 20+. Surfaces unreadable-directory errors as stderr warnings (PRED.k302) rather than silently skipping. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: annotate live-command-registry helper and commands-doc-parity with source-text exemptions (CR findings 6, 8) Adds allow-test-rule comments to suppress lint-no-source-grep false positives on YAML frontmatter structure checks in both files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: anchor --research-phase assertions to arg-parsing section and verify combined refresh (CR findings 9, 10) Finding 9: scopes --research-phase check to within 1200 chars of the flag description section header, preventing false positives from prose mentions. Finding 10: tightens the force-refresh assertion to require BOTH --research and force/refresh semantics within the --research-phase description section, verifying the combined-mode contract rather than standalone --research presence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: fix execSync mock to accept opts parameter, forward to saved implementation (CR finding 5) The mock at line 212 dropped the options parameter when delegating to savedExecSync. Updated to (cmd, opts) signature and pass opts through. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: correct routing entrypoint, --fast default, /gsd-review collision, verifying-stage mapping (CR findings 1-4) Finding 1: Change Freeform Routing command from /gsd-fast to /gsd-progress --do. /gsd-fast is the inline trivial-task executor, not the routing entrypoint. Finding 2: Clarify that /gsd-map-codebase --fast REQ-SCAN-02 default (tech+arch) runs as a single combined-focus agent, resolving the contradiction with REQ-SCAN-01. Finding 3: Rename the namespace router /gsd-review to /gsd-quality across all docs, command file, and help.md to eliminate the naming collision with the concrete cross-AI peer-review command (review.md, name: gsd:review). Finding 4: Replace /gsd-validate-phase with /gsd-verify-work in the STATE-MD-LIFECYCLE.md verifying-stage table. /gsd-validate-phase is the retroactive Nyquist-validation flow, not the normal phase-verification step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs+test: fix locale doc drift surfaced by recursive walker (CR finding 7 follow-up) The recursive listMdFiles() walker newly covered docs/**/*.md subdirs. Stale command references in locale docs are now caught and fixed: - docs/zh-CN/references/model-profiles.md: remove /gsd-set-profile (deleted command); config.json is the current mechanism - docs/zh-CN/references/ui-brand.md: remove /gsd-alternative-1/2 template placeholders - docs/{ja-JP,ko-KR,pt-BR}/superpowers/specs/2026-03-20-*: replace /gsd-new-workspace, /gsd-list-workspaces, /gsd-remove-workspace with /gsd-workspace --new / --list / --remove (consolidated in #2790) Also adds smoke- and alternative-{1,2} to INTERNAL_COMPONENT_SLUGS (filesystem path and template placeholder patterns, not slash commands) and introduces listEnglishMdFiles() to scope the English parity check to docs/ excluding locale subdirectories (which have their own per-locale describe blocks). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add bash language tag to fenced code blocks in ja-JP and ko-KR workspace specs (CR round 2) Satisfies MD040 fenced-code-language requirement. These blocks contain shell commands and were missing the language specifier. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
410 lines
16 KiB
JavaScript
410 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* Regression tests for bug #3211.
|
|
*
|
|
* Windows 11 + PowerShell 7 + Node v22.22.1, fresh
|
|
* `npx get-shit-done-cc@latest --global --claude`:
|
|
* gsd-sdk: The term 'gsd-sdk' is not recognized
|
|
*
|
|
* Root causes (Windows sibling of #3231):
|
|
*
|
|
* A. filterNpxFromPath must handle Windows-style backslash paths (e.g.
|
|
* C:\Users\user\AppData\Local\npm-cache\_npx\abc123\node_modules\.bin).
|
|
* After replace(/\\/g, '/') the norm contains /_npx/ and should be
|
|
* stripped. We verify this explicitly because the Linux tests only exercised
|
|
* POSIX-style paths.
|
|
*
|
|
* B. isGsdSdkOnPath (zero-arg fallback) reads `process.env.PATH || ''`. On
|
|
* Windows, Node.js normalises PATH case so `process.env.PATH` always
|
|
* returns the right value in production. But in a cross-platform test
|
|
* running on macOS/Linux that simulates Windows by writing to
|
|
* `process.env.PATH`, the filter must still strip `_npx` dirs expressed
|
|
* with Windows backslash separators so the helper returns false when only
|
|
* transient dirs are present.
|
|
*
|
|
* C. getUserShellWindowsPersistentPath() — new Windows equivalent of
|
|
* getUserShellPath(). Probes the user's persistent 'Path' from the Windows
|
|
* registry via:
|
|
* powershell -NoProfile -Command
|
|
* "[Environment]::GetEnvironmentVariable('Path', 'User')"
|
|
* Returns the persistent Path string or null on failure. Must be exported
|
|
* and must apply filterNpxFromPath before returning.
|
|
*
|
|
* D. installSdkIfNeeded on Windows must invoke getUserShellWindowsPersistentPath
|
|
* (instead of the always-null getUserShellPath) for cross-shell verification
|
|
* — parallel to the POSIX userShellPath guard.
|
|
*
|
|
* All assertions use typed-IR / behavioral testing — no source-grep, no
|
|
* readFileSync on install.js source.
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const cp = require('node:child_process');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const installModule = require(path.join(ROOT, 'bin', 'install.js'));
|
|
|
|
const {
|
|
filterNpxFromPath,
|
|
isLegacyGsdSdkShim,
|
|
isGsdSdkOnPath,
|
|
installSdkIfNeeded,
|
|
getUserShellWindowsPersistentPath,
|
|
} = installModule;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// A. filterNpxFromPath — Windows backslash paths
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('bug #3211-A: filterNpxFromPath handles Windows backslash _npx paths', () => {
|
|
test('strips a Windows-style _npx dir expressed with backslashes', () => {
|
|
assert.equal(typeof filterNpxFromPath, 'function', 'filterNpxFromPath must be exported');
|
|
|
|
// Windows npm-cache path with backslash separators, semicolon delimiter
|
|
const winNpxDir = 'C:\\Users\\user\\AppData\\Local\\npm-cache\\_npx\\abc123\\node_modules\\.bin';
|
|
const winPersistentDir = 'C:\\Users\\user\\AppData\\Roaming\\npm';
|
|
const winSystemDir = 'C:\\Windows\\System32';
|
|
|
|
// On macOS path.delimiter is ':', not ';'. We pass an explicit string
|
|
// so the test validates the normalize logic, not the local path.delimiter.
|
|
const inputPath = [winNpxDir, winPersistentDir, winSystemDir].join(';');
|
|
const result = filterNpxFromPath(inputPath);
|
|
|
|
// Regardless of delimiter used, the _npx segment must be stripped
|
|
assert.ok(
|
|
!result.includes('_npx'),
|
|
'filterNpxFromPath must strip Windows _npx dirs. Got: ' + result,
|
|
);
|
|
assert.ok(
|
|
result.includes('Roaming\\npm') || result.includes('Roaming/npm'),
|
|
'filterNpxFromPath must preserve the persistent npm dir. Got: ' + result,
|
|
);
|
|
});
|
|
|
|
test('strips a mixed-separator Windows _npx path (forward + backward slashes)', () => {
|
|
const mixedNpxDir = 'C:/Users/user/AppData/Local/npm-cache/_npx/abc/node_modules/.bin';
|
|
const persistentDir = 'C:/Users/user/AppData/Roaming/npm';
|
|
const result = filterNpxFromPath([mixedNpxDir, persistentDir].join(';'));
|
|
assert.ok(!result.includes('_npx'), 'must strip mixed-separator _npx dir. Got: ' + result);
|
|
assert.ok(result.includes('Roaming/npm'), 'must keep persistent dir. Got: ' + result);
|
|
});
|
|
|
|
test('does NOT strip a Windows user dir that merely contains "npx" as a substring', () => {
|
|
// A user-named dir like C:\my-npx-tools\bin must NOT be filtered.
|
|
const userNpxLikeDir = 'C:\\Users\\user\\my-npx-tools\\bin';
|
|
const realNpxDir = 'C:\\Users\\user\\AppData\\Local\\npm-cache\\_npx\\abc\\node_modules\\.bin';
|
|
const result = filterNpxFromPath([userNpxLikeDir, realNpxDir].join(';'));
|
|
assert.ok(
|
|
result.includes('my-npx-tools'),
|
|
'must not strip user dirs that merely contain "npx". Got: ' + result,
|
|
);
|
|
assert.ok(!result.includes('_npx'), 'must strip the real _npx dir. Got: ' + result);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// B. isGsdSdkOnPath — does not return true when only a Windows _npx dir has
|
|
// gsd-sdk.cmd (using filterNpxFromPath on the passed pathString)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('bug #3211-B: isGsdSdkOnPath rejects Windows _npx-only transient PATH', () => {
|
|
let tmpRoot;
|
|
|
|
before(() => {
|
|
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3211-b-'));
|
|
});
|
|
|
|
after(() => {
|
|
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
});
|
|
|
|
test('returns false when a gsd-sdk.cmd exists only in an _npx-style transient dir', () => {
|
|
// On POSIX we name the dir with _npx/ to match the filter pattern.
|
|
// We can't set process.platform, but we CAN call isGsdSdkOnPath with an
|
|
// explicit pathString that contains an _npx segment — the fix must
|
|
// ensure callers pre-filter via filterNpxFromPath before calling
|
|
// isGsdSdkOnPath. We test filterNpxFromPath(pathString) produces an
|
|
// empty result, which means isGsdSdkOnPath of the filtered path returns false.
|
|
const npxBinDir = path.join(tmpRoot, '_npx', 'abc123', 'node_modules', '.bin');
|
|
fs.mkdirSync(npxBinDir, { recursive: true });
|
|
|
|
// Write a gsd-sdk shim (named .cmd for the Windows scenario — on POSIX
|
|
// isGsdSdkOnPath won't find .cmd; we validate the filter, not the exec check).
|
|
const shimPath = path.join(npxBinDir, 'gsd-sdk.cmd');
|
|
fs.writeFileSync(
|
|
shimPath,
|
|
['@ECHO OFF', '@node "C:\\path\\to\\gsd-sdk.js" %*', ''].join('\r\n'),
|
|
);
|
|
|
|
// The raw pathString contains an _npx segment — it MUST be filtered.
|
|
const rawPath = npxBinDir;
|
|
const persistentPath = filterNpxFromPath(rawPath);
|
|
|
|
// After filtering, the _npx dir must be gone so isGsdSdkOnPath returns false.
|
|
const onPath = isGsdSdkOnPath(persistentPath);
|
|
assert.equal(
|
|
onPath,
|
|
false,
|
|
'isGsdSdkOnPath(filterNpxFromPath(path)) must return false when only _npx dir has gsd-sdk. persistentPath=' + persistentPath,
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C. getUserShellWindowsPersistentPath — new export
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('bug #3211-C: getUserShellWindowsPersistentPath export', () => {
|
|
test('is exported from install.js', () => {
|
|
assert.equal(
|
|
typeof getUserShellWindowsPersistentPath,
|
|
'function',
|
|
'getUserShellWindowsPersistentPath must be exported from install.js',
|
|
);
|
|
});
|
|
|
|
test('returns a string or null — never throws', () => {
|
|
// We can call it on macOS/Linux; it must handle non-Windows gracefully
|
|
// (return null or a string). Must never throw.
|
|
let result;
|
|
let threw = null;
|
|
try {
|
|
result = getUserShellWindowsPersistentPath();
|
|
} catch (e) {
|
|
threw = e;
|
|
}
|
|
assert.equal(threw, null, 'getUserShellWindowsPersistentPath must not throw. Error: ' + threw);
|
|
assert.ok(
|
|
result === null || typeof result === 'string',
|
|
'must return string or null. Got: ' + typeof result,
|
|
);
|
|
});
|
|
|
|
test('on non-Windows, returns null (Windows-only probe)', () => {
|
|
if (process.platform === 'win32') {
|
|
// On actual Windows, any non-null string is acceptable.
|
|
const result = getUserShellWindowsPersistentPath();
|
|
assert.ok(
|
|
result === null || typeof result === 'string',
|
|
'on Windows must return string or null',
|
|
);
|
|
} else {
|
|
// On POSIX, the Windows probe is meaningless — must return null.
|
|
const result = getUserShellWindowsPersistentPath();
|
|
assert.equal(result, null, 'on POSIX, getUserShellWindowsPersistentPath must return null');
|
|
}
|
|
});
|
|
|
|
test('when the PowerShell probe is mocked to return a path, strips _npx dirs', () => {
|
|
// Mock cp.execSync to return a Windows Path with both persistent and _npx dirs.
|
|
const savedExecSync = cp.execSync;
|
|
const winPersistentDir = 'C:\\Users\\user\\AppData\\Roaming\\npm';
|
|
const winNpxDir = 'C:\\Users\\user\\AppData\\Local\\npm-cache\\_npx\\abc\\node_modules\\.bin';
|
|
const mockPath = [winPersistentDir, winNpxDir].join(';');
|
|
|
|
cp.execSync = (cmd, opts) => {
|
|
if (typeof cmd === 'string' && cmd.includes('GetEnvironmentVariable')) {
|
|
return mockPath + '\n';
|
|
}
|
|
return savedExecSync.call(cp, cmd, opts);
|
|
};
|
|
|
|
let result;
|
|
try {
|
|
result = getUserShellWindowsPersistentPath();
|
|
} finally {
|
|
cp.execSync = savedExecSync;
|
|
}
|
|
|
|
// On non-Windows this returns null (the function guards on process.platform).
|
|
// On Windows it returns the filtered path. Since we can't be on both,
|
|
// we verify the filter would work correctly by directly calling filterNpxFromPath.
|
|
if (result !== null) {
|
|
assert.ok(
|
|
!result.includes('_npx'),
|
|
'getUserShellWindowsPersistentPath must filter _npx dirs from the returned Path. Got: ' + result,
|
|
);
|
|
assert.ok(
|
|
result.includes('Roaming\\npm') || result.includes('Roaming/npm'),
|
|
'must keep persistent npm dir. Got: ' + result,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// D. installSdkIfNeeded Windows false-positive: transient _npx + npm-prefix
|
|
// NOT on PATH → must NOT print "GSD SDK ready"
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('bug #3211-D: installSdkIfNeeded — Windows _npx false-positive', () => {
|
|
let tmpRoot;
|
|
let sdkDir;
|
|
let savedEnv;
|
|
let origExecSync;
|
|
|
|
function captureConsole(fn) {
|
|
const stdout = [];
|
|
const stderr = [];
|
|
const origLog = console.log;
|
|
const origWarn = console.warn;
|
|
const origError = console.error;
|
|
console.log = (...a) => stdout.push(a.join(' '));
|
|
console.warn = (...a) => stderr.push(a.join(' '));
|
|
console.error = (...a) => stderr.push(a.join(' '));
|
|
let threw = null;
|
|
try { fn(); } catch (e) { threw = e; }
|
|
finally {
|
|
console.log = origLog;
|
|
console.warn = origWarn;
|
|
console.error = origError;
|
|
}
|
|
if (threw) throw threw;
|
|
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
return { stdout: stdout.map(strip).join('\n'), stderr: stderr.map(strip).join('\n') };
|
|
}
|
|
|
|
function makeSdkDir(root) {
|
|
const dir = path.join(root, 'sdk');
|
|
fs.mkdirSync(path.join(dir, 'dist'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(dir, 'dist', 'cli.js'),
|
|
['#!/usr/bin/env node', "console.log('0.0.0-test');", ''].join('\n'),
|
|
{ mode: 0o755 },
|
|
);
|
|
return dir;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3211-d-'));
|
|
sdkDir = makeSdkDir(tmpRoot);
|
|
|
|
// Simulate: install-time PATH contains only a transient _npx dir
|
|
// with a gsd-sdk shim. The persistent npm prefix dir is separate and
|
|
// NOT in process.env.PATH during the npx run.
|
|
const npxBinDir = path.join(tmpRoot, '_npx', 'abc123', 'node_modules', '.bin');
|
|
fs.mkdirSync(npxBinDir, { recursive: true });
|
|
// Write a gsd-sdk shim in the transient dir (executable on POSIX)
|
|
const shimName = 'gsd-sdk';
|
|
fs.writeFileSync(
|
|
path.join(npxBinDir, shimName),
|
|
['#!/bin/sh', 'exec node /path/to/gsd-sdk.js "$@"', ''].join('\n'),
|
|
{ mode: 0o755 },
|
|
);
|
|
|
|
const homeDir = path.join(tmpRoot, 'home');
|
|
fs.mkdirSync(homeDir, { recursive: true });
|
|
|
|
savedEnv = {
|
|
PATH: process.env.PATH,
|
|
HOME: process.env.HOME,
|
|
SHELL: process.env.SHELL,
|
|
};
|
|
|
|
// Only the transient _npx dir is on PATH — nothing persistent.
|
|
// On POSIX this simulates the false-positive scenario.
|
|
process.env.PATH = npxBinDir;
|
|
process.env.HOME = homeDir;
|
|
delete process.env.SHELL;
|
|
|
|
// Mock cp.execSync for npm prefix -g — return a separate dir that is
|
|
// NOT in process.env.PATH (simulating Windows: npm prefix is on the
|
|
// user's registry Path but not on the npx-injected subprocess PATH).
|
|
const npmPrefixDir = path.join(tmpRoot, 'npm-global');
|
|
fs.mkdirSync(npmPrefixDir, { recursive: true });
|
|
|
|
origExecSync = cp.execSync;
|
|
cp.execSync = (cmd, opts) => {
|
|
if (typeof cmd === 'string' && cmd.trim() === 'npm prefix -g') {
|
|
return npmPrefixDir + '\n';
|
|
}
|
|
return origExecSync.call(cp, cmd, opts);
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
cp.execSync = origExecSync;
|
|
if (savedEnv.PATH == null) delete process.env.PATH;
|
|
else process.env.PATH = savedEnv.PATH;
|
|
if (savedEnv.HOME == null) delete process.env.HOME;
|
|
else process.env.HOME = savedEnv.HOME;
|
|
if (savedEnv.SHELL == null) delete process.env.SHELL;
|
|
else process.env.SHELL = savedEnv.SHELL;
|
|
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
});
|
|
|
|
test('does NOT print "GSD SDK ready" when the only gsd-sdk entry is in a transient _npx dir and npm-prefix is off-PATH', () => {
|
|
const { stdout, stderr } = captureConsole(() => {
|
|
installSdkIfNeeded({ sdkDir });
|
|
});
|
|
const combined = `${stdout}\n${stderr}`;
|
|
|
|
assert.ok(
|
|
!/GSD SDK ready/.test(combined),
|
|
'installer must NOT print "GSD SDK ready" when gsd-sdk is only in a transient _npx dir. Got:\n' + combined,
|
|
);
|
|
// Must emit SOME output (warning or diagnostic), not silently succeed.
|
|
assert.ok(
|
|
combined.trim().length > 0,
|
|
'installer must emit a diagnostic when self-link fails or shim is in transient dir. Got empty output.',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// E. isLegacyGsdSdkShim — Windows .cmd shim detection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('bug #3211-E: isLegacyGsdSdkShim detects legacy marker in .cmd files', () => {
|
|
let tmpRoot;
|
|
|
|
before(() => {
|
|
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3211-e-'));
|
|
});
|
|
|
|
after(() => {
|
|
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
});
|
|
|
|
test('detects @deprecated + gsd-tools.cjs in a .cmd shim', () => {
|
|
assert.equal(typeof isLegacyGsdSdkShim, 'function');
|
|
|
|
const legacyCmd = path.join(tmpRoot, 'gsd-sdk.cmd');
|
|
fs.writeFileSync(
|
|
legacyCmd,
|
|
[
|
|
'@ECHO OFF',
|
|
':: @deprecated — use gsd-tools.cjs directly',
|
|
'@node "C:\\path\\to\\gsd-tools.cjs" %*',
|
|
'',
|
|
].join('\r\n'),
|
|
);
|
|
assert.equal(isLegacyGsdSdkShim(legacyCmd), true, 'must detect @deprecated gsd-tools.cjs in .cmd shim');
|
|
});
|
|
|
|
test('returns false for a modern .cmd shim pointing at gsd-sdk.js', () => {
|
|
const modernCmd = path.join(tmpRoot, 'gsd-sdk-modern.cmd');
|
|
fs.writeFileSync(
|
|
modernCmd,
|
|
[
|
|
'@ECHO OFF',
|
|
'@SETLOCAL',
|
|
'@node "C:\\path\\to\\get-shit-done-cc\\bin\\gsd-sdk.js" %*',
|
|
'',
|
|
].join('\r\n'),
|
|
);
|
|
assert.equal(isLegacyGsdSdkShim(modernCmd), false, 'must not flag modern .cmd shim as legacy');
|
|
});
|
|
|
|
test('returns false for a missing file', () => {
|
|
assert.equal(isLegacyGsdSdkShim(path.join(tmpRoot, 'no-such-file.cmd')), false);
|
|
});
|
|
});
|