'use strict'; /** * Behavioral regression tests for ADR-1239 Phase B write-confinement. * * Tests cover: * - copyWithPathReplacement: happy path, escape rejection, dest===root, * fail-closed (no confinementRoot), symlink escape * - installCodexConfig: happy path, agent name-injection rejection * - _copyStaged: escape rejection, symlink escape (regression preserved) */ const { describe, test } = 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 { cleanup } = require('./helpers.cjs'); process.env['GSD_TEST_MODE'] = '1'; const { copyWithPathReplacement, installCodexConfig, _copyStaged, } = require('../bin/install.js'); // --------------------------------------------------------------------------- // copyWithPathReplacement // --------------------------------------------------------------------------- describe('copyWithPathReplacement write-confinement', () => { test('1. happy path: file is written under confinementRoot', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-happy-')); try { const srcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-src-')); try { fs.writeFileSync(path.join(srcDir, 'test.md'), '---\nname: test\n---\nbody\n', 'utf8'); const destDir = path.join(root, 'sub', 'dest'); copyWithPathReplacement(srcDir, destDir, '~/.claude/', 'claude', false, false, root); // The dest dir and its content must exist inside root const written = fs.existsSync(path.join(destDir, 'test.md')); assert.ok(written, 'test.md must have been written to destDir under root'); assert.ok( path.resolve(destDir).startsWith(path.resolve(root) + path.sep), 'destDir must be under root', ); } finally { cleanup(srcDir); } } finally { cleanup(root); } }); test('2. escape rejected: destDir outside confinementRoot → throws, nothing written at escape path', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-root-')); const escapeName = 'gsd-cwpr-escape-' + Date.now(); const escapePath = path.join(os.tmpdir(), escapeName); try { const srcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-src2-')); try { fs.writeFileSync(path.join(srcDir, 'evil.md'), '# evil\n', 'utf8'); // destDir resolves outside root via parent traversal const destDir = path.join(root, '..', escapeName); assert.throws( () => copyWithPathReplacement(srcDir, destDir, '~/.claude/', 'claude', false, false, root), /escap|must be a strict subpath|refusing/i, ); // Nothing must have been created at the escape path assert.ok(!fs.existsSync(escapePath), 'must not create anything at the escape path'); } finally { cleanup(srcDir); } } finally { cleanup(root); // also remove escapePath if it was somehow created (defensive) if (fs.existsSync(escapePath)) cleanup(escapePath); } }); test('3. dest === root rejected: throws when destDir equals confinementRoot', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-eqroot-')); try { const srcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-src3-')); try { fs.writeFileSync(path.join(srcDir, 'x.md'), '# x\n', 'utf8'); assert.throws( () => copyWithPathReplacement(srcDir, root, '~/.claude/', 'claude', false, false, root), /escap|must be a strict subpath|refusing|configHome itself/i, ); } finally { cleanup(srcDir); } } finally { cleanup(root); } }); test('4. fail-closed: omitting confinementRoot throws with descriptive message', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-fc-')); try { const srcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-src4-')); try { fs.writeFileSync(path.join(srcDir, 'y.md'), '# y\n', 'utf8'); const destDir = path.join(root, 'sub'); assert.throws( () => copyWithPathReplacement(srcDir, destDir, '~/.claude/', 'claude', false, false, undefined), /confinementRoot is required/, ); } finally { cleanup(srcDir); } } finally { cleanup(root); } }); test('5. symlink escape: destDir via symlink outside root → throws', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-syml-')); const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-out-')); try { const srcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cwpr-src5-')); try { fs.writeFileSync(path.join(srcDir, 'z.md'), '# z\n', 'utf8'); const linkPath = path.join(root, 'link'); try { fs.symlinkSync(outside, linkPath); } catch (_symlinkErr) { // Symlink creation unsupported on this platform/privilege — skip test body t.skip('symlink creation unsupported on this platform/privilege'); return; } const destDir = path.join(linkPath, 'sub'); assert.throws( () => copyWithPathReplacement(srcDir, destDir, '~/.claude/', 'claude', false, false, root), /symlink|escap|confinement|install root/i, ); // Nothing written to outside assert.strictEqual(fs.readdirSync(outside).length, 0, 'must not write to the outside dir via symlink'); } finally { cleanup(srcDir); } } finally { // unlink the symlink before cleanup to avoid crossing boundaries try { fs.unlinkSync(path.join(root, 'link')); } catch { /* already gone */ } cleanup(root); cleanup(outside); } }); }); // --------------------------------------------------------------------------- // installCodexConfig // --------------------------------------------------------------------------- describe('installCodexConfig write-confinement', () => { test('6. happy path: config.toml and agents/.toml written under targetDir', () => { const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-happy-')); try { const agentsSrc = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-src-')); try { // Minimal valid agent frontmatter fs.writeFileSync( path.join(agentsSrc, 'gsd-foo.md'), '---\nname: gsd-foo\ndescription: x\n---\nbody\n', 'utf8', ); const count = installCodexConfig(targetDir, agentsSrc); assert.strictEqual(count, 1, 'must return count of 1 agent processed'); assert.ok(fs.existsSync(path.join(targetDir, 'config.toml')), 'config.toml must exist under targetDir'); assert.ok(fs.existsSync(path.join(targetDir, 'agents', 'gsd-foo.toml')), 'agents/gsd-foo.toml must exist under targetDir'); } finally { cleanup(agentsSrc); } } finally { cleanup(targetDir); } }); test('7a. name-injection rejected: frontmatter name "../../evil" must throw, nothing written at escape', () => { const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-inj-')); // Use a unique escape name derived from the targetDir basename so the escape // path can never collide with pre-existing files in os.tmpdir(). // agentsTomlDir = resolve(targetDir, 'agents'); ../../.toml from // there = resolve(targetDir, '../.toml') = dirname(targetDir)/.toml const escapeName = path.basename(targetDir) + '-escape'; const escapePath = path.join(path.dirname(targetDir), escapeName + '.toml'); try { const agentsSrc = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-src-inj-')); try { fs.writeFileSync( path.join(agentsSrc, 'gsd-evil.md'), `---\nname: ../../${escapeName}\ndescription: injected\n---\nbody\n`, 'utf8', ); assert.throws( () => installCodexConfig(targetDir, agentsSrc), /escap|strict subpath|refusing|NUL/i, ); // Verify nothing was written at the escape location assert.ok(!fs.existsSync(escapePath), 'no file/dir written at escape location'); } finally { cleanup(agentsSrc); } } finally { cleanup(targetDir); if (fs.existsSync(escapePath)) cleanup(escapePath); } }); test('7b. name-injection: "../config" and "../evil" must both throw (clobber-prevention, tighter agentsTomlDir root)', () => { // With confinement rooted at agentsTomlDir (not targetDir), a name like // "../config" resolves to targetDir/config.toml — still inside the configHome // but OUTSIDE agents/ — so the gate must throw (clobber prevention). // Similarly "../evil" resolves to targetDir/evil.toml, also outside agents/. // Both must throw regardless of whether they escape targetDir. // Case A: "../config" — would clobber config.toml, must throw. const targetDirA = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-inj2a-')); try { const agentsSrcA = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-src-inj2a-')); try { fs.writeFileSync( path.join(agentsSrcA, 'gsd-clobber.md'), '---\nname: ../config\ndescription: clobber attempt\n---\nbody\n', 'utf8', ); assert.throws( () => installCodexConfig(targetDirA, agentsSrcA), /escap|strict subpath|refusing|NUL/i, 'name "../config" must throw — it escapes agents/ even though it stays inside targetDir', ); } finally { cleanup(agentsSrcA); } } finally { cleanup(targetDirA); } // Case B: "../evil" — escapes agents/, must throw (new behavior with agentsTomlDir root). const targetDirB = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-inj2b-')); try { const agentsSrcB = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-src-inj2b-')); try { fs.writeFileSync( path.join(agentsSrcB, 'gsd-up.md'), '---\nname: ../up-escape-attempt\ndescription: boundary test\n---\nbody\n', 'utf8', ); assert.throws( () => installCodexConfig(targetDirB, agentsSrcB), /escap|strict subpath|refusing|NUL/i, 'name "../evil" must throw — it escapes agents/ (resolves to targetDir/evil.toml)', ); } finally { cleanup(agentsSrcB); } } finally { cleanup(targetDirB); } // Case C: "../../evil" still throws (escapes both agents/ and targetDir). const targetDirC = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-inj2c-')); try { const agentsSrcC = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-src-inj2c-')); try { fs.writeFileSync( path.join(agentsSrcC, 'gsd-deep.md'), '---\nname: ../../deep-escape\ndescription: deep escape\n---\nbody\n', 'utf8', ); assert.throws( () => installCodexConfig(targetDirC, agentsSrcC), /escap|strict subpath|refusing|NUL/i, ); } finally { cleanup(agentsSrcC); } } finally { cleanup(targetDirC); } }); test('10. symlink-escape: agents/ is a symlink outside targetDir → throws', (t) => { const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-syml-')); const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-syml-out-')); try { const agentsSrc = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-icc-syml-src-')); try { fs.writeFileSync( path.join(agentsSrc, 'gsd-foo.md'), '---\nname: gsd-foo\ndescription: x\n---\nbody\n', 'utf8', ); const agentsLink = path.join(targetDir, 'agents'); try { fs.symlinkSync(outsideDir, agentsLink); } catch (_symlinkErr) { // Symlink creation unsupported on this platform/privilege — skip t.skip('symlink creation unsupported on this platform/privilege'); return; } assert.throws( () => installCodexConfig(targetDir, agentsSrc), /symlink|escap|refusing/i, ); // Nothing must have been written to the outside dir via the symlink assert.strictEqual(fs.readdirSync(outsideDir).length, 0, 'must not write to the outside dir via symlink'); } finally { cleanup(agentsSrc); } } finally { try { fs.unlinkSync(path.join(targetDir, 'agents')); } catch { /* already gone */ } cleanup(targetDir); cleanup(outsideDir); } }); }); // --------------------------------------------------------------------------- // _copyStaged // --------------------------------------------------------------------------- describe('_copyStaged write-confinement', () => { test('8. escape rejected (regression): destDir escaping configDir → throws', () => { const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cs-cfg-')); const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cs-staged-')); const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cs-outside-')); try { fs.writeFileSync(path.join(stagedDir, 'help.md'), '# help\n', 'utf8'); assert.throws( () => _copyStaged(stagedDir, outsideDir, { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, configDir), /escap|strict subpath|refusing|configHome/i, ); } finally { cleanup(configDir); cleanup(stagedDir); cleanup(outsideDir); } }); test('9. symlink escape rejected: destDir containing symlink to outside → throws', (t) => { const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cs-syml-')); const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cs-syml-out-')); const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cs-staged2-')); try { fs.writeFileSync(path.join(stagedDir, 'help.md'), '# help\n', 'utf8'); const linkPath = path.join(configDir, 'link'); try { fs.symlinkSync(outside, linkPath); } catch (symlinkErr) { // Symlink creation unsupported on this platform/privilege — skip t.skip('symlink creation unsupported on this platform/privilege'); return; } const destDir = path.join(linkPath, 'sub'); assert.throws( () => _copyStaged(stagedDir, destDir, { kind: 'commands', destSubpath: 'commands/link/sub', prefix: 'gsd-' }, configDir), /symlink|escap|confinement|install root/i, ); assert.strictEqual(fs.readdirSync(outside).length, 0, 'must not have written to outside dir'); } finally { try { fs.unlinkSync(path.join(configDir, 'link')); } catch { /* already gone */ } cleanup(configDir); cleanup(outside); cleanup(stagedDir); } }); test('11. fail-closed: omitting configDir throws with descriptive message', () => { const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cs-fc-staged-')); const destDir = path.join(os.tmpdir(), 'gsd-cs-fc-dest-' + Date.now()); try { fs.writeFileSync(path.join(stagedDir, 'help.md'), '# help\n', 'utf8'); assert.throws( () => _copyStaged(stagedDir, destDir, { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, undefined), /configDir.*required|required to confine/i, ); } finally { cleanup(stagedDir); if (fs.existsSync(destDir)) cleanup(destDir); } }); }); // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2998-pristine-dir-populated.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2998-pristine-dir-populated (consolidation epic #1969 B1 #1970)", () => { 'use strict'; process.env.GSD_TEST_MODE = '1'; /** * Bug #2998: gsd-pristine/ snapshot is documented but never populated by * the installer. saveLocalPatches declared a pristineDir variable and * promised "saves pristine copies (from manifest) to gsd-pristine/ to * enable three-way merge during reapply-patches" -- but no code ever * wrote to that directory. Effect: the /gsd-reapply-patches Step 5 * verifier (#2972) silently degrades to its over-broad fallback heuristic * ("every significant backup line"), exactly the silent-success-on-lost- * content failure mode #2969 was designed to prevent. * * Fix: new populatePristineDir({...}) helper runs the install transform * pipeline (copyWithPathReplacement) into a tmp staging dir, then copies * out the modified-file paths into gsd-pristine/. saveLocalPatches now * accepts a pristineCtx and calls the helper when local patches are * detected. */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const crypto = require('node:crypto'); const ROOT = path.join(__dirname, '..'); const INSTALL = require(path.join(ROOT, 'bin', 'install.js')); const { cleanup } = require('./helpers.cjs'); function sha256(content) { return crypto.createHash('sha256').update(content).digest('hex'); } describe('Bug #2998: populatePristineDir is exported and writes pristine for modified files', () => { test('exported as a function', () => { assert.equal(typeof INSTALL.populatePristineDir, 'function', 'expected populatePristineDir in install.js exports (#2998)'); }); test('returns 0 when no files are modified (no-op)', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-')); try { const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir: path.join(tmp, 'gsd-pristine'), modified: [], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 0); } finally { cleanup(tmp); } }); test('writes one pristine file per modified path that exists in source', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { // Pick a real installed-side relPath from the package source. The // install transforms map source `gsd-core/` to installed // `gsd-core/` for skills-aware runtimes (like claude), // so the relPath is the same on both sides. const candidate = path.join('gsd-core', 'workflows', 'reapply-patches.md'); const sourcePath = path.join(ROOT, candidate); assert.equal(fs.existsSync(sourcePath), true, `precondition: source file exists at ${candidate}`); const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: [candidate], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 1, 'expected exactly one pristine file written'); const out = path.join(pristineDir, candidate); assert.equal(fs.existsSync(out), true, `expected pristine file at ${out}`); // The pristine content should be the transformed version (not raw source): // copyWithPathReplacement substitutes ~/.claude/ for the runtime path prefix. // For claude+global, the prefix is $HOME/.claude/ which equals the original, // so the transform is effectively identity here. We assert the content is a // non-empty markdown file rather than asserting on transform specifics. const content = fs.readFileSync(out, 'utf-8'); assert.ok(content.length > 0, 'pristine file should be non-empty'); } finally { cleanup(tmp); } }); test('skips paths not present in source (does not corrupt pristine with stale data)', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: ['gsd-core/this-path-does-not-exist.md'], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 0, 'expected zero pristine files for non-existent source paths'); const out = path.join(pristineDir, 'gsd-core/this-path-does-not-exist.md'); assert.equal(fs.existsSync(out), false, 'pristine should not contain ghost paths'); } finally { cleanup(tmp); } }); test('pristine files have stable content (transformations are deterministic)', () => { // Determinism is what makes the verifier's hash check meaningful: // backup-meta.json records pristine_hashes computed at this same step, // so re-running with the same inputs must yield byte-identical files. const tmp1 = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-d1-')); const tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-d2-')); try { const candidate = path.join('gsd-core', 'workflows', 'reapply-patches.md'); const ctx = { packageSrc: ROOT, modified: [candidate], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }; INSTALL.populatePristineDir(Object.assign({ pristineDir: path.join(tmp1, 'gsd-pristine') }, ctx)); INSTALL.populatePristineDir(Object.assign({ pristineDir: path.join(tmp2, 'gsd-pristine') }, ctx)); const a = fs.readFileSync(path.join(tmp1, 'gsd-pristine', candidate)); const b = fs.readFileSync(path.join(tmp2, 'gsd-pristine', candidate)); assert.equal(sha256(a), sha256(b), 'two runs of the same inputs must yield identical pristine content'); } finally { cleanup(tmp1); cleanup(tmp2); } }); }); // ─── #3004 CR follow-up: multi-root pristine expansion ───────────────────── describe('Bug #2998 (#3004 CR): pristine expansion covers every manifest install root', () => { test('paths under agents/ are staged via copyWithPathReplacement, not silently skipped', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-multi-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { const candidate = path.join('agents', 'gsd-planner.md'); const sourcePath = path.join(ROOT, candidate); assert.equal(fs.existsSync(sourcePath), true, `precondition: source file exists at ${candidate}`); const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: [candidate], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 1, 'expected agents/ path to be staged and copied to pristine'); assert.equal(fs.existsSync(path.join(pristineDir, candidate)), true); } finally { cleanup(tmp); } }); test('a mix of gsd-core/ and agents/ paths in modified list are all staged', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-mix-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { const a = path.join('gsd-core', 'workflows', 'reapply-patches.md'); const b = path.join('agents', 'gsd-planner.md'); assert.equal(fs.existsSync(path.join(ROOT, a)), true); assert.equal(fs.existsSync(path.join(ROOT, b)), true); const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: [a, b], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 2, 'expected both top-level dirs to be staged'); assert.equal(fs.existsSync(path.join(pristineDir, a)), true); assert.equal(fs.existsSync(path.join(pristineDir, b)), true); } finally { cleanup(tmp); } }); }); describe('Bug #2998: saveLocalPatches no longer leaves the pristineDir variable unused', () => { test('saveLocalPatches accepts a pristineCtx and exposes the helper for direct testing', () => { // Structural assertion: the function exists with the new signature shape. // Behavioral end-to-end is covered by the populatePristineDir tests above // (that helper is what saveLocalPatches calls internally). assert.equal(typeof INSTALL.populatePristineDir, 'function'); // The signature for saveLocalPatches isn't exported, but the helper IS, // and it's the unit of behavior the bug is about. Asserting on the helper // is the structural-IR equivalent of the no-source-grep convention. }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-3407-pristine-stale-content.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-3407-pristine-stale-content (consolidation epic #1969 B1 #1970)", () => { 'use strict'; process.env.GSD_TEST_MODE = '1'; /** * Bug #3407: Installer leaves stale content in gsd-pristine/ * * Root cause: populatePristineDir() in saveLocalPatches() snapshots from * pristineCtx.packageSrc — the NEWLY-downloaded release tree — and writes * those bytes into gsd-pristine/. For files changed between the old and new * release, this writes the NEW bytes into the pristine baseline instead of * the OLD bytes. The three-way-diff verifier then classifies upstream-changed * lines as user-added → Step 5a gate fails with false FAIL_USER_LINES_MISSING. * * The #3657 fix (OK_PRISTINE_DRIFT_DETECTED) was a symptom workaround: the * verifier detects hash mismatch (backup-meta.json records old-release hash * but gsd-pristine/ has new-release bytes) and skips to over-broad mode * instead of false-failing. The root-cause stale write was never fixed. * * Fix: when a correctly-populated gsd-pristine/ already exists from the * previous install (i.e., the file's sha256 matches the originalHash recorded * in the manifest), preserve it — do NOT wipe and re-populate from the new * release source. This ensures gsd-pristine/ holds old-release bytes even * after an upgrade where the file content changed upstream. * * Regression contract (byte-comparison): * After saveLocalPatches() is called with a user-modified file whose * gsd-pristine/ entry was correctly set by the previous install, the * gsd-pristine/ file MUST still contain the old-release bytes, not the * new-release bytes supplied in pristineCtx.packageSrc. * * Closes: #3407 */ const { test, describe, beforeEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const crypto = require('node:crypto'); const ROOT = path.join(__dirname, '..'); const INSTALL = require(path.join(ROOT, 'bin', 'install.js')); const { cleanup } = require('./helpers.cjs'); const MANIFEST_NAME = 'gsd-file-manifest.json'; const PATCHES_DIR_NAME = 'gsd-local-patches'; function sha256(content) { return crypto.createHash('sha256').update(content instanceof Buffer ? content : Buffer.from(content)).digest('hex'); } // ─── Bug #3407: gsd-pristine/ must preserve OLD-release bytes across upgrade ── describe('Bug #3407: saveLocalPatches preserves old-release pristine across upgrade', () => { let tmpDir; let configDir; let fakeSrcDir; beforeEach((t) => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3407-')); configDir = path.join(tmpDir, 'config'); fakeSrcDir = path.join(tmpDir, 'new-release-src'); fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(fakeSrcDir, { recursive: true }); t.after(() => { cleanup(tmpDir); }); }); /** * Core regression test. * * Timeline: * Install v1: file content = OLD_RELEASE_CONTENT, gsd-pristine/ROOT_FILE * = OLD_RELEASE_CONTENT (correctly set by previous install), * manifest hash = sha256(OLD_RELEASE_CONTENT) * User edits: configDir/ROOT_FILE = USER_MODIFIED_CONTENT * Upgrade v2: pristineCtx.packageSrc has NEW_RELEASE_CONTENT for ROOT_FILE * saveLocalPatches is called before the wipe. * * Expected AFTER fix: gsd-pristine/ROOT_FILE still == OLD_RELEASE_CONTENT * Actual BEFORE fix: gsd-pristine/ROOT_FILE == NEW_RELEASE_CONTENT (stale) */ test('gsd-pristine/ retains old-release bytes when upgrading a user-modified file', () => { const OLD_RELEASE_CONTENT = '# Old Release Content\nThis is v1 pristine.\n'; const NEW_RELEASE_CONTENT = '# New Release Content\nThis is v2 — upstream changed this line.\n'; const USER_MODIFIED_CONTENT = '# Old Release Content\nThis is v1 pristine.\n## User addition\nUser customization here.\n'; const oldHash = sha256(OLD_RELEASE_CONTENT); // Simulate a root-level installed file. Root-level files in the manifest // are denoted without a subdirectory (slash-free relPath). const relPath = 'test-root-file.md'; // Set up configDir: user-modified installed file + manifest recording old hash fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // Set up fakeSrcDir (new release): the file has NEW content fs.writeFileSync(path.join(fakeSrcDir, relPath), NEW_RELEASE_CONTENT); // Set up gsd-pristine/ with OLD content (as correctly populated by previous install) const pristineDir = path.join(configDir, 'gsd-pristine'); fs.mkdirSync(pristineDir, { recursive: true }); fs.writeFileSync(path.join(pristineDir, relPath), OLD_RELEASE_CONTENT); // Call saveLocalPatches with the new release as packageSrc (the buggy scenario) INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); // Assert: gsd-pristine/ must still contain OLD-release bytes const pristineFile = path.join(pristineDir, relPath); assert.ok( fs.existsSync(pristineFile), `gsd-pristine/${relPath} must exist after saveLocalPatches` ); const actualPristineContent = fs.readFileSync(pristineFile, 'utf8'); assert.equal( sha256(actualPristineContent), oldHash, [ `gsd-pristine/${relPath} must contain OLD-release bytes (sha256=${oldHash.slice(0, 12)}…)`, `but got sha256=${sha256(actualPristineContent).slice(0, 12)}…`, `(If equal to sha256(NEW_RELEASE_CONTENT)=${sha256(NEW_RELEASE_CONTENT).slice(0, 12)}… then #3407 is NOT fixed)`, ].join(' ') ); // Secondary: confirm backup-meta records the old hash (not new) const backupMeta = JSON.parse( fs.readFileSync(path.join(configDir, PATCHES_DIR_NAME, 'backup-meta.json'), 'utf8') ); assert.ok( Object.prototype.hasOwnProperty.call(backupMeta.pristine_hashes, relPath), 'backup-meta.json must record pristine_hash for modified file' ); assert.equal( backupMeta.pristine_hashes[relPath], oldHash, 'backup-meta.json pristine_hash must equal old-release hash (not new-release hash)' ); }); /** * Regression test for Codex finding: when gsd-pristine/ entry is absent * (e.g., post-buggy-run deletion or first upgrade without prior pristine) * but the file is UNCHANGED between old and new release, the hash-validated * regeneration path must restore the pristine entry using new-release source. * * When sha256(newReleaseBytesForFile) === originalHash, the file is identical * between releases — new-release generated bytes ARE the old-release pristine * and may be safely promoted. * * Previously (before the regeneration path was added): missing entries were * left absent unconditionally, causing permanent over-broad fallback even * when the file was unchanged upstream. */ test('gsd-pristine/ is regenerated for missing entries when file is unchanged between releases', () => { const SHARED_RELEASE_CONTENT = '# Shared Content\nThis file is identical in v1 and v2.\n'; const USER_MODIFIED_CONTENT = '# Shared Content\nThis file is identical in v1 and v2.\n## User addition\nCustom.\n'; const oldHash = sha256(SHARED_RELEASE_CONTENT); const relPath = 'test-unchanged-file.md'; // configDir has user-modified file + manifest with old-release hash fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // fakeSrcDir (new release) has the SAME content — file was not changed upstream fs.writeFileSync(path.join(fakeSrcDir, relPath), SHARED_RELEASE_CONTENT); // NOTE: gsd-pristine/ does NOT exist (simulating post-buggy-run or first-time scenario) INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); // The regeneration path should have detected that sha256(new-release candidate) // === originalHash, and promoted the candidate into gsd-pristine/. const pristineFile = path.join(configDir, 'gsd-pristine', relPath); assert.ok( fs.existsSync(pristineFile), [ `gsd-pristine/${relPath} must exist after hash-validated regeneration.`, `When new-release bytes hash to originalHash, the file was unchanged between`, `releases and the candidate should be promoted to restore the pristine baseline.`, ].join(' ') ); const actualContent = fs.readFileSync(pristineFile, 'utf8'); assert.equal( sha256(actualContent), oldHash, [ `gsd-pristine/${relPath} must contain bytes matching originalHash after regeneration`, `(sha256=${oldHash.slice(0, 12)}…)`, ].join(' ') ); }); /** * Stale-pristine recovery test (pre-fix bug artifact). * * Timeline: * Buggy run: gsd-pristine/ was written with NEW_RELEASE_CONTENT * (the exact #3407 artifact — stale bytes from a buggy populatePristineDir). * Fix run: saveLocalPatches detects the hash mismatch * (sha256(NEW_RELEASE_CONTENT) !== originalHash recorded in manifest), * removes the stale entry, then attempts regeneration. * * When the file CHANGED between releases (NEW !== OLD): * - The stale entry is removed. * - Regeneration discards the new-release candidate (hash mismatch). * - gsd-pristine/ must be ABSENT (over-broad fallback — correct). * * When the file is UNCHANGED between releases (NEW === OLD): * - The stale entry (which happens to have correct bytes despite the bug) is * detected as correct (hash matches originalHash) and PRESERVED. * - gsd-pristine/ must remain present with the correct bytes. * * This test covers the "file changed across release boundary" case. * The "unchanged" case is already covered by the regeneration test above. */ test('stale gsd-pristine/ entry (new-release bytes) is removed when file changed between releases', () => { const OLD_RELEASE_CONTENT = '# Old Release\nv1 content here.\n'; const NEW_RELEASE_CONTENT = '# New Release\nv2 content — upstream changed this.\n'; const USER_MODIFIED_CONTENT = '# Old Release\nv1 content here.\n## User section\nCustom work.\n'; const oldHash = sha256(OLD_RELEASE_CONTENT); const relPath = 'test-stale-recovery.md'; // configDir: user-modified file + manifest recording OLD hash fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // fakeSrcDir (new release): contains the NEW content fs.writeFileSync(path.join(fakeSrcDir, relPath), NEW_RELEASE_CONTENT); // Pre-populate gsd-pristine/ with NEW_RELEASE_CONTENT — the exact pre-fix bug artifact. // This simulates a prior buggy run that wrote new-release bytes into the pristine baseline. const STALE_BYTES = NEW_RELEASE_CONTENT; // named constant for clarity const pristineDir = path.join(configDir, 'gsd-pristine'); fs.mkdirSync(pristineDir, { recursive: true }); fs.writeFileSync(path.join(pristineDir, relPath), STALE_BYTES); // Verify the pre-condition: stale bytes do NOT match the original hash. // If this assert fails, the test fixture is wrong (not a fix regression). assert.notEqual( sha256(STALE_BYTES), oldHash, 'test fixture check: stale bytes must differ from originalHash' ); INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); // The fix must detect the hash mismatch (stale entry) and remove it. // The regeneration path discards the new-release candidate (its hash !== oldHash). // Result: gsd-pristine/ must be ABSENT — over-broad fallback is the safe outcome. const pristineFile = path.join(pristineDir, relPath); assert.strictEqual( fs.existsSync(pristineFile), false, [ `expected gsd-pristine/${relPath} to be absent after stale-pristine recovery.`, `The stale entry (new-release bytes, sha256=${sha256(STALE_BYTES).slice(0, 12)}…)`, `must be removed; regeneration must discard the candidate because`, `sha256(new-release)=${sha256(NEW_RELEASE_CONTENT).slice(0, 12)}… !== originalHash=${oldHash.slice(0, 12)}….`, `Presence of the file means the stale bytes were NOT cleaned up (pre-fix behavior).`, ].join(' ') ); }); /** * Second scenario: gsd-pristine/ does NOT pre-exist (first upgrade with no * prior pristine population). In this case there is no way to obtain the * old-release pristine bytes — populatePristineDir must NOT write the new- * release bytes either. The correct outcome is: gsd-pristine/ stays empty * for this file, and the verifier falls back to over-broad mode (safe). */ test('gsd-pristine/ stays empty when no prior pristine exists (first upgrade, no stale write)', () => { const OLD_RELEASE_CONTENT = '# Old Release Content\nThis is v1.\n'; const NEW_RELEASE_CONTENT = '# New Release Content\nThis is v2 — changed.\n'; const USER_MODIFIED_CONTENT = '# Old Release Content\nThis is v1.\n## User addition\nCustom.\n'; const oldHash = sha256(OLD_RELEASE_CONTENT); const relPath = 'test-first-upgrade.md'; // configDir has user-modified file + manifest fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // fakeSrcDir (new release) has new content fs.writeFileSync(path.join(fakeSrcDir, relPath), NEW_RELEASE_CONTENT); // NOTE: gsd-pristine/ does NOT exist yet (first upgrade) INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); const pristineFile = path.join(configDir, 'gsd-pristine', relPath); assert.strictEqual( fs.existsSync(pristineFile), false, [ `expected gsd-pristine/${relPath} to be absent when file changed across release boundary.`, `Writing new-release bytes as pristine for a file whose hash is unknown leads to`, `false FAIL_USER_LINES_MISSING in the reapply-patches verifier (#3407).`, `Over-broad fallback mode is the correct outcome here.`, ].join(' ') ); }); }); // The former "Antipattern hunt" describe block (structural typeof checks only) was // removed — it provided no real behavioral coverage and was a vacuous-truth pattern // per /test-rigor skill. Behavioral tests for populatePristineDir are covered above. }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/fix-1679-destsubpath-confinement.test.cjs — consolidation epic #1969 (B5 #1974) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:fix-1679-destsubpath-confinement (consolidation epic #1969 B5 #1974)", () => { 'use strict'; /** * Tests for ADR-1239 Phase B: destSubpath write-confinement security gate. * * Verifies that assertDestWithinConfigHome rejects escaping destSubpath values * and that createRuntimeArtifactInstallPlan and createRuntimeArtifactUninstallPlan * both reject them at plan-build time. * * Also covers: * F3 - assertDestWithinConfigHome rejects destSubpath === configHome itself * F4 - migrateLegacyDevPreferencesToSkill routes through the confinement gate * F2 - write sites (installOpencodeFamilySkills) reject symlink-escaping destDir */ const { test, describe, beforeEach, afterEach } = 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 { assertDestWithinConfigHome, createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan, } = require('../gsd-core/bin/lib/runtime-artifact-install-plan.cjs'); const { migrateLegacyDevPreferencesToSkill, installOpencodeFamilySkills, installRuntimeArtifacts, _copyStaged, } = require('../gsd-core/bin/lib/install-engine.cjs'); const { createTempDir, cleanup } = require('./helpers.cjs'); // --------------------------------------------------------------------------- // Unit tests for assertDestWithinConfigHome // --------------------------------------------------------------------------- describe('assertDestWithinConfigHome', () => { let configDir; beforeEach(() => { configDir = createTempDir('gsd-confine-test-'); }); afterEach(() => { cleanup(configDir); }); // --- Rejection cases --- test('rejects destSubpath "../../etc" that escapes configDir', () => { assert.throws( () => assertDestWithinConfigHome(configDir, '../../etc'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('escapes configHome'), `expected "escapes configHome" in: ${err.message}`, ); return true; }, ); }); test('rejects destSubpath "../foo" that escapes configDir', () => { assert.throws( () => assertDestWithinConfigHome(configDir, '../foo'), /escapes configHome/, ); }); test('rejects destSubpath "a/../../b" that escapes configDir', () => { assert.throws( () => assertDestWithinConfigHome(configDir, 'a/../../b'), /escapes configHome/, ); }); test('rejects destSubpath containing a NUL byte', () => { assert.throws( () => assertDestWithinConfigHome(configDir, 'skills\0evil'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('NUL'), `expected "NUL" in: ${err.message}`, ); return true; }, ); }); // --- F3: reject destSubpath that resolves to configHome itself --- test('F3: rejects destSubpath "." that resolves to configHome itself', () => { assert.throws( () => assertDestWithinConfigHome(configDir, '.'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('not configHome itself') || err.message.includes('escapes configHome'), `expected confinement error in: ${err.message}`, ); return true; }, ); }); test('F3: rejects destSubpath "a/.." that resolves to configHome itself', () => { assert.throws( () => assertDestWithinConfigHome(configDir, 'a/..'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('not configHome itself') || err.message.includes('escapes configHome'), `expected confinement error in: ${err.message}`, ); return true; }, ); }); test('F3: rejects destSubpath "skills/../.." that resolves to configHome parent', () => { assert.throws( () => assertDestWithinConfigHome(configDir, 'skills/../..'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('not configHome itself') || err.message.includes('escapes configHome'), `expected confinement error in: ${err.message}`, ); return true; }, ); }); // --- Accepted cases --- test('accepts "skills" and returns path under configDir', () => { const result = assertDestWithinConfigHome(configDir, 'skills'); assert.ok( result.startsWith(path.resolve(configDir)), `expected result to start with configDir (${path.resolve(configDir)}), got: ${result}`, ); assert.strictEqual(result, path.join(path.resolve(configDir), 'skills')); }); test('accepts "commands/gsd" and returns path under configDir', () => { const result = assertDestWithinConfigHome(configDir, 'commands/gsd'); assert.ok(result.startsWith(path.resolve(configDir))); assert.strictEqual(result, path.join(path.resolve(configDir), 'commands', 'gsd')); }); test('accepts "./skills" and returns resolved path under configDir', () => { const result = assertDestWithinConfigHome(configDir, './skills'); assert.ok(result.startsWith(path.resolve(configDir))); assert.strictEqual(result, path.join(path.resolve(configDir), 'skills')); }); test('does not match a sibling directory with a shared prefix', () => { // configDir = /tmp/gsd-foo; a sibling like /tmp/gsd-foobar must NOT be accepted. // The path.sep guard in the implementation prevents a startsWith match // from crossing directory boundaries. We verify the happy-path: a valid // nested subpath resolves to a path strictly under configDir (includes sep). const result = assertDestWithinConfigHome(configDir, 'subdir/nested'); assert.ok(result.startsWith(path.resolve(configDir) + path.sep)); }); }); // --------------------------------------------------------------------------- // Integration tests for createRuntimeArtifactInstallPlan // --------------------------------------------------------------------------- describe('createRuntimeArtifactInstallPlan destSubpath confinement', () => { let configDir; beforeEach(() => { configDir = createTempDir('gsd-plan-confine-'); }); afterEach(() => { cleanup(configDir); }); function noopStage() { return '/tmp/staged-noop'; } function makeLayout(destSubpath) { return { runtime: 'claude', configDir, scope: 'global', kinds: [ { kind: 'skills', destSubpath, prefix: 'gsd-', stage: noopStage, }, ], }; } test('rejects an escaping destSubpath ("../../escape") at plan-build time', () => { const layout = makeLayout('../../escape'); assert.throws( () => createRuntimeArtifactInstallPlan({ layout, resolvedProfile: { name: 'core' }, deps: { rewriteStagedSkillBodies: () => undefined, rewriteStagedCommandBodies: () => undefined, }, }), (err) => { assert.ok(err instanceof Error); assert.ok( err.message.includes('escapes'), `expected "escapes" in: ${err.message}`, ); return true; }, ); }); test('normal destSubpath produces plan with destDir under configDir', () => { const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-staged-')); try { const layout = { runtime: 'claude', configDir, scope: 'global', kinds: [ { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-', stage: () => stagedDir, }, ], }; const result = createRuntimeArtifactInstallPlan({ layout, resolvedProfile: { name: 'core' }, deps: { rewriteStagedSkillBodies: () => undefined, rewriteStagedCommandBodies: () => undefined, }, }); assert.strictEqual(result.ok, true, 'plan must succeed for normal destSubpath'); assert.strictEqual(result.plan.items.length, 1); const destDir = result.plan.items[0].destDir; assert.ok( destDir.startsWith(path.resolve(configDir)), `destDir (${destDir}) must be under configDir (${configDir})`, ); } finally { cleanup(stagedDir); } }); }); // --------------------------------------------------------------------------- // Integration tests for createRuntimeArtifactUninstallPlan // --------------------------------------------------------------------------- describe('createRuntimeArtifactUninstallPlan destSubpath confinement', () => { let configDir; beforeEach(() => { configDir = createTempDir('gsd-uninstall-confine-'); }); afterEach(() => { cleanup(configDir); }); function makeUninstallLayout(destSubpath) { return { runtime: 'claude', configDir, kinds: [ { kind: 'skills', destSubpath, prefix: 'gsd-', stage: () => '/tmp/staged-noop', }, ], }; } test('rejects an escaping destSubpath ("../../escape") at uninstall-plan-build time', () => { const layout = makeUninstallLayout('../../escape'); assert.throws( () => createRuntimeArtifactUninstallPlan(layout), (err) => { assert.ok(err instanceof Error); assert.ok( err.message.includes('escapes'), `expected "escapes" in: ${err.message}`, ); return true; }, ); }); test('rejects destSubpath "../outside" at uninstall-plan-build time', () => { const layout = makeUninstallLayout('../outside'); assert.throws( () => createRuntimeArtifactUninstallPlan(layout), /escapes/, ); }); test('normal destSubpath produces uninstall plan with destDir under configDir', () => { const layout = makeUninstallLayout('skills'); const plan = createRuntimeArtifactUninstallPlan(layout); assert.strictEqual(plan.items.length, 1); const destDir = plan.items[0].destDir; assert.ok( destDir.startsWith(path.resolve(configDir)), `destDir (${destDir}) must be under configDir (${configDir})`, ); assert.strictEqual(destDir, path.join(path.resolve(configDir), 'skills')); }); test('normal nested destSubpath ("commands/gsd") produces uninstall plan with destDir under configDir', () => { const layout = makeUninstallLayout('commands/gsd'); const plan = createRuntimeArtifactUninstallPlan(layout); assert.strictEqual(plan.items.length, 1); const destDir = plan.items[0].destDir; assert.ok( destDir.startsWith(path.resolve(configDir)), `destDir (${destDir}) must be under configDir (${configDir})`, ); assert.strictEqual(destDir, path.join(path.resolve(configDir), 'commands', 'gsd')); }); }); // --------------------------------------------------------------------------- // F4: migrateLegacyDevPreferencesToSkill must route through the confinement gate // --------------------------------------------------------------------------- describe('F4: migrateLegacyDevPreferencesToSkill confinement', () => { let configDir; let outsideDir; beforeEach(() => { configDir = createTempDir('gsd-f4-confine-'); outsideDir = createTempDir('gsd-f4-outside-'); }); afterEach(() => { cleanup(configDir); cleanup(outsideDir); }); test('F4: migrateLegacyDevPreferencesToSkill throws when destSubpath resolves to configHome itself (via mocked layout with "." destSubpath)', () => { // We cannot easily inject a bad destSubpath through the real layout resolver // (it resolves to a real valid path). Instead we validate that the function // uses assertDestWithinConfigHome by passing a runtime whose layout's // skillsKindEntry.destSubpath, when joined with configDir, would escape — but // since real layouts are always safe, we test the guard on a deliberately // crafted saved map calling the real function and observing the path written // is always within configDir for a real runtime. // // Real-layout sanity: verify 'opencode' produces a write inside configDir. const savedLegacy = new Map([['dev-preferences.md', '# dev prefs\n']]); // Real opencode layout — should succeed without throwing assert.doesNotThrow(() => { migrateLegacyDevPreferencesToSkill(configDir, savedLegacy, 'opencode', 'global'); }, 'migrateLegacyDevPreferencesToSkill with real opencode layout must not throw'); // Verify the written file is inside configDir const written = []; function findMd(dir) { if (!fs.existsSync(dir)) return; for (const e of fs.readdirSync(dir, { withFileTypes: true })) { if (e.isDirectory()) findMd(path.join(dir, e.name)); else if (e.name.endsWith('.md')) written.push(path.join(dir, e.name)); } } findMd(configDir); assert.ok(written.length > 0, 'at least one .md must have been written'); for (const f of written) { assert.ok( f.startsWith(path.resolve(configDir) + path.sep), `written file ${f} must be inside configDir ${configDir}`, ); } }); test('F4: migrateLegacyDevPreferencesToSkill uses assertDestWithinConfigHome — path.join on configDir+destSubpath cannot escape via symlink in destSubpath string', () => { // Validate that the guard (assertDestWithinConfigHome) would have caught a // manipulated destSubpath value. We simulate by calling assertDestWithinConfigHome // directly with a "."-equivalent subpath (F3 guard) to prove F4 now relies on it. assert.throws( () => assertDestWithinConfigHome(configDir, '.'), (err) => { assert.ok(err instanceof Error); return true; }, 'assertDestWithinConfigHome must reject "." (used by F4 guard)', ); }); }); // --------------------------------------------------------------------------- // F2: write sites reject a symlink-escaping destDir // --------------------------------------------------------------------------- describe('F2: installOpencodeFamilySkills rejects symlink-escaping destDir', () => { let configDir; let outsideDir; let symlinkTarget; beforeEach(() => { configDir = createTempDir('gsd-f2-config-'); outsideDir = createTempDir('gsd-f2-outside-'); // Create a symlink inside configDir pointing outside symlinkTarget = path.join(configDir, 'skills'); fs.symlinkSync(outsideDir, symlinkTarget); }); afterEach(() => { // Remove symlink before cleanup to avoid errors try { fs.unlinkSync(symlinkTarget); } catch { /* already gone */ } cleanup(configDir); cleanup(outsideDir); }); test('F2: installOpencodeFamilySkills throws when skills/ is a symlink pointing outside configDir', () => { // Create a minimal rawCommandsDir with one .md file const rawDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-f2-raw-')); try { fs.writeFileSync(path.join(rawDir, 'help.md'), '# help\n', 'utf8'); assert.throws( () => installOpencodeFamilySkills('opencode', configDir, rawDir, '~/.opencode/'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.toLowerCase().includes('symlink') || err.message.toLowerCase().includes('escap') || err.message.toLowerCase().includes('outside') || err.message.toLowerCase().includes('confinement'), `expected symlink/escape error in: ${err.message}`, ); return true; }, ); // Verify nothing was written to outsideDir const outsideFiles = fs.readdirSync(outsideDir); assert.strictEqual(outsideFiles.length, 0, 'must not have written anything outside configDir'); } finally { cleanup(rawDir); } }); }); // --------------------------------------------------------------------------- // M1: _copyStaged defense-in-depth must also reject dest === configRoot // --------------------------------------------------------------------------- describe('M1: _copyStaged rejects dest equal to configRoot', () => { let configDir; let stagedDir; beforeEach(() => { configDir = createTempDir('gsd-m1-config-'); stagedDir = createTempDir('gsd-m1-staged-'); // Write a dummy file into stagedDir so _copyStaged has something to copy fs.writeFileSync(path.join(stagedDir, 'help.md'), '# help\n', 'utf8'); }); afterEach(() => { cleanup(configDir); cleanup(stagedDir); }); test('M1: _copyStaged throws when destDir equals configRoot (was silently accepted before fix)', () => { // dest === configRoot: the canonical gate (assertDestWithinConfigHome) rejects // resolved === root with "escapes configHome" / "not configHome itself". assert.throws( () => _copyStaged(stagedDir, configDir, { kind: 'commands', destSubpath: '.', prefix: 'gsd-' }, configDir), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('escapes configHome') || err.message.includes('not configHome itself') || err.message.includes('outside') || err.message.includes('inside'), `expected confinement error in: ${err.message}`, ); return true; }, ); }); test('M1: _copyStaged throws when destDir is outside configRoot', () => { const outsideDir = createTempDir('gsd-m1-outside-'); try { assert.throws( () => _copyStaged(stagedDir, outsideDir, { kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' }, configDir), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( // After EDIT 1, _copyStaged delegates to assertDestWithinConfigHome which // emits "escapes configHome"; the old "_copyStaged" prefix is no longer present. err.message.includes('escapes configHome') || err.message.includes('strict subpath') || err.message.includes('refusing'), `expected confinement error in: ${err.message}`, ); return true; }, ); } finally { cleanup(outsideDir); } }); test('M1: _copyStaged accepts destDir strictly under configRoot', () => { const destDir = path.join(configDir, 'commands', 'gsd'); fs.mkdirSync(destDir, { recursive: true }); // Should not throw — just copies (stagedDir has help.md, kind=commands) assert.doesNotThrow( () => _copyStaged(stagedDir, destDir, { kind: 'commands', destSubpath: 'commands/gsd', prefix: 'gsd-' }, configDir), ); }); }); // --------------------------------------------------------------------------- // L2: symlink guard BEFORE mkdirSync in installRuntimeArtifacts // --------------------------------------------------------------------------- describe('L2: installRuntimeArtifacts rejects symlink-escaping dest before mkdirSync', () => { let configDir; let outsideDir; beforeEach(() => { configDir = createTempDir('gsd-l2-config-'); outsideDir = createTempDir('gsd-l2-outside-'); // Create configDir/skills as a symlink pointing outside fs.symlinkSync(outsideDir, path.join(configDir, 'skills')); }); afterEach(() => { // Remove symlink before cleanup to avoid crossing dir boundaries try { fs.unlinkSync(path.join(configDir, 'skills')); } catch { /* already gone */ } cleanup(configDir); cleanup(outsideDir); }); test('L2: installRuntimeArtifacts throws before creating dirs when skills/ is a symlink pointing outside', () => { // Use the full profile shape (skills: '*') so staging short-circuits early // and the symlink guard is the first thing that fires. assert.throws( () => installRuntimeArtifacts('opencode', configDir, 'global', { name: 'full', skills: '*', agents: new Set() }), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.toLowerCase().includes('symlink') || err.message.toLowerCase().includes('escap') || err.message.toLowerCase().includes('outside') || err.message.toLowerCase().includes('confinement') || err.message.toLowerCase().includes('install root'), `expected symlink/escape error in: ${err.message}`, ); return true; }, ); // The symlink itself still exists but no new entries were created in outsideDir const outsideEntries = fs.readdirSync(outsideDir); assert.strictEqual(outsideEntries.length, 0, 'must not have created any dirs/files outside configDir'); }); }); // --------------------------------------------------------------------------- // L1: symlink guard in migrateLegacyDevPreferencesToSkill // --------------------------------------------------------------------------- describe('L1: migrateLegacyDevPreferencesToSkill rejects symlink-escaping skillDir', () => { let configDir; let outsideDir; beforeEach(() => { configDir = createTempDir('gsd-l1-config-'); outsideDir = createTempDir('gsd-l1-outside-'); // Create configDir/skills as a symlink pointing outside fs.symlinkSync(outsideDir, path.join(configDir, 'skills')); }); afterEach(() => { try { fs.unlinkSync(path.join(configDir, 'skills')); } catch { /* already gone */ } cleanup(configDir); cleanup(outsideDir); }); test('L1: migrateLegacyDevPreferencesToSkill throws when skills/ is a symlink pointing outside', () => { const saved = new Map([['dev-preferences.md', '# dev prefs\n']]); assert.throws( () => migrateLegacyDevPreferencesToSkill(configDir, saved, 'opencode', 'global'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.toLowerCase().includes('symlink') || err.message.toLowerCase().includes('escap') || err.message.toLowerCase().includes('outside') || err.message.toLowerCase().includes('install root'), `expected symlink/escape error in: ${err.message}`, ); return true; }, ); // Nothing must have been written outside const outsideFiles = fs.readdirSync(outsideDir); assert.strictEqual(outsideFiles.length, 0, 'must not have written anything outside configDir'); }); }); // --------------------------------------------------------------------------- // L3: relative configDir support // --------------------------------------------------------------------------- describe('L3: assertDestWithinConfigHome handles relative configDir', () => { test('L3: throws when relative configDir + escaping destSubpath resolves outside', () => { // path.resolve handles relative roots; '../../etc' from '.' would escape assert.throws( () => assertDestWithinConfigHome('.', '../../etc'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('escapes configHome') || err.message.includes('outside'), `expected escape error in: ${err.message}`, ); return true; }, ); }); test('L3: throws when "." destSubpath resolves to the relative configDir itself', () => { // '.' resolves to the same directory as the configDir — must be rejected (F3) assert.throws( () => assertDestWithinConfigHome('.', '.'), /escapes configHome|not configHome itself/, ); }); test('L3: accepts "skills" under relative "./somedir" and returns absolute path', () => { const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-l3-')); const relDir = path.relative(process.cwd(), tmpBase); try { const result = assertDestWithinConfigHome(relDir, 'skills'); const expectedBase = path.resolve(relDir); assert.ok( result.startsWith(expectedBase + path.sep), `result (${result}) must be under resolved relDir (${expectedBase})`, ); assert.strictEqual(result, path.join(expectedBase, 'skills')); } finally { fs.rmdirSync(tmpBase); } }); }); // --------------------------------------------------------------------------- // N1: sibling-prefix NEGATIVE assertion // --------------------------------------------------------------------------- describe('N1: sibling directory with shared prefix is rejected', () => { test('N1: rejects sibling path sharing a prefix with configDir', () => { // /tmp/gsd-foobar is NOT inside /tmp/gsd-foo — must throw despite the // startsWith prefix overlap at the string level (the sep-check prevents it). assert.throws( () => assertDestWithinConfigHome('/tmp/gsd-foo', '../gsd-foobar'), (err) => { assert.ok(err instanceof Error, 'must be an Error'); assert.ok( err.message.includes('escapes configHome') || err.message.includes('outside'), `expected confinement error in: ${err.message}`, ); return true; }, ); }); test('N1: accepts a true child subpath inside configDir', () => { // 'bar' appended INSIDE /tmp/gsd-foo => the child path — accepted. // Compute expected via path.resolve (the same primitive the helper uses) so // the assertion is platform-portable: on Windows path.resolve prepends the // cwd drive (C:\...) and uses backslashes, which a hardcoded posix literal / // path.join (no drive) would not match (#1679 Windows-CI portability). const root = path.resolve('/tmp/gsd-foo'); const result = assertDestWithinConfigHome('/tmp/gsd-foo', 'bar'); assert.strictEqual(result, path.resolve('/tmp/gsd-foo', 'bar')); assert.ok(result.startsWith(root + path.sep)); }); test('N1: the accepted child does not imply the sibling is accepted', () => { // Double-check: 'bar' inside is fine, but '../gsd-foobar' (the sibling) is not. // 'bar' resolves to /tmp/gsd-foo/bar ✓ assert.doesNotThrow(() => assertDestWithinConfigHome('/tmp/gsd-foo', 'bar')); // '../gsd-foobar' resolves to /tmp/gsd-foobar — NOT inside /tmp/gsd-foo assert.throws( () => assertDestWithinConfigHome('/tmp/gsd-foo', '../gsd-foobar'), /escapes configHome/, ); }); }); // --------------------------------------------------------------------------- // N3: Windows-separator coverage (structural guard using path.win32) // --------------------------------------------------------------------------- describe('N3: Windows-separator confinement logic (path.win32 semantics)', () => { /** * Replicate the assertDestWithinConfigHome predicate using path.win32 * so we can test the sep-guard logic on any platform. * * This mirrors the implementation in runtime-artifact-install-plan.cjs * but forces win32 path semantics. */ function assertDestWithinConfigHomeWin32(configDir, destSubpath) { if (destSubpath.includes('\0')) { throw new Error(`destSubpath "${destSubpath}" contains a NUL byte and is not valid`); } const root = path.win32.resolve(configDir); const resolved = path.win32.resolve(configDir, destSubpath); if (resolved === root || !resolved.startsWith(root + path.win32.sep)) { throw new Error( `destSubpath "${destSubpath}" must be a strict subpath of configHome "${configDir}" — not configHome itself or outside it (escapes configHome)`, ); } return resolved; } const winRoot = 'C:\\Users\\me\\.claude'; test('N3: rejects ..\\..\\Windows (Windows backslash traversal)', () => { assert.throws( () => assertDestWithinConfigHomeWin32(winRoot, '..\\..\\Windows'), /escapes configHome/, ); }); test('N3: rejects mixed ../..\\x traversal', () => { assert.throws( () => assertDestWithinConfigHomeWin32(winRoot, '../..\\x'), /escapes configHome/, ); }); test('N3: rejects "." that resolves to configHome itself', () => { assert.throws( () => assertDestWithinConfigHomeWin32(winRoot, '.'), /escapes configHome/, ); }); test('N3: accepts "skills" under Windows root', () => { const result = assertDestWithinConfigHomeWin32(winRoot, 'skills'); assert.strictEqual(result, path.win32.join(winRoot, 'skills')); assert.ok(result.startsWith(winRoot + path.win32.sep)); }); test('N3: accepts "commands\\gsd" (Windows nested path) under Windows root', () => { const result = assertDestWithinConfigHomeWin32(winRoot, 'commands\\gsd'); assert.strictEqual(result, path.win32.join(winRoot, 'commands', 'gsd')); assert.ok(result.startsWith(winRoot + path.win32.sep)); }); test('N3: rejects sibling C:\\Users\\me\\.claude-extra under win32 semantics', () => { assert.throws( () => assertDestWithinConfigHomeWin32(winRoot, '..\\.claude-extra'), /escapes configHome/, ); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2998-pristine-dir-populated.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2998-pristine-dir-populated (consolidation epic #1969 B1 #1970)", () => { 'use strict'; process.env.GSD_TEST_MODE = '1'; /** * Bug #2998: gsd-pristine/ snapshot is documented but never populated by * the installer. saveLocalPatches declared a pristineDir variable and * promised "saves pristine copies (from manifest) to gsd-pristine/ to * enable three-way merge during reapply-patches" -- but no code ever * wrote to that directory. Effect: the /gsd-reapply-patches Step 5 * verifier (#2972) silently degrades to its over-broad fallback heuristic * ("every significant backup line"), exactly the silent-success-on-lost- * content failure mode #2969 was designed to prevent. * * Fix: new populatePristineDir({...}) helper runs the install transform * pipeline (copyWithPathReplacement) into a tmp staging dir, then copies * out the modified-file paths into gsd-pristine/. saveLocalPatches now * accepts a pristineCtx and calls the helper when local patches are * detected. */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const crypto = require('node:crypto'); const ROOT = path.join(__dirname, '..'); const INSTALL = require(path.join(ROOT, 'bin', 'install.js')); const { cleanup } = require('./helpers.cjs'); function sha256(content) { return crypto.createHash('sha256').update(content).digest('hex'); } describe('Bug #2998: populatePristineDir is exported and writes pristine for modified files', () => { test('exported as a function', () => { assert.equal(typeof INSTALL.populatePristineDir, 'function', 'expected populatePristineDir in install.js exports (#2998)'); }); test('returns 0 when no files are modified (no-op)', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-')); try { const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir: path.join(tmp, 'gsd-pristine'), modified: [], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 0); } finally { cleanup(tmp); } }); test('writes one pristine file per modified path that exists in source', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { // Pick a real installed-side relPath from the package source. The // install transforms map source `gsd-core/` to installed // `gsd-core/` for skills-aware runtimes (like claude), // so the relPath is the same on both sides. const candidate = path.join('gsd-core', 'workflows', 'reapply-patches.md'); const sourcePath = path.join(ROOT, candidate); assert.equal(fs.existsSync(sourcePath), true, `precondition: source file exists at ${candidate}`); const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: [candidate], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 1, 'expected exactly one pristine file written'); const out = path.join(pristineDir, candidate); assert.equal(fs.existsSync(out), true, `expected pristine file at ${out}`); // The pristine content should be the transformed version (not raw source): // copyWithPathReplacement substitutes ~/.claude/ for the runtime path prefix. // For claude+global, the prefix is $HOME/.claude/ which equals the original, // so the transform is effectively identity here. We assert the content is a // non-empty markdown file rather than asserting on transform specifics. const content = fs.readFileSync(out, 'utf-8'); assert.ok(content.length > 0, 'pristine file should be non-empty'); } finally { cleanup(tmp); } }); test('skips paths not present in source (does not corrupt pristine with stale data)', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: ['gsd-core/this-path-does-not-exist.md'], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 0, 'expected zero pristine files for non-existent source paths'); const out = path.join(pristineDir, 'gsd-core/this-path-does-not-exist.md'); assert.equal(fs.existsSync(out), false, 'pristine should not contain ghost paths'); } finally { cleanup(tmp); } }); test('pristine files have stable content (transformations are deterministic)', () => { // Determinism is what makes the verifier's hash check meaningful: // backup-meta.json records pristine_hashes computed at this same step, // so re-running with the same inputs must yield byte-identical files. const tmp1 = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-d1-')); const tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-d2-')); try { const candidate = path.join('gsd-core', 'workflows', 'reapply-patches.md'); const ctx = { packageSrc: ROOT, modified: [candidate], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }; INSTALL.populatePristineDir(Object.assign({ pristineDir: path.join(tmp1, 'gsd-pristine') }, ctx)); INSTALL.populatePristineDir(Object.assign({ pristineDir: path.join(tmp2, 'gsd-pristine') }, ctx)); const a = fs.readFileSync(path.join(tmp1, 'gsd-pristine', candidate)); const b = fs.readFileSync(path.join(tmp2, 'gsd-pristine', candidate)); assert.equal(sha256(a), sha256(b), 'two runs of the same inputs must yield identical pristine content'); } finally { cleanup(tmp1); cleanup(tmp2); } }); }); // ─── #3004 CR follow-up: multi-root pristine expansion ───────────────────── describe('Bug #2998 (#3004 CR): pristine expansion covers every manifest install root', () => { test('paths under agents/ are staged via copyWithPathReplacement, not silently skipped', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-multi-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { const candidate = path.join('agents', 'gsd-planner.md'); const sourcePath = path.join(ROOT, candidate); assert.equal(fs.existsSync(sourcePath), true, `precondition: source file exists at ${candidate}`); const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: [candidate], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 1, 'expected agents/ path to be staged and copied to pristine'); assert.equal(fs.existsSync(path.join(pristineDir, candidate)), true); } finally { cleanup(tmp); } }); test('a mix of gsd-core/ and agents/ paths in modified list are all staged', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2998-mix-')); const pristineDir = path.join(tmp, 'gsd-pristine'); try { const a = path.join('gsd-core', 'workflows', 'reapply-patches.md'); const b = path.join('agents', 'gsd-planner.md'); assert.equal(fs.existsSync(path.join(ROOT, a)), true); assert.equal(fs.existsSync(path.join(ROOT, b)), true); const written = INSTALL.populatePristineDir({ packageSrc: ROOT, pristineDir, modified: [a, b], runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); assert.equal(written, 2, 'expected both top-level dirs to be staged'); assert.equal(fs.existsSync(path.join(pristineDir, a)), true); assert.equal(fs.existsSync(path.join(pristineDir, b)), true); } finally { cleanup(tmp); } }); }); describe('Bug #2998: saveLocalPatches no longer leaves the pristineDir variable unused', () => { test('saveLocalPatches accepts a pristineCtx and exposes the helper for direct testing', () => { // Structural assertion: the function exists with the new signature shape. // Behavioral end-to-end is covered by the populatePristineDir tests above // (that helper is what saveLocalPatches calls internally). assert.equal(typeof INSTALL.populatePristineDir, 'function'); // The signature for saveLocalPatches isn't exported, but the helper IS, // and it's the unit of behavior the bug is about. Asserting on the helper // is the structural-IR equivalent of the no-source-grep convention. }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-3407-pristine-stale-content.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-3407-pristine-stale-content (consolidation epic #1969 B1 #1970)", () => { 'use strict'; process.env.GSD_TEST_MODE = '1'; /** * Bug #3407: Installer leaves stale content in gsd-pristine/ * * Root cause: populatePristineDir() in saveLocalPatches() snapshots from * pristineCtx.packageSrc — the NEWLY-downloaded release tree — and writes * those bytes into gsd-pristine/. For files changed between the old and new * release, this writes the NEW bytes into the pristine baseline instead of * the OLD bytes. The three-way-diff verifier then classifies upstream-changed * lines as user-added → Step 5a gate fails with false FAIL_USER_LINES_MISSING. * * The #3657 fix (OK_PRISTINE_DRIFT_DETECTED) was a symptom workaround: the * verifier detects hash mismatch (backup-meta.json records old-release hash * but gsd-pristine/ has new-release bytes) and skips to over-broad mode * instead of false-failing. The root-cause stale write was never fixed. * * Fix: when a correctly-populated gsd-pristine/ already exists from the * previous install (i.e., the file's sha256 matches the originalHash recorded * in the manifest), preserve it — do NOT wipe and re-populate from the new * release source. This ensures gsd-pristine/ holds old-release bytes even * after an upgrade where the file content changed upstream. * * Regression contract (byte-comparison): * After saveLocalPatches() is called with a user-modified file whose * gsd-pristine/ entry was correctly set by the previous install, the * gsd-pristine/ file MUST still contain the old-release bytes, not the * new-release bytes supplied in pristineCtx.packageSrc. * * Closes: #3407 */ const { test, describe, beforeEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const crypto = require('node:crypto'); const ROOT = path.join(__dirname, '..'); const INSTALL = require(path.join(ROOT, 'bin', 'install.js')); const { cleanup } = require('./helpers.cjs'); const MANIFEST_NAME = 'gsd-file-manifest.json'; const PATCHES_DIR_NAME = 'gsd-local-patches'; function sha256(content) { return crypto.createHash('sha256').update(content instanceof Buffer ? content : Buffer.from(content)).digest('hex'); } // ─── Bug #3407: gsd-pristine/ must preserve OLD-release bytes across upgrade ── describe('Bug #3407: saveLocalPatches preserves old-release pristine across upgrade', () => { let tmpDir; let configDir; let fakeSrcDir; beforeEach((t) => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3407-')); configDir = path.join(tmpDir, 'config'); fakeSrcDir = path.join(tmpDir, 'new-release-src'); fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(fakeSrcDir, { recursive: true }); t.after(() => { cleanup(tmpDir); }); }); /** * Core regression test. * * Timeline: * Install v1: file content = OLD_RELEASE_CONTENT, gsd-pristine/ROOT_FILE * = OLD_RELEASE_CONTENT (correctly set by previous install), * manifest hash = sha256(OLD_RELEASE_CONTENT) * User edits: configDir/ROOT_FILE = USER_MODIFIED_CONTENT * Upgrade v2: pristineCtx.packageSrc has NEW_RELEASE_CONTENT for ROOT_FILE * saveLocalPatches is called before the wipe. * * Expected AFTER fix: gsd-pristine/ROOT_FILE still == OLD_RELEASE_CONTENT * Actual BEFORE fix: gsd-pristine/ROOT_FILE == NEW_RELEASE_CONTENT (stale) */ test('gsd-pristine/ retains old-release bytes when upgrading a user-modified file', () => { const OLD_RELEASE_CONTENT = '# Old Release Content\nThis is v1 pristine.\n'; const NEW_RELEASE_CONTENT = '# New Release Content\nThis is v2 — upstream changed this line.\n'; const USER_MODIFIED_CONTENT = '# Old Release Content\nThis is v1 pristine.\n## User addition\nUser customization here.\n'; const oldHash = sha256(OLD_RELEASE_CONTENT); // Simulate a root-level installed file. Root-level files in the manifest // are denoted without a subdirectory (slash-free relPath). const relPath = 'test-root-file.md'; // Set up configDir: user-modified installed file + manifest recording old hash fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // Set up fakeSrcDir (new release): the file has NEW content fs.writeFileSync(path.join(fakeSrcDir, relPath), NEW_RELEASE_CONTENT); // Set up gsd-pristine/ with OLD content (as correctly populated by previous install) const pristineDir = path.join(configDir, 'gsd-pristine'); fs.mkdirSync(pristineDir, { recursive: true }); fs.writeFileSync(path.join(pristineDir, relPath), OLD_RELEASE_CONTENT); // Call saveLocalPatches with the new release as packageSrc (the buggy scenario) INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); // Assert: gsd-pristine/ must still contain OLD-release bytes const pristineFile = path.join(pristineDir, relPath); assert.ok( fs.existsSync(pristineFile), `gsd-pristine/${relPath} must exist after saveLocalPatches` ); const actualPristineContent = fs.readFileSync(pristineFile, 'utf8'); assert.equal( sha256(actualPristineContent), oldHash, [ `gsd-pristine/${relPath} must contain OLD-release bytes (sha256=${oldHash.slice(0, 12)}…)`, `but got sha256=${sha256(actualPristineContent).slice(0, 12)}…`, `(If equal to sha256(NEW_RELEASE_CONTENT)=${sha256(NEW_RELEASE_CONTENT).slice(0, 12)}… then #3407 is NOT fixed)`, ].join(' ') ); // Secondary: confirm backup-meta records the old hash (not new) const backupMeta = JSON.parse( fs.readFileSync(path.join(configDir, PATCHES_DIR_NAME, 'backup-meta.json'), 'utf8') ); assert.ok( Object.prototype.hasOwnProperty.call(backupMeta.pristine_hashes, relPath), 'backup-meta.json must record pristine_hash for modified file' ); assert.equal( backupMeta.pristine_hashes[relPath], oldHash, 'backup-meta.json pristine_hash must equal old-release hash (not new-release hash)' ); }); /** * Regression test for Codex finding: when gsd-pristine/ entry is absent * (e.g., post-buggy-run deletion or first upgrade without prior pristine) * but the file is UNCHANGED between old and new release, the hash-validated * regeneration path must restore the pristine entry using new-release source. * * When sha256(newReleaseBytesForFile) === originalHash, the file is identical * between releases — new-release generated bytes ARE the old-release pristine * and may be safely promoted. * * Previously (before the regeneration path was added): missing entries were * left absent unconditionally, causing permanent over-broad fallback even * when the file was unchanged upstream. */ test('gsd-pristine/ is regenerated for missing entries when file is unchanged between releases', () => { const SHARED_RELEASE_CONTENT = '# Shared Content\nThis file is identical in v1 and v2.\n'; const USER_MODIFIED_CONTENT = '# Shared Content\nThis file is identical in v1 and v2.\n## User addition\nCustom.\n'; const oldHash = sha256(SHARED_RELEASE_CONTENT); const relPath = 'test-unchanged-file.md'; // configDir has user-modified file + manifest with old-release hash fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // fakeSrcDir (new release) has the SAME content — file was not changed upstream fs.writeFileSync(path.join(fakeSrcDir, relPath), SHARED_RELEASE_CONTENT); // NOTE: gsd-pristine/ does NOT exist (simulating post-buggy-run or first-time scenario) INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); // The regeneration path should have detected that sha256(new-release candidate) // === originalHash, and promoted the candidate into gsd-pristine/. const pristineFile = path.join(configDir, 'gsd-pristine', relPath); assert.ok( fs.existsSync(pristineFile), [ `gsd-pristine/${relPath} must exist after hash-validated regeneration.`, `When new-release bytes hash to originalHash, the file was unchanged between`, `releases and the candidate should be promoted to restore the pristine baseline.`, ].join(' ') ); const actualContent = fs.readFileSync(pristineFile, 'utf8'); assert.equal( sha256(actualContent), oldHash, [ `gsd-pristine/${relPath} must contain bytes matching originalHash after regeneration`, `(sha256=${oldHash.slice(0, 12)}…)`, ].join(' ') ); }); /** * Stale-pristine recovery test (pre-fix bug artifact). * * Timeline: * Buggy run: gsd-pristine/ was written with NEW_RELEASE_CONTENT * (the exact #3407 artifact — stale bytes from a buggy populatePristineDir). * Fix run: saveLocalPatches detects the hash mismatch * (sha256(NEW_RELEASE_CONTENT) !== originalHash recorded in manifest), * removes the stale entry, then attempts regeneration. * * When the file CHANGED between releases (NEW !== OLD): * - The stale entry is removed. * - Regeneration discards the new-release candidate (hash mismatch). * - gsd-pristine/ must be ABSENT (over-broad fallback — correct). * * When the file is UNCHANGED between releases (NEW === OLD): * - The stale entry (which happens to have correct bytes despite the bug) is * detected as correct (hash matches originalHash) and PRESERVED. * - gsd-pristine/ must remain present with the correct bytes. * * This test covers the "file changed across release boundary" case. * The "unchanged" case is already covered by the regeneration test above. */ test('stale gsd-pristine/ entry (new-release bytes) is removed when file changed between releases', () => { const OLD_RELEASE_CONTENT = '# Old Release\nv1 content here.\n'; const NEW_RELEASE_CONTENT = '# New Release\nv2 content — upstream changed this.\n'; const USER_MODIFIED_CONTENT = '# Old Release\nv1 content here.\n## User section\nCustom work.\n'; const oldHash = sha256(OLD_RELEASE_CONTENT); const relPath = 'test-stale-recovery.md'; // configDir: user-modified file + manifest recording OLD hash fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // fakeSrcDir (new release): contains the NEW content fs.writeFileSync(path.join(fakeSrcDir, relPath), NEW_RELEASE_CONTENT); // Pre-populate gsd-pristine/ with NEW_RELEASE_CONTENT — the exact pre-fix bug artifact. // This simulates a prior buggy run that wrote new-release bytes into the pristine baseline. const STALE_BYTES = NEW_RELEASE_CONTENT; // named constant for clarity const pristineDir = path.join(configDir, 'gsd-pristine'); fs.mkdirSync(pristineDir, { recursive: true }); fs.writeFileSync(path.join(pristineDir, relPath), STALE_BYTES); // Verify the pre-condition: stale bytes do NOT match the original hash. // If this assert fails, the test fixture is wrong (not a fix regression). assert.notEqual( sha256(STALE_BYTES), oldHash, 'test fixture check: stale bytes must differ from originalHash' ); INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); // The fix must detect the hash mismatch (stale entry) and remove it. // The regeneration path discards the new-release candidate (its hash !== oldHash). // Result: gsd-pristine/ must be ABSENT — over-broad fallback is the safe outcome. const pristineFile = path.join(pristineDir, relPath); assert.strictEqual( fs.existsSync(pristineFile), false, [ `expected gsd-pristine/${relPath} to be absent after stale-pristine recovery.`, `The stale entry (new-release bytes, sha256=${sha256(STALE_BYTES).slice(0, 12)}…)`, `must be removed; regeneration must discard the candidate because`, `sha256(new-release)=${sha256(NEW_RELEASE_CONTENT).slice(0, 12)}… !== originalHash=${oldHash.slice(0, 12)}….`, `Presence of the file means the stale bytes were NOT cleaned up (pre-fix behavior).`, ].join(' ') ); }); /** * Second scenario: gsd-pristine/ does NOT pre-exist (first upgrade with no * prior pristine population). In this case there is no way to obtain the * old-release pristine bytes — populatePristineDir must NOT write the new- * release bytes either. The correct outcome is: gsd-pristine/ stays empty * for this file, and the verifier falls back to over-broad mode (safe). */ test('gsd-pristine/ stays empty when no prior pristine exists (first upgrade, no stale write)', () => { const OLD_RELEASE_CONTENT = '# Old Release Content\nThis is v1.\n'; const NEW_RELEASE_CONTENT = '# New Release Content\nThis is v2 — changed.\n'; const USER_MODIFIED_CONTENT = '# Old Release Content\nThis is v1.\n## User addition\nCustom.\n'; const oldHash = sha256(OLD_RELEASE_CONTENT); const relPath = 'test-first-upgrade.md'; // configDir has user-modified file + manifest fs.writeFileSync(path.join(configDir, relPath), USER_MODIFIED_CONTENT); fs.writeFileSync( path.join(configDir, MANIFEST_NAME), JSON.stringify({ version: '1.0.0', files: { [relPath]: oldHash } }, null, 2) ); // fakeSrcDir (new release) has new content fs.writeFileSync(path.join(fakeSrcDir, relPath), NEW_RELEASE_CONTENT); // NOTE: gsd-pristine/ does NOT exist yet (first upgrade) INSTALL.saveLocalPatches(configDir, { packageSrc: fakeSrcDir, runtime: 'claude', pathPrefix: '$HOME/.claude/', isGlobal: true, }); const pristineFile = path.join(configDir, 'gsd-pristine', relPath); assert.strictEqual( fs.existsSync(pristineFile), false, [ `expected gsd-pristine/${relPath} to be absent when file changed across release boundary.`, `Writing new-release bytes as pristine for a file whose hash is unknown leads to`, `false FAIL_USER_LINES_MISSING in the reapply-patches verifier (#3407).`, `Over-broad fallback mode is the correct outcome here.`, ].join(' ') ); }); }); // The former "Antipattern hunt" describe block (structural typeof checks only) was // removed — it provided no real behavioral coverage and was a vacuous-truth pattern // per /test-rigor skill. Behavioral tests for populatePristineDir are covered above. }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2995-post-install-script-paths.test.cjs — consolidation epic #1969 (B6 #1975) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2995-post-install-script-paths (consolidation epic #1969 B6 #1975)", () => { 'use strict'; process.env.GSD_TEST_MODE = '1'; 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 ROOT = path.join(__dirname, '..'); const { auditWorkflowScriptPaths, AUDIT_FINDING } = require( path.join(ROOT, 'scripts', 'audit-workflow-script-paths.cjs'), ); const { cleanup } = require('./helpers.cjs'); // auditWorkflowScriptPaths is a pure function: it walks workflowsDir, // extracts every ${GSD_HOME}/ script reference, and returns a // structured report. Tests assert on the typed report — no regex on // console output. // #2996 CR: per-fixture repos are rooted under a single tmpRoot so the // after()-hook actually cleans them up. The previous shape created tmpRoot // in before() but never used it, leaking each fixture's mkdtempSync dir. let tmpRoot; function fixtureRepo({ workflows, files }) { // workflows: { 'foo.md': '...content with ${GSD_HOME}/...' } // files: [ 'gsd-core/bin/x.cjs', ... ] — files to create in repo const repoRoot = fs.mkdtempSync(path.join(tmpRoot, 'repo-')); const workflowsDir = path.join(repoRoot, 'gsd-core', 'workflows'); fs.mkdirSync(workflowsDir, { recursive: true }); for (const [name, body] of Object.entries(workflows || {})) { fs.writeFileSync(path.join(workflowsDir, name), body); } for (const rel of files || []) { const full = path.join(repoRoot, rel); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, ''); } return { repoRoot, workflowsDir }; } before(() => { tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2995-')); }); after(() => { cleanup(tmpRoot); }); describe('Bug #2995: post-install script-paths audit (#2995)', () => { test('AUDIT_FINDING enum exposes the documented codes', () => { assert.deepEqual( Object.keys(AUDIT_FINDING).sort(), ['MISSING_FROM_REPO', 'NOT_INSTALLED'].sort(), ); }); test('returns { ok: true, findings: [] } when workflow refs an existing, installed-path script', () => { const { repoRoot, workflowsDir } = fixtureRepo({ workflows: { 'good.md': 'node "${GSD_HOME}/gsd-core/bin/foo.cjs" --json\n', }, files: ['gsd-core/bin/foo.cjs'], }); const r = auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes: ['gsd-core', 'commands', 'agents', 'hooks'], }); assert.deepEqual(r, { ok: true, findings: [] }); }); }); describe('Bug #2995: detection paths', () => { const { auditWorkflowScriptPaths, AUDIT_FINDING } = require(require('node:path').join(__dirname, '..', 'scripts', 'audit-workflow-script-paths.cjs')); test('reports MISSING_FROM_REPO when the referenced file does not exist in the repo', () => { const { repoRoot, workflowsDir } = fixtureRepo({ workflows: { 'foo.md': 'node "${GSD_HOME}/gsd-core/bin/typo.cjs" --json\n', }, files: [], }); const r = auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes: ['gsd-core'], }); assert.equal(r.ok, false); assert.equal(r.findings.length, 1); assert.deepEqual(r.findings[0], { workflow: 'foo.md', path: 'gsd-core/bin/typo.cjs', kind: AUDIT_FINDING.MISSING_FROM_REPO, }); }); test('reports NOT_INSTALLED when first path segment is outside installedPrefixes (the #2994 case)', () => { const { repoRoot, workflowsDir } = fixtureRepo({ workflows: { 'foo.md': 'node "${GSD_HOME}/scripts/verify-reapply-patches.cjs"\n', }, files: ['scripts/verify-reapply-patches.cjs'], // file exists, but `scripts/` not in installed prefixes }); const r = auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes: ['gsd-core', 'commands', 'agents', 'hooks'], }); assert.equal(r.ok, false); assert.equal(r.findings.length, 1); assert.deepEqual(r.findings[0], { workflow: 'foo.md', path: 'scripts/verify-reapply-patches.cjs', kind: AUDIT_FINDING.NOT_INSTALLED, }); }); test('handles ${GSD_HOME:-$HOME/.claude}/... default-fallback syntax', () => { const { repoRoot, workflowsDir } = fixtureRepo({ workflows: { 'a.md': 'node "${GSD_HOME:-$HOME/.claude}/gsd-core/bin/x.cjs"\n', }, files: ['gsd-core/bin/x.cjs'], }); const r = auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes: ['gsd-core'], }); assert.deepEqual(r, { ok: true, findings: [] }); }); test('reports both findings when one workflow has multiple problems', () => { const { repoRoot, workflowsDir } = fixtureRepo({ workflows: { 'multi.md': [ 'node "${GSD_HOME}/scripts/a.cjs"', 'node "${GSD_HOME}/gsd-core/bin/b.cjs"', 'node "${GSD_HOME}/gsd-core/bin/missing.cjs"', ].join('\n') + '\n', }, files: ['scripts/a.cjs', 'gsd-core/bin/b.cjs'], }); const r = auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes: ['gsd-core'], }); assert.equal(r.ok, false); assert.equal(r.findings.length, 2); const kinds = r.findings.map((f) => f.kind).sort(); assert.deepEqual(kinds, [AUDIT_FINDING.MISSING_FROM_REPO, AUDIT_FINDING.NOT_INSTALLED]); }); test('extracts no findings from a workflow without GSD_HOME script refs', () => { const { repoRoot, workflowsDir } = fixtureRepo({ workflows: { 'plain.md': '# A workflow\n\nSome prose, no script refs.\n', }, }); const r = auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes: ['gsd-core'], }); assert.deepEqual(r, { ok: true, findings: [] }); }); }); describe('Bug #2995: real workflow audit', () => { const { auditWorkflowScriptPaths, AUDIT_FINDING } = require(require('node:path').join(__dirname, '..', 'scripts', 'audit-workflow-script-paths.cjs')); // The set of top-level directories the installer (bin/install.js) actually // copies into ${configDir}/. Touching this set requires updating both // bin/install.js AND this constant — the parity is intentional. const INSTALLED_PREFIXES = [ 'gsd-core', // workflows, references, bin/lib, templates 'commands', // commands/gsd/*.md (Claude Code local + Gemini global) 'skills', // skills/gsd-*/SKILL.md (Claude Code 2.1.88+ global, Codex, etc.) 'agents', // agents/gsd-*.md 'hooks', // hooks/gsd-*.{sh,js} ]; // Known existing gaps tracked in their own issues. Removing an entry should // land in the same PR that fixes the underlying issue; CI surfaces any NEW // gap as a hard failure. // (#2994 entry removed: this PR moves verify-reapply-patches.cjs to // gsd-core/bin/ which IS an installed prefix, closing the gap.) const KNOWN_GAPS = new Set(); test('no NEW workflow refs fail to resolve at the deployed path (KNOWN_GAPS allow-listed)', () => { const r = auditWorkflowScriptPaths({ workflowsDir: require('node:path').join(ROOT, 'gsd-core', 'workflows'), repoRoot: ROOT, installedPrefixes: INSTALLED_PREFIXES, }); const newGaps = r.findings.filter( (f) => !KNOWN_GAPS.has(`${f.workflow}|${f.path}|${f.kind}`), ); if (newGaps.length > 0) { const summary = newGaps.map( (f) => ` ${f.workflow}: ${f.path} (${f.kind})`, ).join('\n'); assert.fail( `New workflow ref does not resolve at the deployed path:\n${summary}\n\n` + `Either move the script under one of [${INSTALLED_PREFIXES.join(', ')}], ` + `update bin/install.js to copy the new top-level directory, or ` + `(if intentionally tracked) add an entry to KNOWN_GAPS with the issue reference.`, ); } }); // #2996 CR: a reference that is both outside an installed prefix AND // missing from the repo must emit BOTH findings in one run. Previously // the code short-circuited on NOT_INSTALLED, hiding MISSING_FROM_REPO // until the developer fixed the prefix and re-ran CI. test('a reference that is both not-installed AND missing-from-repo emits both findings (no short-circuit)', () => { const { repoRoot, workflowsDir } = fixtureRepo({ workflows: { 'foo.md': '```bash\nnode "${GSD_HOME}/scripts/missing.cjs"\n```\n', }, // Note: scripts/missing.cjs intentionally NOT created in the repo. }); const r = auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes: ['gsd-core', 'agents', 'hooks', 'commands'], }); assert.equal(r.ok, false); const kinds = r.findings.filter((f) => f.path === 'scripts/missing.cjs').map((f) => f.kind).sort(); assert.deepEqual( kinds, [AUDIT_FINDING.MISSING_FROM_REPO, AUDIT_FINDING.NOT_INSTALLED].sort(), 'expected both NOT_INSTALLED and MISSING_FROM_REPO findings for the same ref', ); }); test('KNOWN_GAPS entries still match real findings — fixed gaps must be removed from the allow-list', () => { const r = auditWorkflowScriptPaths({ workflowsDir: require('node:path').join(ROOT, 'gsd-core', 'workflows'), repoRoot: ROOT, installedPrefixes: INSTALLED_PREFIXES, }); const realKeys = new Set(r.findings.map((f) => `${f.workflow}|${f.path}|${f.kind}`)); const stale = [...KNOWN_GAPS].filter((k) => !realKeys.has(k)); assert.deepEqual( stale, [], `KNOWN_GAPS contains entries not present in audit findings — remove these: ${stale.join(', ')}`, ); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-3442-shim-projection-drift-guard.test.cjs — consolidation epic #1969 (B6 #1975) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-3442-shim-projection-drift-guard (consolidation epic #1969 B6 #1975)", () => { 'use strict'; process.env.GSD_TEST_MODE = '1'; const { describe, test } = 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 { spawnSync } = require('node:child_process'); const { cleanup } = require('./helpers.cjs'); const ROOT = path.resolve(__dirname, '..'); const DRIFT_LINT = path.join(ROOT, 'scripts', 'lint-shell-command-projection-drift.cjs'); function runLint(targetFile) { return spawnSync(process.execPath, [DRIFT_LINT, targetFile], { cwd: ROOT, encoding: 'utf8', }); } // (The buildWindowsShimTriple parity test was removed with the gsd-sdk shim, // #191. The serialized-command drift guard below is retained and unaffected.) describe('bug #3442: shim/wrapper serialized-command drift guard', () => { test('drift guard passes for current install.js', () => { const result = runLint(path.join(ROOT, 'bin', 'install.js')); assert.equal(result.status, 0, `expected lint pass, got:\n${result.stderr || result.stdout}`); }); test('drift guard fails when install-owned inline shim text builder is present', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3442-')); try { const fixture = path.join(tmp, 'install-inline-builder.js'); fs.writeFileSync( fixture, [ 'function badBuilder() {', " return '@ECHO OFF\\r\\n@SETLOCAL\\r\\n@node \"C:/shim.js\" %*\\r\\n';", '}', '', ].join('\n'), ); const result = runLint(fixture); assert.notEqual(result.status, 0, 'inline shim renderer should be rejected by the drift guard'); } finally { cleanup(tmp); } }); test('drift guard does not block safe subprocess execution patterns', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3442-')); try { const fixture = path.join(tmp, 'install-subprocess-safe.js'); fs.writeFileSync( fixture, [ "const cp = require('node:child_process');", "cp.spawnSync('cmd.exe', ['/c', 'echo ok']);", "cp.execFileSync('bash', ['-lc', 'printf %s \"$PATH\"']);", '', ].join('\n'), ); const result = runLint(fixture); assert.equal(result.status, 0, `spawnSync/execFileSync should remain allowed:\n${result.stderr || result.stdout}`); } finally { cleanup(tmp); } }); }); }); }