test(#1975): consolidate 51 CLI + scripts-tooling regression tests into module suites
Fold 51 issue-named CLI black-box + scripts-tooling regression files into their canonical module suites (runtime-launcher-parity, worktree-safety, install-*, managed-hooks, read-guard, capability-registry, etc.), plus a NEW slash-command-namespace.test.cjs grouping the 4 slash/colon-namespace-leak invariant suites that had no canonical owner. Verbatim block-scoped describe wrappers; 427 subtests conserved 1:1. Host-env pre-check (per B2): no CLI-receiving host sets a redirecting GSD_WORKSTREAM/GSD_PROJECT value. One folded suite (bug-3668 runtime resolver) creates an extension-less PATH gsd-tools stub + bash -c; co-locating it with the host's chmodSync tripped local/no-unguarded-nonportable-exec, so it's now Windows-guarded (skip on win32) matching the host suite's own bash -c guard. Regenerates regression-name allowlist (222->182), ratchets file-count allowlist (graphify 7->6, docs entry removed), makes 26 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456; prunes stale ids). Repoints 13 tests/ references across CONTEXT.md, COMMANDS.md/FEATURES.md (EN + ja/ko/pt/zh) and ADR-0002. lint:ci green. Part of epic #1969. Closes #1975. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1750,3 +1750,909 @@ describe('N3: Windows-separator confinement logic (path.win32 semantics)', () =>
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// 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/<rel>` to installed
|
||||
// `gsd-core/<rel>` 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/<rel> 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/<rel> 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/<rel> 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/<rel> 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}/<path> 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user