From a1f4996d9af14d761c2351336451869e15a0fe6e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 30 May 2026 12:01:04 -0400 Subject: [PATCH] fix(#505): remove dead SDK-shim verification subsystem from bin/install.js (#515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#505): remove dead SDK-shim verification subsystem from bin/install.js Post-ADR-0174 the @opengsd/gsd-sdk package was retired; sdk/ no longer ships. installSdkIfNeeded had no callers in the live install flow and its entire transitive call graph (classifySdkInstall, buildSdkFailFastReport, renderSdkFailFastReport, buildGsdSdkVersionMismatchReport, readGsdSdkVersion, parseGsdSdkVersion, findGsdSdkOnPath, isGsdSdkOnPath, isLegacyGsdSdkShim, filterNpxFromPath, getUserShellPath, getUserShellWindowsPersistentPath, trySelfLinkGsdSdk, trySelfLinkGsdSdkWindows, buildWindowsShimTriple, formatSdkPathDiagnostic, renderGsdSdkVersionMismatchReport) was dead code. Also removed two now-empty test files (no-unconditional-win32-skip.test.cjs, bug-3020-install-shell-path-probe.test.cjs) that exercised the removed functions, and added a regression guard (bug-505-remove-dead-sdk-verification.test.cjs). detectStaleStandaloneSdk and formatStaleStandaloneSdkWarning are deliberately KEPT — they handle a real leftover-global-SDK condition (#3406). Co-Authored-By: Claude Opus 4.8 * chore: add changeset for #505 dead SDK-shim removal Co-Authored-By: Claude Opus 4.8 * fix(#505): restore buildWindowsShimTriple/formatSdkPathDiagnostic projection surfaces The dead-code removal also deleted buildWindowsShimTriple and formatSdkPathDiagnostic (plus their imports/exports). These have no production caller, but they are the install.js side of a projection-contract drift guard: tests/bug-3441 and tests/bug-3442 assert install.js delegates to shell-command-projection.cjs rather than hand-rolling the projection. Removing them broke those tests (TypeError: ... is not a function) — surfaced by the full/coverage CI matrix, which runs suites the local scoped run skipped. Restore the two thin wrappers, their `*FromProjection` import aliases, and their exports. Update the bug-505 guard test to assert they remain exported as contract surfaces (moved out of the dead-symbol list). Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/calm-tigers-click.md | 5 + bin/install.js | 828 +----------------- ...bug-3020-install-shell-path-probe.test.cjs | 169 ---- ...-505-remove-dead-sdk-verification.test.cjs | 86 ++ tests/no-unconditional-win32-skip.test.cjs | 70 -- 5 files changed, 99 insertions(+), 1059 deletions(-) create mode 100644 .changeset/calm-tigers-click.md delete mode 100644 tests/bug-3020-install-shell-path-probe.test.cjs create mode 100644 tests/bug-505-remove-dead-sdk-verification.test.cjs delete mode 100644 tests/no-unconditional-win32-skip.test.cjs diff --git a/.changeset/calm-tigers-click.md b/.changeset/calm-tigers-click.md new file mode 100644 index 000000000..8fa6c8e95 --- /dev/null +++ b/.changeset/calm-tigers-click.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 515 +--- +Remove dead SDK-shim verification subsystem (installSdkIfNeeded and 17 transitively-called functions) from bin/install.js. Post-ADR-0174 sdk/ no longer ships; the subsystem had no live callers and could produce misleading 'tarball is missing sdk/dist/' errors. detectStaleStandaloneSdk and formatStaleStandaloneSdkWarning are kept (handle real stale-global-SDK condition). diff --git a/bin/install.js b/bin/install.js index e33bb4c81..2eecb8e6f 100755 --- a/bin/install.js +++ b/bin/install.js @@ -10692,341 +10692,6 @@ function maybeSuggestPathExport(globalBin, homeDir) { console.log(''); } -/** - * Verify the prebuilt SDK dist is present and the gsd-sdk shim is wired up. - * - * As of fix/2441-sdk-decouple, sdk/dist/ is shipped prebuilt inside the - * @opengsd/get-shit-done-redux npm tarball. The parent package declares a bin entry - * "gsd-sdk": "bin/gsd-sdk.js" so npm chmods the shim correctly when - * installing from a packed tarball — eliminating the mode-644 failure - * (issue #2453) and the build-from-source failure modes (#2439, #2441). - * - * This function verifies the invariant: sdk/dist/cli.js exists and is - * executable. If the execute bit is missing (possible in dev/clone setups - * where sdk/dist was committed without +x), we fix it in-place. - * - * --no-sdk skips the check entirely (back-compat). - * --sdk forces the check even if it would otherwise be skipped. - */ -/** - * Classify the install context for the SDK directory. - * - * Distinguishes three shapes the installer must handle differently when - * `sdk/dist/` is missing: - * - * - `tarball` + `npxCache: true` - * User ran `npx @opengsd/get-shit-done-redux@latest`. sdk/ lives under - * `/_npx//node_modules/@opengsd/get-shit-done-redux/sdk` which - * is treated as read-only by npm/npx on Windows (#2649). We MUST - * NOT attempt a nested `npm install` there — it will fail with - * EACCES/EPERM and produce the misleading "Failed to npm install - * in sdk/" error the user reported. Point at the global upgrade. - * - * - `tarball` + `npxCache: false` - * User ran a global install (`npm i -g @opengsd/get-shit-done-redux`). sdk/dist - * ships in the published tarball; if it's missing, the published - * artifact itself is broken (see #2647). Same user-facing fix: - * upgrade to latest. - * - * - `dev-clone` - * Developer running from a git clone. Keep the existing "cd sdk && - * npm install && npm run build" hint — the user is expected to run - * that themselves. The installer itself never shells out to npm. - * - * Detection heuristics are path-based and side-effect-free: we look for - * `_npx` and `node_modules` segments that indicate a packaged install, - * and for a `.git` directory nearby that indicates a clone. A best-effort - * write probe detects read-only filesystems (tmpfile create + unlink); - * probe failures are treated as read-only. - */ -function classifySdkInstall(sdkDir) { - const path = require('path'); - const fs = require('fs'); - const segments = sdkDir.split(/[\\/]+/); - const npxCache = segments.includes('_npx'); - const inNodeModules = segments.includes('node_modules'); - const parent = path.dirname(sdkDir); - const hasGitNearby = fs.existsSync(path.join(parent, '.git')); - - let mode; - if (hasGitNearby && !npxCache && !inNodeModules) { - mode = 'dev-clone'; - } else if (npxCache || inNodeModules) { - mode = 'tarball'; - } else { - mode = 'dev-clone'; - } - - let readOnly = npxCache; // assume true for npx cache - if (!readOnly) { - try { - const probe = path.join(sdkDir, `.gsd-write-probe-${process.pid}`); - fs.writeFileSync(probe, ''); - fs.unlinkSync(probe); - } catch { - readOnly = true; - } - } - - return { mode, npxCache, readOnly }; -} - -/** - * #2974: pure builder for the SDK fail-fast report. Returns a structured IR - * with everything the renderer needs PLUS everything tests need to assert - * on. Tests can call `buildSdkFailFastReport(sdkDir, sdkCliPath)` directly - * and assert on `report.reason`, `report.context`, `report.fix_command` - * etc. without intercepting console.error or matching against rendered - * text. - * - * Shape (frozen contract — extending requires a new test): - * { - * ok: false, - * reason: 'sdk_fail_fast', // ERROR_REASON.SDK_FAIL_FAST - * context: 'npx-cache' | 'tarball' | 'dev-clone', - * missing_path: '/sdk/dist/cli.js', - * missing_artifact: 'sdk/dist', - * fix_command: 'npm install -g @opengsd/get-shit-done-redux@latest' | 'cd sdk && npm install && npm run build', - * attempted_nested_install: false, // contract: never true - * } - */ -function buildSdkFailFastReport(sdkDir, sdkCliPath) { - const ctx = classifySdkInstall(sdkDir); - let context, fix_command; - if (ctx.mode === 'tarball') { - context = ctx.npxCache ? 'npx-cache' : 'tarball'; - fix_command = 'npm install -g @opengsd/get-shit-done-redux@latest'; - } else { - context = 'dev-clone'; - fix_command = 'cd sdk && npm install && npm run build'; - } - return { - ok: false, - reason: 'sdk_fail_fast', - context, - missing_path: sdkCliPath, - missing_artifact: 'sdk/dist', - fix_command, - attempted_nested_install: false, - }; -} - -/** - * Renderer for the structured fail-fast report. Text formatting only — - * tests never call this. Splits the IR fields back into the same human- - * readable lines the previous shape produced. - */ -function renderSdkFailFastReport(ir) { - const bar = '━'.repeat(72); - const redBold = `${red}${bold}`; - console.error(''); - console.error(`${redBold}${bar}${reset}`); - console.error(`${redBold} ✗ GSD SDK dist not found — /gsd-* commands will not work${reset}`); - console.error(`${redBold}${bar}${reset}`); - console.error(` ${red}Reason:${reset} ${ir.missing_artifact}/cli.js not found at ${ir.missing_path}`); - console.error(''); - if (ir.context === 'npx-cache') { - console.error(` Detected read-only npx cache install (${dim}${path.dirname(ir.missing_path).replace(/\/dist$/, '')}${reset}).`); - console.error(` The installer will ${bold}not${reset} attempt \`npm install\` inside the npx cache.`); - console.error(''); - console.error(` Fix: install a version that ships sdk/dist/ globally:`); - console.error(` ${cyan}${ir.fix_command}${reset}`); - console.error(` Or, if you prefer a one-shot run, clear the npx cache first:`); - console.error(` ${cyan}npx --yes @opengsd/get-shit-done-redux@latest${reset}`); - console.error(` Or build from source (git clone):`); - console.error(` ${cyan}git clone https://github.com/open-gsd/get-shit-done-redux && cd get-shit-done/sdk && npm install && npm run build${reset}`); - } else if (ir.context === 'tarball') { - console.error(` The published tarball appears to be missing sdk/dist/ (see #2647).`); - console.error(''); - console.error(` Fix: install a version that ships sdk/dist/ globally:`); - console.error(` ${cyan}${ir.fix_command}${reset}`); - console.error(` Or build from source (git clone):`); - console.error(` ${cyan}git clone https://github.com/open-gsd/get-shit-done-redux && cd get-shit-done/sdk && npm install && npm run build${reset}`); - } else { - console.error(` Running from a git clone — build the SDK first:`); - console.error(` ${cyan}${ir.fix_command}${reset}`); - } - console.error(`${redBold}${bar}${reset}`); - console.error(''); -} - -function installSdkIfNeeded(opts) { - opts = opts || {}; - if (hasNoSdk && !opts.sdkDir) { - console.log(`\n ${dim}Skipping GSD SDK check (--no-sdk)${reset}`); - return; - } - - const path = require('path'); - const fs = require('fs'); - - const sdkDir = opts.sdkDir || path.resolve(__dirname, '..', 'sdk'); - const sdkCliPath = path.join(sdkDir, 'dist', 'cli.js'); - - // #2678 / #2829: local installs do not write to global node_modules, so we - // cannot fall through to the global-install error path. But the parent - // package (which carries bin/gsd-sdk.js and sdk/dist/cli.js) IS available - // wherever the installer is running from — npx cache, npm-global, or git - // clone. The shim resolves sdk/dist/cli.js relative to its own __dirname, - // so a self-link into a user-writable PATH dir makes `gsd-sdk` callable - // from local-mode installs too. Only when the dist is genuinely missing - // do we bail out with a non-fatal warning. - // - // #3033: --sdk (opts.forceSdk) overrides the local-install early-return — - // the user explicitly requested SDK deployment, so treat the missing-dist - // case like a global install (fail fast with an actionable diagnostic) - // instead of silently skipping. - if (opts.isLocal && !opts.forceSdk && !fs.existsSync(sdkCliPath)) { - console.warn(`\n ${yellow}⚠${reset} Skipping SDK check for local install — sdk/dist/cli.js not found at ${sdkCliPath}.`); - return; - } - - if (!fs.existsSync(sdkCliPath)) { - const ir = buildSdkFailFastReport(sdkDir, sdkCliPath); - renderSdkFailFastReport(ir); - if (opts.throwOnFailure) { - const error = new Error(`GSD SDK prebuilt artifact missing: ${sdkCliPath}`); - error.code = 'GSD_SDK_MISSING_DIST'; - error.exitCode = 1; - throw error; - } - process.exit(1); - } - - // Ensure execute bit is set. tsc emits files at 0o644; git clone preserves - // whatever mode was committed. Fix in-place so node-invoked paths work too. - try { - const stat = fs.statSync(sdkCliPath); - const isExecutable = !!(stat.mode & 0o111); - if (!isExecutable) { - fs.chmodSync(sdkCliPath, stat.mode | 0o111); - } - } catch { - // Non-fatal: if chmod fails (e.g. read-only fs) the shim still works via - // `node sdkCliPath` invocation in bin/gsd-sdk.js. - } - - // #2775: do not assert "GSD SDK ready" until `gsd-sdk` actually resolves on - // PATH. `npx @opengsd/get-shit-done-redux` only links the package's primary bin; the - // secondary `gsd-sdk` shim is left dangling under the npx cache and is NOT - // callable as a bare command. The previous file-presence-only check was a - // strictly weaker invariant than the one workflows depend on - // (`command -v gsd-sdk` resolving), and led to a false ✓ in npx-cache - // installs (issue #2775). - // - // #3231: strip transient npx-injected PATH segments before checking. The - // installer subprocess PATH includes `~/.npm/_npx//node_modules/.bin` - // which is ephemeral — it is NOT reachable from the user's interactive - // shell. A gsd-sdk found there must NOT count as "on PATH". - const shimSrc = path.resolve(__dirname, 'gsd-sdk.js'); - const persistentPath = filterNpxFromPath(process.env.PATH || ''); - let resolvedSdkPath = findGsdSdkOnPath(persistentPath); - let onPath = !!resolvedSdkPath; - - // Track WHERE we wrote the shim so the diagnostic can be specific even - // when isGsdSdkOnPath() returns false because the write target isn't on - // PATH (#3011: Windows users hit this when npm's global bin dir is - // populated but not on every shell's PATH — Git Bash vs PowerShell vs - // cmd.exe each read PATH from different sources). - let shimDir = null; - if (!onPath) { - // Try to materialize the shim into a user-writable PATH location so the - // installer can deliver on the success message without requiring the user - // to run `npm install -g` separately. Picks the first PATH entry that - // looks like a user-owned bin dir; falls back to ~/.local/bin even if - // it's not on PATH (then a follow-up suggestion is printed). - const linked = trySelfLinkGsdSdk(shimSrc); - if (linked) { - shimDir = path.dirname(linked); - resolvedSdkPath = findGsdSdkOnPath(persistentPath); - onPath = !!resolvedSdkPath; - if (onPath) { - console.log(` ${dim}↪ linked gsd-sdk → ${linked}${reset}`); - } - } - } - - // #3020: cross-shell PATH verification. Even when the install-time - // process.env.PATH walk found the shim, the user's later interactive - // shells may have a different PATH — Windows cross-shell .cmd/no-ext - // mismatch, POSIX ~/.local/bin missing from login shell, or node- - // version-manager PATH shims. Probe the user's login shell PATH and - // require the shim to be reachable there too before claiming ✓. - // - // #3211 (Windows): getUserShellWindowsPersistentPath() reads the user-level - // 'Path' registry key via PowerShell — the correct cross-shell source on - // Windows (Git Bash, PowerShell, and cmd.exe all inherit it). Returns null - // when PowerShell is unavailable or the probe times out. - // - // #3231: when getUserShellPath() / getUserShellWindowsPersistentPath() - // returns null (probe failed or unavailable), we cannot confirm persistent - // reachability. Since we already filtered npx dirs from persistentPath above, - // onPath=true means a non-transient dir has the shim — that is the best - // available invariant and is sufficient to claim ✓. - const userShellPath = process.platform === 'win32' - ? getUserShellWindowsPersistentPath() - : getUserShellPath(); - if (onPath && userShellPath !== null) { - // filterNpxFromPath is applied inside getUserShellWindowsPersistentPath - // (Windows) and here for the POSIX case. - const persistentUserShellPath = process.platform === 'win32' - ? userShellPath // already filtered by getUserShellWindowsPersistentPath - : filterNpxFromPath(userShellPath); - const userSdkPath = findGsdSdkOnPath(persistentUserShellPath); - if (!userSdkPath) { - onPath = false; - resolvedSdkPath = null; - } else { - resolvedSdkPath = userSdkPath; - } - } - // If userShellPath is null (probe failed or unavailable), onPath reflects - // the persistent-PATH check — that is the best available invariant. - - if (onPath) { - const versionReport = buildGsdSdkVersionMismatchReport(resolvedSdkPath, pkg.version, { isLocal: !!opts.isLocal }); - if (versionReport) { - renderGsdSdkVersionMismatchReport(versionReport); - } else { - console.log(` ${green}✓${reset} GSD SDK ready (sdk/dist/cli.js)`); - } - } else { - // #3011: actionable diagnostic. The previous shape printed a generic - // "not on your PATH" message that didn't tell the user where to look. - // formatSdkPathDiagnostic produces a typed IR that we then render to - // stdout; tests assert on the IR (no source-grep, no console capture). - const ir = formatSdkPathDiagnostic({ - shimDir, - platform: process.platform, - runDir: __dirname, - }); - console.log(''); - console.log(` ${yellow}⚠${reset} GSD SDK files are present but ${bold}gsd-sdk${reset} is not on your PATH.`); - console.log(` Workflows that call ${cyan}gsd-sdk query …${reset} will fail with "command not found".`); - if (ir.shimLocationLine) console.log(` ${ir.shimLocationLine}`); - for (const line of ir.actionLines) console.log(` ${line}`); - if (ir.npxNoteLines.length > 0) { - for (const line of ir.npxNoteLines) console.log(` ${line}`); - } - console.log(''); - } - - // #2620: warn if npm's global bin is not on PATH, suppressing the - // absolute-path suggestion when the user's rc already covers it via - // a HOME-relative entry (e.g. `export PATH="$HOME/.npm-global/bin:$PATH"`). - try { - const cp = require('child_process'); - const npmPrefix = cp.execSync('npm prefix -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); - if (npmPrefix) { - // On Windows npm prefix IS the bin dir; on POSIX it's `${prefix}/bin`. - const globalBin = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin'); - maybeSuggestPathExport(globalBin, os.homedir()); - } - } catch { - // npm not available / exec failed — silently skip the PATH advice. - } -} - /** * #3406 helper: detect a stale globally-installed `@opengsd/gsd-sdk` package * shadowing the `gsd-sdk` shim that `@opengsd/get-shit-done-redux` installs. @@ -11101,483 +10766,20 @@ function formatStaleStandaloneSdkWarning(info) { ].join('\n'); } -/** - * #3231 helper: detect whether a `gsd-sdk` binary is the legacy deprecated - * shim pointing at `gsd-tools.cjs`. - * - * Reads the first 512 bytes of the file and looks for the `@deprecated` - * marker alongside a `gsd-tools.cjs` reference — the fingerprint that - * distinguishes the old binary from the modern SDK. Treats any I/O error - * (missing file, EACCES) as "not legacy" so callers do not need to guard. - * - * This is intentionally a plain-text sniff of the file header, not a - * semantic parse — the marker is a stable, human-authored string that we - * own. Returns false conservatively (prefer false positives to false - * negatives: a non-legacy binary reported as legacy triggers a harmless - * replacement; a legacy binary reported as non-legacy would keep the broken - * shim in place). - */ -function isLegacyGsdSdkShim(filePath) { - const fs = require('fs'); - try { - const fd = fs.openSync(filePath, 'r'); - let header; - try { - const buf = Buffer.alloc(512); - const bytesRead = fs.readSync(fd, buf, 0, 512, 0); - header = buf.slice(0, bytesRead).toString('utf8'); - } finally { - try { fs.closeSync(fd); } catch {} - } - // The legacy binary contains "@deprecated" AND "gsd-tools.cjs" within - // its first 512 bytes. - return header.includes('@deprecated') && header.includes('gsd-tools.cjs'); - } catch { - return false; - } -} - -/** - * #3231 helper: strip transient npx-injected PATH segments. - * - * npm/npx injects `~/.npm/_npx//node_modules/.bin` (and equivalents) - * into the installer subprocess PATH. Those directories are ephemeral — they - * exist only for the duration of the `npx` run — and MUST NOT be treated as - * evidence that `gsd-sdk` is durably reachable. - * - * Strips any segment whose absolute form contains `/_npx/` or `\\_npx\\` - * as a proper path-component boundary. A user-named directory that merely - * contains the substring "npx" (e.g. `/home/user/my-npx-scripts/bin`) is - * preserved: we require the boundary characters (`/` or `\`) on both sides. - * - * Returns the filtered PATH string (may be empty if all segments were npx). - */ -function filterNpxFromPath(pathString) { - const path = require('path'); - const input = typeof pathString === 'string' ? pathString : (process.env.PATH || ''); - return input - .split(path.delimiter) - .filter((seg) => { - if (!seg) return false; - // Normalize to forward-slash form for the pattern check so both - // POSIX and Windows paths match a single expression. The sep-anchored - // pattern avoids matching "my-npx-scripts" etc. - const norm = seg.replace(/\\/g, '/'); - // Must have /_npx/ as a real path component, not just a substring. - return !norm.includes('/_npx/'); - }) - .join(path.delimiter); -} - -/** - * #2775 helper: find a callable `gsd-sdk` on a PATH. - * - * Pure PATH walk (no spawn) — we look for a regular file or symlink named - * `gsd-sdk` (or `gsd-sdk.cmd`/`.exe` on Windows) in any directory on PATH and - * verify it carries the execute bit on POSIX. Avoids paying spawn cost and - * avoids the chicken-and-egg of needing to run the not-yet-installed binary. - * - * #3020: accepts an optional explicit PATH string. The install subprocess's - * process.env.PATH is not the same set the user's later interactive shells - * see (Windows cross-shell, POSIX ~/.local/bin, node-version-manager - * shims). Callers can pass the user-shell PATH from getUserShellPath() to - * verify the shim is reachable from the runtime shell, not just the - * install context. Zero-arg form preserves existing behavior. - * - * #3231: a candidate that passes the file/exec check is further tested via - * isLegacyGsdSdkShim — a symlink pointing at the deprecated gsd-tools.cjs - * binary must NOT be treated as "on PATH" even if it is executable. - */ -function findGsdSdkOnPath(pathString) { - const path = require('path'); - const fs = require('fs'); - // Type-guard the explicit input (#3028 CR): callers may pass null - // (getUserShellPath() can return null), and `null.split()` throws. - // Only honor pathString when it's a string; fall back otherwise. - const pathEnv = typeof pathString === 'string' ? pathString : (process.env.PATH || ''); - const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : ['']; - for (const seg of pathEnv.split(path.delimiter)) { - if (!seg) continue; - for (const ext of exts) { - const candidate = path.join(seg, `gsd-sdk${ext}`); - try { - const st = fs.statSync(candidate); - if (st.isFile()) { - if (process.platform === 'win32') { - if (!isLegacyGsdSdkShim(candidate)) return candidate; - } else if ((st.mode & 0o111) !== 0) { - // #3231: resolve symlink before sniffing, so we detect legacy - // through any level of indirection. - let target = candidate; - try { target = fs.realpathSync(candidate); } catch {} - if (!isLegacyGsdSdkShim(target)) return candidate; - } - } - } catch { - // missing / EACCES on dir — keep scanning. - } - } - } - return null; -} - -function isGsdSdkOnPath(pathString) { - return !!findGsdSdkOnPath(pathString); -} - -function parseGsdSdkVersion(text) { - const match = String(text || '').match(/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/); - return match ? match[1] : null; -} - -function readGsdSdkVersion(sdkPath) { - if (!sdkPath) return null; - const cp = require('child_process'); - try { - const isWindowsCommandShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(String(sdkPath)); - const result = cp.spawnSync(isWindowsCommandShim ? 'cmd.exe' : sdkPath, isWindowsCommandShim ? ['/c', sdkPath, '--version'] : ['--version'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 2000, - env: process.env, - }); - if (result.error || result.status !== 0) return null; - return parseGsdSdkVersion(`${result.stdout || ''}\n${result.stderr || ''}`); - } catch { - return null; - } -} - -function buildGsdSdkVersionMismatchReport(sdkPath, expectedVersion, opts) { - const actualVersion = readGsdSdkVersion(sdkPath); - if (!actualVersion || !expectedVersion) return null; - if (actualVersion === expectedVersion) return null; - // #3668: local installs should NOT be told to run `npm install -g` — - // that contradicts the intent of --local (self-contained, no global dep). - // For local installs the correct remediation is to re-run the local - // installer (e.g. `npx get-shit-done-cc@latest --claude --local`). - const isLocal = opts && opts.isLocal; - const fix_command = isLocal - ? 'npx @opengsd/get-shit-done-redux@latest --claude --local' - : 'npm install -g @opengsd/get-shit-done-redux@latest'; - return { - ok: false, - reason: 'gsd_sdk_version_mismatch', - sdk_path: sdkPath, - actual_version: actualVersion, - expected_version: expectedVersion, - fix_command, - is_local: !!isLocal, - }; -} - -function renderGsdSdkVersionMismatchReport(ir) { - console.log(''); - console.log(` ${yellow}⚠${reset} ${bold}gsd-sdk version mismatch${reset} — PATH resolves a stale SDK.`); - console.log(` Resolved gsd-sdk: ${ir.sdk_path}`); - console.log(` Resolved version: ${ir.actual_version}`); - console.log(` Installer version: ${ir.expected_version}`); - console.log(` Workflows that call ${cyan}gsd-sdk query …${reset} will use the stale executable first.`); - console.log(` Fix: ${cyan}${ir.fix_command}${reset}`); - console.log(` Or remove the stale global install / adjust PATH so the current shim is first.`); - console.log(''); -} - -/** - * #3020: probe the user's login shell to learn the PATH that will be - * visible at workflow runtime. - * - * The install subprocess inherits process.env.PATH from npm/npx, which - * may include directories the user's interactive shells do not (e.g. - * ~/.local/bin auto-injected by npm-prefix tooling, or nvm-shimmed - * paths). Asserting `gsd-sdk` is on the install-subprocess PATH is a - * weaker invariant than the runtime contract — workflows shell out via - * `bash -c "gsd-sdk …"`, and that bash inherits PATH from the user's - * login shell. - * - * Uses `$SHELL -lc 'printf %s "$PATH"'` on POSIX. Returns null on Windows - * (the Windows counterpart is getUserShellWindowsPersistentPath, which reads - * the user-level 'Path' registry key via PowerShell). Returns null - * when $SHELL is unset, when the spawn fails, or when the result is - * empty — callers must fall back to process.env.PATH in those cases. - * - * Synchronous so it can be called from the existing post-install check - * without restructuring the whole flow as async. - */ -function getUserShellPath() { - if (process.platform === 'win32') return null; - const shellEnv = typeof process.env.SHELL === 'string' ? process.env.SHELL : ''; - if (!shellEnv) return null; - const cp = require('child_process'); - try { - const out = cp.execFileSync(shellEnv, ['-lc', 'printf %s "$PATH"'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - // 2-second cap so a misconfigured rc file (e.g. interactive prompt) - // can't hang the install. The probe is best-effort — null on timeout - // is the safe fallback. - timeout: 2000, - }); - // #3028 CR: login startup scripts can print banners / motd / stale - // log lines BEFORE the printf, polluting stdout. Take the LAST - // non-empty line as the PATH candidate so noise doesn't flip the - // cross-shell check to false. PATH itself is single-line. - const lines = String(out || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean); - const candidate = lines.length > 0 ? lines[lines.length - 1] : ''; - return candidate.length > 0 ? candidate : null; - } catch { - return null; - } -} - -/** - * #3211: Windows counterpart to getUserShellPath(). Probes the effective - * persistent Path from the Windows registry via PowerShell by merging - * Machine-level + User-level entries: - * - * $m=[Environment]::GetEnvironmentVariable('Path','Machine') - * $u=[Environment]::GetEnvironmentVariable('Path','User') - * ($m + ';' + $u).Trim(';') - * - * This is the correct primitive for Windows cross-shell PATH verification — - * Git Bash, PowerShell, and cmd.exe all inherit the effective (Machine;User) - * registry Path, while the install-subprocess process.env.PATH is polluted - * with transient npx entries and may not include directories added by the - * user post-install. Reading only User-level Path would produce a false - * warning when gsd-sdk is in a machine-level bin dir (e.g. C:\Program Files\nodejs). - * - * Returns the filtered persistent Path string (npx segments stripped) or null - * on any failure (non-Windows, PowerShell not available, spawn timeout, empty - * result). Callers must treat null as "check unavailable — trust install-time - * filtered PATH". - * - * Synchronous, 2-second timeout, best-effort — safe to call from - * installSdkIfNeeded without restructuring to async. - */ -function getUserShellWindowsPersistentPath() { - if (process.platform !== 'win32') return null; - const cp = require('child_process'); - // Use the same execFileSync form as getUserShellPath() above — static - // literal args, no user input, no injection vector. - const execFile = cp.execFileSync.bind(cp); - try { - // Read Machine + User Path and merge them — the effective PATH that - // PowerShell, cmd.exe, and Git Bash inherit is Machine;User (machine - // entries first). Reading only User-level Path would produce a false - // warning when gsd-sdk is installed in a machine-level bin dir - // (e.g. C:\Program Files\nodejs). - const out = execFile( - 'powershell.exe', - [ - '-NoProfile', - '-Command', - "$u=[Environment]::GetEnvironmentVariable('Path','User');" + - "$m=[Environment]::GetEnvironmentVariable('Path','Machine');" + - "[Console]::Out.Write(($m + ';' + $u).Trim(';'))", - ], - { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - // 2-second cap — a locked registry or slow profile can't hang the install. - timeout: 2000, - }, - ); - // Take the last non-empty line so any motd/banner noise before the output - // doesn't corrupt the result — same defensive pattern as getUserShellPath. - const lines = String(out || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean); - const candidate = lines.length > 0 ? lines[lines.length - 1] : ''; - if (!candidate) return null; - // Strip transient npx dirs from the persistent Path before returning — - // the registry can accumulate stale _npx entries from prior runs. - const filtered = filterNpxFromPath(candidate); - return filtered.length > 0 ? filtered : null; - } catch { - return null; - } -} - -/** - * #2775 helper: attempt to materialize the `gsd-sdk` shim at a user-writable - * PATH location. Returns the absolute path created on success, or null if no - * suitable location was usable. - * - * Strategy (POSIX): prefer ~/.local/bin (creating it if absent — many distros - * already have it on PATH via .profile). Fall back to the first PATH entry - * under HOME we can write to. Skip on Windows (npm install -g is the right - * primitive there; we don't try to fabricate a .cmd shim). - */ -function trySelfLinkGsdSdk(shimSrc) { - if (process.platform === 'win32') { - return trySelfLinkGsdSdkWindows(shimSrc); - } - const path = require('path'); - const fs = require('fs'); - const home = os.homedir(); - if (!home) return null; - - const localBin = path.join(home, '.local', 'bin'); - const pathCandidates = []; - const pathEnv = process.env.PATH || ''; - for (const seg of pathEnv.split(path.delimiter)) { - if (!seg) continue; - const abs = path.resolve(seg); - if (abs.startsWith(home + path.sep) && !pathCandidates.includes(abs)) { - pathCandidates.push(abs); - } - } - // If ~/.local/bin is already on PATH, keep it first (preserves existing UX - // for the common case). Otherwise prefer PATH-backed HOME dirs first so we - // self-link somewhere actually on PATH, falling back to ~/.local/bin only - // when no on-PATH HOME dir is writable. (#2775 CodeRabbit follow-up) - const candidates = pathCandidates.includes(localBin) - ? [localBin, ...pathCandidates.filter((dir) => dir !== localBin)] - : [...pathCandidates, localBin]; - - for (const dir of candidates) { - try { - fs.mkdirSync(dir, { recursive: true }); - const target = path.join(dir, 'gsd-sdk'); - // Replace any existing entry — it may be stale (prior install of an - // older version pointing at a now-absent shim). - try { fs.unlinkSync(target); } catch {} - try { - fs.symlinkSync(shimSrc, target); - } catch { - // Filesystems that don't support symlinks (some FUSE mounts): write a - // tiny wrapper that `require()`s the real shim by absolute path. We - // cannot copyFileSync(shimSrc, target) — bin/gsd-sdk.js resolves the - // CLI via `path.resolve(__dirname, '..', 'sdk', 'dist', 'cli.js')`, - // and after a copy `__dirname` would be the link directory (e.g. - // ~/.local/bin), causing the resolved CLI path to be broken - // (~/.local/sdk/dist/cli.js). Wrapping via require() preserves - // __dirname resolution because the require runs against shimSrc's - // own location. (#2775 CodeRabbit follow-up) - fs.writeFileSync( - target, - `#!/usr/bin/env node\nrequire(${JSON.stringify(shimSrc)});\n`, - ); - try { fs.chmodSync(target, 0o755); } catch {} - } - return target; - } catch { - // permission / EROFS — try next candidate. - } - } - return null; -} - -/** - * #2962: Windows counterpart to trySelfLinkGsdSdk. Prior to this, the function - * unconditionally returned null on Windows ("we don't try to fabricate a .cmd - * shim there"), which left `--sdk --global` installs without a callable - * `gsd-sdk` on PATH despite the installer reporting success. - * - * Strategy: discover npm's global bin directory via `npm prefix -g` (which on - * Windows IS the bin dir, no `bin/` suffix — see line 8721) and write the same - * three-file shim set npm itself emits: `gsd-sdk.cmd` (cmd.exe), `gsd-sdk.ps1` - * (PowerShell), and a Bash wrapper named `gsd-sdk` (for Cygwin/MSYS/Git-Bash). - * Each shim invokes `node ""` with passed - * args so the shim location is decoupled from the SDK location — same logical - * structure as the POSIX wrapper-via-require() fallback above. - * - * Returns the .cmd file path on success (the primary handle the installer's - * onPath check looks for), null otherwise. - */ -/** - * Pure builder: compute the structured Windows shim triple from a shimSrc path. - * No filesystem I/O, no spawn — produces the IR that `trySelfLinkGsdSdkWindows` - * then renders to disk. Exposed for tests so assertions can run against typed - * fields (interpreter, shimAbs, eol, fileNames) instead of substring matches - * over rendered shim text. - */ +// Projection-contract surfaces (drift guards: tests/bug-3441, tests/bug-3442). +// These have no production caller after the #505 dead-SDK-verification removal, +// but they are the install.js side of the shell-command projection contract: +// the drift-guard tests assert install.js delegates to +// shell-command-projection.cjs rather than hand-rolling the projection. Kept +// exported so that contract stays verifiable. function buildWindowsShimTriple(shimSrc) { return buildWindowsShimTripleFromProjection(shimSrc); } -/** - * #3011: pure builder for the SDK-not-on-PATH diagnostic. Takes the - * resolved shim directory (or null if write failed), the current platform, - * and the install.js __dirname (used to detect npx-cache invocation). - * Returns a typed IR with: - * - shimLocationLine: prose mentioning where the shim is (or empty if no - * write happened) - * - actionLines: ordered list of commands the user can run to add the - * shim dir to their PATH (platform-specific shells), or fallback to - * `npm install -g` advice when no shim was written - * - npxNoteLines: ordered list of lines warning about npx persistence - * when runDir is under an `_npx` cache segment - * - * Tests assert on the typed fields (paths/commands), not on rendered - * console output. Pure function — no fs, no spawn, no console. - */ function formatSdkPathDiagnostic({ shimDir, platform, runDir }) { return formatSdkPathDiagnosticFromProjection({ shimDir, platform, runDir }); } -function trySelfLinkGsdSdkWindows(shimSrc) { - const path = require('path'); - const fs = require('fs'); - const cp = require('child_process'); - - let npmPrefix; - try { - // On Windows, `npm` is `npm.cmd` — Node's child_process docs explicitly - // call out that .cmd/.bat files cannot be spawned via execFile/execFileSync - // without a shell ("Spawning .bat and .cmd files on Windows" section). - // Match the existing convention at line ~8718 which uses execSync for the - // same `npm prefix -g` lookup. Inputs here are static literals, so shell - // interpolation is not an injection vector. - npmPrefix = cp - .execSync('npm prefix -g', { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }) - .trim(); - } catch { - return null; - } - if (!npmPrefix || !fs.existsSync(npmPrefix)) return null; - - // Verify writability before producing partial shim sets. - try { - fs.mkdirSync(npmPrefix, { recursive: true }); - const probe = path.join(npmPrefix, '.gsd-sdk-write-probe'); - fs.writeFileSync(probe, ''); - fs.unlinkSync(probe); - } catch { - return null; - } - - const triple = buildWindowsShimTriple(shimSrc); - const targets = { - cmd: path.join(npmPrefix, triple.fileNames.cmd), - ps1: path.join(npmPrefix, triple.fileNames.ps1), - sh: path.join(npmPrefix, triple.fileNames.sh), - }; - - try { - // Replace any existing shims — they may be stale (prior install of an - // older version pointing at a now-absent shim path). - for (const target of Object.values(targets)) { - try { fs.unlinkSync(target); } catch {} - } - fs.writeFileSync(targets.cmd, triple.render.cmd()); - fs.writeFileSync(targets.ps1, triple.render.ps1()); - fs.writeFileSync(targets.sh, triple.render.sh()); - // chmod is a no-op on Windows-native node but harmless; sets exec bit on - // WSL-mounted filesystems where Bash users live. - try { fs.chmodSync(targets.sh, 0o755); } catch {} - return targets.cmd; - } catch { - // Partial-write on permission flap — best-effort cleanup so the next run - // starts from a clean slate. - for (const target of Object.values(targets)) { - try { fs.unlinkSync(target); } catch {} - } - return null; - } -} - /** * Install GSD for all selected runtimes */ @@ -11729,15 +10931,10 @@ module.exports = { install, installAllRuntimes, uninstall, - installSdkIfNeeded, detectStaleStandaloneSdk, formatStaleStandaloneSdkWarning, - buildSdkFailFastReport, - renderSdkFailFastReport, - buildGsdSdkVersionMismatchReport, - renderGsdSdkVersionMismatchReport, - classifySdkInstall, - readGsdSdkVersion, + buildWindowsShimTriple, + formatSdkPathDiagnostic, convertClaudeCommandToCodexSkill, convertClaudeToOpencodeFrontmatter, convertClaudeToKiloFrontmatter, @@ -11788,15 +10985,6 @@ module.exports = { populatePristineDir, USER_OWNED_ARTIFACTS, finishInstall, - trySelfLinkGsdSdk, - trySelfLinkGsdSdkWindows, - buildWindowsShimTriple, - formatSdkPathDiagnostic, - filterNpxFromPath, - isLegacyGsdSdkShim, - isGsdSdkOnPath, - getUserShellPath, - getUserShellWindowsPersistentPath, homePathCoveredByRc, maybeSuggestPathExport, runtimeMap, diff --git a/tests/bug-3020-install-shell-path-probe.test.cjs b/tests/bug-3020-install-shell-path-probe.test.cjs deleted file mode 100644 index 96bf6229f..000000000 --- a/tests/bug-3020-install-shell-path-probe.test.cjs +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Regression test for bug #3020. - * - * The installer prints `✓ GSD SDK ready (sdk/dist/cli.js)` whenever - * isGsdSdkOnPath() — which reads process.env.PATH from the install - * subprocess — finds the shim. That set is not the same as the user's - * later interactive shell PATH: - * - * - Windows cross-shell: gsd-sdk.cmd resolves under PowerShell/cmd - * (PATHEXT) but bare `gsd-sdk` does not resolve under Git Bash / - * MSYS / WSL bash. - * - POSIX ~/.local/bin: install subprocess inherits npm/npx-injected - * PATH containing ~/.local/bin; user's login shell may not. - * - Node version managers (nvm/fnm/volta) shim PATH per-shell. - * - * Result: green ✓ at install time, "command not found" at workflow - * runtime (#3011 originals + @x0rk + @stefanoginella). - * - * Fix: introduce two helpers and use them at install time. - * - * isGsdSdkOnPath(pathString?: string) - * - Now accepts an optional explicit PATH string. When omitted, - * falls back to process.env.PATH (preserves existing behavior). - * - Pure: no spawn, no I/O beyond fs.statSync on candidates. - * - * getUserShellPath() → string | null - * - Probes the user's login shell ($SHELL -lc 'printf %s "$PATH"') on - * POSIX so we can predict the runtime shell PATH. - * - Returns null on Windows or when the probe fails (caller falls - * back to process.env.PATH). - * - * Tests are typed-IR / structural — no console capture, no source grep. - */ - -'use strict'; - -process.env.GSD_TEST_MODE = '1'; - -const { test, describe } = 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 INSTALL = require(path.join(__dirname, '..', 'bin', 'install.js')); -const { isGsdSdkOnPath, getUserShellPath } = INSTALL; - -describe('bug #3020: isGsdSdkOnPath accepts an explicit PATH string', () => { - test('exported as a function', () => { - assert.equal(typeof isGsdSdkOnPath, 'function'); - }); - - test('returns true when an executable gsd-sdk exists in the supplied PATH', () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3020-')); - try { - // Create a fake `gsd-sdk` shim with the executable bit set. - const shimName = process.platform === 'win32' ? 'gsd-sdk.cmd' : 'gsd-sdk'; - const shimPath = path.join(tmp, shimName); - fs.writeFileSync(shimPath, process.platform === 'win32' ? '@echo off\nexit 0\n' : '#!/bin/sh\nexit 0\n'); - if (process.platform !== 'win32') fs.chmodSync(shimPath, 0o755); - const result = isGsdSdkOnPath(tmp); - assert.equal(result, true, `expected true for PATH=${tmp}, got ${result}`); - } finally { - fs.rmSync(tmp, { recursive: true, force: true }); - } - }); - - test('returns false when the supplied PATH has no gsd-sdk', () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3020-')); - try { - const result = isGsdSdkOnPath(tmp); - assert.equal(result, false); - } finally { - fs.rmSync(tmp, { recursive: true, force: true }); - } - }); - - test('zero-arg form preserves existing behavior (reads process.env.PATH)', () => { - // Just call it — it shouldn't throw and should return a boolean. - const result = isGsdSdkOnPath(); - assert.equal(typeof result, 'boolean'); - }); - - test('treats an empty PATH string as no segments to scan', () => { - const result = isGsdSdkOnPath(''); - assert.equal(result, false); - }); - - test('null pathString is type-guarded — falls back to process.env.PATH (#3028 CR)', () => { - // Pre-fix: isGsdSdkOnPath(null) threw "Cannot read properties of null - // (reading 'split')". Post-fix: typeof check falls back to process.env.PATH. - let threw = null; - let result; - try { - result = isGsdSdkOnPath(null); - } catch (e) { - threw = e; - } - assert.equal(threw, null, `must not throw on null input, got: ${threw && threw.message}`); - assert.equal(typeof result, 'boolean', 'must return a boolean'); - }); - - test('non-string pathString (number, object) falls back to process.env.PATH (#3028 CR)', () => { - // Defensive: any non-string argument should fall back, not throw. - assert.equal(typeof isGsdSdkOnPath(0), 'boolean'); - assert.equal(typeof isGsdSdkOnPath({}), 'boolean'); - assert.equal(typeof isGsdSdkOnPath([]), 'boolean'); - }); -}); - -describe('bug #3020: getUserShellPath probes the user login shell PATH', () => { - test('exported as a function', () => { - assert.equal(typeof getUserShellPath, 'function'); - }); - - test('returns a string with at least one segment OR null', () => { - const result = getUserShellPath(); - if (result === null) { - // Acceptable on Windows or when probing fails — caller must fall back. - return; - } - assert.equal(typeof result, 'string'); - // PATH must have segments separated by the platform delimiter. - assert.ok(result.length > 0, 'non-null result must be non-empty'); - }); - - test('returns null on Windows (POSIX shell probe is not portable)', () => { - if (process.platform !== 'win32') return; - const result = getUserShellPath(); - assert.equal(result, null); - }); - - test('returns null when SHELL env var is unset', () => { - if (process.platform === 'win32') return; - const original = process.env.SHELL; - delete process.env.SHELL; - try { - const result = getUserShellPath(); - assert.equal(result, null, 'must return null when $SHELL is unset (POSIX caller falls back to process.env.PATH)'); - } finally { - if (original !== undefined) process.env.SHELL = original; - } - }); -}); - -describe('bug #3020: cross-shell PATH mismatch is detectable via the new helpers', () => { - test('install-time PATH has shim, user-shell PATH does not → mismatch detected', () => { - const installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3020-install-')); - const userDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3020-user-')); - try { - const shimName = process.platform === 'win32' ? 'gsd-sdk.cmd' : 'gsd-sdk'; - const shimPath = path.join(installDir, shimName); - fs.writeFileSync(shimPath, process.platform === 'win32' ? '@echo off\nexit 0\n' : '#!/bin/sh\nexit 0\n'); - if (process.platform !== 'win32') fs.chmodSync(shimPath, 0o755); - - const installSees = isGsdSdkOnPath(installDir); - const userSees = isGsdSdkOnPath(userDir); - - assert.equal(installSees, true, 'install-time PATH sees the shim'); - assert.equal(userSees, false, 'user-shell PATH does not see the shim'); - // The mismatch is what the post-install check must detect to avoid - // the false ✓. - assert.notEqual(installSees, userSees, 'shim presence differs between install-time PATH and user-shell PATH'); - } finally { - fs.rmSync(installDir, { recursive: true, force: true }); - fs.rmSync(userDir, { recursive: true, force: true }); - } - }); -}); diff --git a/tests/bug-505-remove-dead-sdk-verification.test.cjs b/tests/bug-505-remove-dead-sdk-verification.test.cjs new file mode 100644 index 000000000..2bc760c45 --- /dev/null +++ b/tests/bug-505-remove-dead-sdk-verification.test.cjs @@ -0,0 +1,86 @@ +/** + * Regression guard for #505: dead SDK-shim verification subsystem removed. + * + * Post-ADR-0174 the `@opengsd/gsd-sdk` package was retired; `sdk/` no longer + * ships. `installSdkIfNeeded` and all functions it transitively called are + * dead code with no live callers. This test asserts: + * + * 1. All removed symbols are NO LONGER exported from bin/install.js. + * 2. The two live stale-standalone-SDK helpers (detectStaleStandaloneSdk, + * formatStaleStandaloneSdkWarning) are STILL exported as functions — they + * handle a real user-facing condition (#3406) and MUST NOT be removed. + */ + +'use strict'; + +process.env.GSD_TEST_MODE = '1'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); + +const inst = require('../bin/install.js'); + +describe('bug #505: dead SDK verification subsystem removed from bin/install.js', () => { + // ---------------------------------------------------------------- + // Dead symbols — must NOT be exported after removal + // ---------------------------------------------------------------- + const deadSymbols = [ + 'installSdkIfNeeded', + 'classifySdkInstall', + 'buildSdkFailFastReport', + 'renderSdkFailFastReport', + 'buildGsdSdkVersionMismatchReport', + 'renderGsdSdkVersionMismatchReport', + 'readGsdSdkVersion', + 'parseGsdSdkVersion', + 'findGsdSdkOnPath', + 'isGsdSdkOnPath', + 'isLegacyGsdSdkShim', + 'trySelfLinkGsdSdk', + 'trySelfLinkGsdSdkWindows', + 'filterNpxFromPath', + 'getUserShellPath', + 'getUserShellWindowsPersistentPath', + ]; + + for (const sym of deadSymbols) { + test(`dead symbol '${sym}' is not exported`, () => { + assert.equal( + typeof inst[sym], + 'undefined', + `'${sym}' should have been removed (post #505 dead-code removal) but is still exported as ${typeof inst[sym]}`, + ); + }); + } + + // ---------------------------------------------------------------- + // Live symbols — MUST still be exported as functions + // ---------------------------------------------------------------- + test('live function detectStaleStandaloneSdk is still exported', () => { + assert.equal( + typeof inst.detectStaleStandaloneSdk, + 'function', + 'detectStaleStandaloneSdk handles real stale-SDK condition (#3406) and must not be removed', + ); + }); + + test('live function formatStaleStandaloneSdkWarning is still exported', () => { + assert.equal( + typeof inst.formatStaleStandaloneSdkWarning, + 'function', + 'formatStaleStandaloneSdkWarning handles real stale-SDK condition (#3406) and must not be removed', + ); + }); + + // buildWindowsShimTriple / formatSdkPathDiagnostic have no production caller + // after this removal, but they are the install.js side of a projection-contract + // drift guard (tests/bug-3441, tests/bug-3442) that assert install.js delegates + // to shell-command-projection.cjs. They are deliberately kept exported. + test('contract surface buildWindowsShimTriple is still exported', () => { + assert.equal(typeof inst.buildWindowsShimTriple, 'function'); + }); + + test('contract surface formatSdkPathDiagnostic is still exported', () => { + assert.equal(typeof inst.formatSdkPathDiagnostic, 'function'); + }); +}); diff --git a/tests/no-unconditional-win32-skip.test.cjs b/tests/no-unconditional-win32-skip.test.cjs deleted file mode 100644 index ce1a5ea43..000000000 --- a/tests/no-unconditional-win32-skip.test.cjs +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -process.env.GSD_TEST_MODE = '1'; - -/** - * Behavior-based regression guard for #2962-class bugs. - * - * "Nothing for Windows should be deferred — if it wasn't in, it was missed - * not deferred." (maintainer guidance, 2026-05-01.) - * - * Specifically guards against trySelfLinkGsdSdk silently no-op'ing on - * Windows. Rather than regex-scanning bin/install.js source (which would - * fail on harmless refactors and conflicts with the repo's no-source-grep - * testing standard), this test exercises the function under a simulated - * `process.platform === 'win32'` and asserts shim files actually land on - * disk — i.e., the Windows branch dispatches, doesn't early-return null. - */ - -const { test, describe, 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')); - -describe('Windows parity guard for trySelfLinkGsdSdk (#2962)', () => { - let tmpDir; - let origPlatform; - let origExecSync; - - before(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-win32-guard-')); - origPlatform = process.platform; - origExecSync = cp.execSync; - // Override process.platform to simulate Windows. process.platform is a - // configurable property in Node — Object.defineProperty can swap it. - Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); - cp.execSync = (cmd) => { - if (typeof cmd === 'string' && cmd.trim() === 'npm prefix -g') { - return tmpDir + '\n'; - } - throw new Error(`unexpected execSync: ${cmd}`); - }; - }); - - after(() => { - Object.defineProperty(process, 'platform', { value: origPlatform, configurable: true }); - cp.execSync = origExecSync; - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - test('trySelfLinkGsdSdk dispatches to the Windows handler and writes shims (does NOT silently return null)', () => { - const shimSrc = path.join(ROOT, 'bin', 'gsd-sdk.js'); - const result = installModule.trySelfLinkGsdSdk(shimSrc); - - assert.notEqual( - result, - null, - 'trySelfLinkGsdSdk must not silently return null on Windows; ' + - 'a no-op skip is a missed-parity regression (see #2962, #2775).', - ); - assert.ok( - fs.existsSync(path.join(tmpDir, 'gsd-sdk.cmd')), - 'Windows dispatch must materialize gsd-sdk.cmd in the npm global bin', - ); - }); -});