test(#2126): fix os.tmpdir() cross-file race + dedup folds surfaced by gsd-test (no-defer)

Phase 3's gsd-test surfaced 8 pre-existing test-isolation races (in #2090's test
files now on next). Per CLAUDE.md's no-defer rule these are fixed inline in the
current change. Root-caused via /qa-test-architect — all bad-test (the
rewrite-engine production code is race-free):

- install-runtime-artifacts.test.cjs: the "rmSync when readFileSync throws" test
  diffed the SHARED os.tmpdir() for gsd-cmd-rewrites-* dirs and force-deleted any
  new one with no ownership check. Under --test-concurrency it deleted a sibling
  test file's LIVE tempDir mid-copy (the #1575 "ENOENT .../graphify.md") and
  misattributed it as its own leak. Fixed: capture the exact tempDir THIS call
  creates (fs.mkdtempSync monkeypatch, restored in finally) and assert only on
  that — never sweep/delete the shared os.tmpdir(). Also deduped the enh-1511
  block the #1969 consolidation folded in 3x byte-identically (#1970/#1974/#1975)
  down to 1 copy; 308 unique test titles unchanged (verified).
- issue-1575-agent-descriptor-parity.test.cjs: a missing }); nested the M2
  'cursor attribution' test inside the per-runtime loop so it ran 7x (widening
  the tempDir window). Fixed the brace -> runs once as a describe sibling.
- config-get-default.test.cjs: local run()/runRaw() spawned node via
  execFileSync with a fixed 5s timeout and no retry -> ETIMEDOUT under Docker
  load. Redesigned to call cmdConfigGet in-process (fs.writeSync fd-capture +
  process.exit sentinel, both restored in finally) — no subprocess, no wall clock.
- runtime-artifact-conversion.cts: fixed the stale "No production caller today"
  JSDoc on rewriteStagedCommandBodies (real callers: applySurface,
  createRuntimeArtifactInstallPlan) — the false doc invited the bad test.

Refs #2126, #2090

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-09 21:10:57 -04:00
parent 74e77974af
commit 119702ff29
4 changed files with 126 additions and 864 deletions

View File

@@ -2522,10 +2522,12 @@ function rewriteStagedSkillBodies(stagedDir, opts) {
* attribution from opts, then delegates to applyRuntimeContentRewritesForCommandsInPlace
* (single copy+rewrite owner).
*
* @internal — symmetric companion to rewriteStagedSkillBodies; retained as the deep-seam
* API for command bodies. No production caller today (install rewrites commands via
* copyWithPathReplacement → applyRuntimeContentRewritesForCommandsInPlace). Kept for
* API symmetry + test coverage.
* @internal — symmetric companion to rewriteStagedSkillBodies; the deep-seam API for
* command bodies. Production callers: applySurface (surface.cts) and the install path
* in createRuntimeArtifactInstallPlan (runtime-artifact-install-plan.cts) — both keep
* the returned temp dir alive until they have copied its contents out, then clean it up
* in their own finally. (A test that treats this as a throwaway shared-tmp path will
* race those live temp dirs under --test-concurrency; see #1575/#2090.)
*
* @returns {string} path to the temp dir (caller is responsible for cleanup)
*/

View File

@@ -13,11 +13,83 @@ const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const os = require('os');
const { cleanup } = require('./helpers.cjs');
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
// In-process invocation, not execFileSync: cmdConfigGet is a pure CJS
// function reachable without spawning `node` as a child. The prior
// execFileSync(..., { timeout: 5000 }) raced a real subprocess's startup
// (full node boot + gsd-tools.cjs's large eager require graph — capability
// registry, phase/roadmap/agent/check/task routers, verify.cjs,
// cli-skew-check, findProjectRoot, etc.) against a fixed 5s wall clock, with
// no retry. Under Docker host contention that wall clock loses
// nondeterministically (ETIMEDOUT) — a test-harness race, not a product
// defect. bin/lib/config.cjs requires none of that dispatcher machinery, so
// calling cmdConfigGet directly removes the subprocess-spawn cost and the
// wall-clock race entirely: no timeout of any size can flake this.
const config = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'config.cjs'));
/**
* cmdConfigGet's error() path (gsd-core/bin/lib/io.cjs) calls process.exit(1)
* directly (it predates the ExitError/runMain seam used by the CLI
* entrypoint's non-error paths). Intercepting process.exit with a throwable
* sentinel lets the error path be exercised in-process without killing the
* test worker.
*/
class _ExitSignal extends Error {
constructor(code) {
super(`process.exit(${code})`);
this.code = code;
}
}
/**
* bin/lib/io.cjs's output()/error() write directly to the raw fd (1 or 2)
* via fs.writeSync — they bypass console.log entirely, so
* tests/helpers.cjs's captureConsole() cannot observe them (see
* tests/io.test.cjs: "output() writes directly to fd 1"). Monkeypatch
* fs.writeSync itself — save the original, override, restore in a finally,
* the project's standard IO capture/fault-injection seam — to capture what
* would have hit the fd.
*/
function captureFdWrite(fd, fn) {
const orig = fs.writeSync;
let captured = Buffer.alloc(0);
fs.writeSync = (writeFd, ...rest) => {
if (writeFd !== fd) return orig.call(fs, writeFd, ...rest);
const [data, offset = 0, length] = rest;
const chunk = Buffer.isBuffer(data)
? data.subarray(offset, offset + (length ?? data.length - offset))
: Buffer.from(String(data), 'utf8');
captured = Buffer.concat([captured, chunk]);
return chunk.length;
};
try {
fn();
} finally {
fs.writeSync = orig;
}
return captured.toString('utf-8');
}
/**
* Parse a CLI-style config-get argv (mirrors gsd-core/bin/gsd-tools.cjs's
* 'config-get' case: key is args[1], optional --default <value>, optional
* --raw) into cmdConfigGet's positional params. Keeps the test bodies below
* expressed in the same CLI-args vocabulary they always were.
*/
function parseConfigGetArgs(args) {
const rest = args.slice(1); // drop the leading 'config-get'
let raw = false;
let defaultValue;
const positional = [];
for (let i = 0; i < rest.length; i++) {
if (rest[i] === '--raw') { raw = true; continue; }
if (rest[i] === '--default') { defaultValue = rest[i + 1] ?? ''; i++; continue; }
positional.push(rest[i]);
}
return { keyPath: positional[0], raw, defaultValue };
}
describe('config-get --default flag (#1893)', () => {
let tmpDir;
@@ -34,10 +106,11 @@ describe('config-get --default flag (#1893)', () => {
});
function run(...args) {
return execFileSync('node', [GSD_TOOLS, ...args, '--cwd', tmpDir], {
encoding: 'utf-8',
timeout: 5000,
}).trim();
const { keyPath, raw, defaultValue } = parseConfigGetArgs(args);
const out = captureFdWrite(1, () => {
config.cmdConfigGet(tmpDir, keyPath, raw, defaultValue);
});
return out.trim();
}
function runRaw(...args) {
@@ -45,17 +118,27 @@ describe('config-get --default flag (#1893)', () => {
}
function runExpectError(...args) {
const { keyPath, raw, defaultValue } = parseConfigGetArgs(args);
const origExit = process.exit;
let exitCode;
process.exit = (code) => {
exitCode = code;
throw new _ExitSignal(code);
};
let stderr;
try {
execFileSync('node', [GSD_TOOLS, ...args, '--cwd', tmpDir], {
encoding: 'utf-8',
timeout: 5000,
stdio: ['pipe', 'pipe', 'pipe'],
stderr = captureFdWrite(2, () => {
try {
config.cmdConfigGet(tmpDir, keyPath, raw, defaultValue);
} catch (e) {
if (!(e instanceof _ExitSignal)) throw e;
}
});
assert.fail('Expected command to exit non-zero');
} catch (err) {
assert.ok(err.status !== 0, 'Expected non-zero exit code');
return err;
} finally {
process.exit = origExit;
}
assert.ok(exitCode !== 0 && exitCode !== undefined, 'Expected non-zero exit code');
return { status: exitCode, stderr };
}
test('absent key without --default errors', () => {

View File

@@ -4002,13 +4002,19 @@ describe('applyRuntimeContentRewritesForCommandsInPlace — error-path tempDir c
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-error-path-'));
fs.writeFileSync(path.join(stagedDir, 'x.md'), '# test\n');
const before = new Set(
fs.readdirSync(os.tmpdir()).filter(n => n.startsWith('gsd-cmd-rewrites-'))
);
// Capture the EXACT tempDir THIS invocation creates (via the function's own
// fs.mkdtempSync call) instead of diffing the shared os.tmpdir() listing.
// The old diff-and-sweep approach raced any concurrently-running test file
// that mkdtemps its own gsd-cmd-rewrites-* dir under --test-concurrency: it
// misattributed a sibling's live dir as this test's leak AND force-deleted
// it mid-use (the #1575 ENOENT on graphify.md). Owning a single,
// self-generated fixture makes this Independent + Repeatable under any
// parallelism.
const origMkdtempSync = fs.mkdtempSync;
const origReadFileSync = fs.readFileSync;
let leaked = [];
let capturedTempDir = null;
try {
fs.mkdtempSync = (...args) => (capturedTempDir = origMkdtempSync.apply(fs, args));
fs.readFileSync = () => { throw new Error('injected read failure'); };
assert.throws(
@@ -4016,21 +4022,22 @@ describe('applyRuntimeContentRewritesForCommandsInPlace — error-path tempDir c
/injected read failure/,
);
// Restore before any further fs use so the snapshot read is trustworthy.
// Restore before any further fs use so the existsSync check is trustworthy.
fs.mkdtempSync = origMkdtempSync;
fs.readFileSync = origReadFileSync;
const after = fs.readdirSync(os.tmpdir()).filter(n => n.startsWith('gsd-cmd-rewrites-'));
leaked = after.filter(n => !before.has(n));
assert.deepStrictEqual(leaked, [], `tempDir not cleaned up on error: ${leaked.join(',')}`);
assert.ok(capturedTempDir, 'function under test must create a tempDir before failing');
assert.equal(
fs.existsSync(capturedTempDir),
false,
`tempDir not cleaned up on error: ${capturedTempDir}`,
);
} finally {
// Idempotent restore — guard against early-throw paths above.
fs.mkdtempSync = origMkdtempSync;
fs.readFileSync = origReadFileSync;
// Clean up the staged dir created for this test.
// Clean up only OUR OWN fixture — never sweep the shared os.tmpdir().
cleanup(stagedDir);
// Clean up any genuinely leaked gsd-cmd-rewrites-* dirs so the runner stays clean.
for (const n of leaked) {
cleanup(path.join(os.tmpdir(), n));
}
}
});
});
@@ -7142,421 +7149,6 @@ describe('processAttribution (relocated to runtime-artifact-conversion)', () =>
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/enh-1511-rewrite-engine-relocation.test.cjs — consolidation epic #1969 (B1 #1970)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:enh-1511-rewrite-engine-relocation (consolidation epic #1969 B1 #1970)", () => {
'use strict';
/**
* Tests for ADR-1508 Phase 2: rewrite engine relocation to runtime-artifact-conversion.
* Issue #1511 — verifies the deep public seam signatures and behavior.
*
* Tests are behavioral (no source-grep). All filesystem operations use tmp dirs.
*/
const { describe, test, before } = 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');
let conversion;
before(() => {
process.env['GSD_TEST_MODE'] = '1';
conversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
});
// ---------------------------------------------------------------------------
// _computePathPrefix unit tests
// ---------------------------------------------------------------------------
describe('_computePathPrefix', () => {
test('global under home → $HOME/... form', () => {
const prefix = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: '/home/u/.cursor',
homeDir: '/home/u',
});
assert.equal(prefix, '$HOME/.cursor/');
});
test('non-global → resolvedTarget/ form', () => {
const prefix = conversion._computePathPrefix({
isGlobal: false,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: '/project/.cursor',
homeDir: '/home/u',
});
assert.equal(prefix, '/project/.cursor/');
});
test('global opencode skips $HOME shorthand', () => {
// OpenCode uses ~/.config/opencode which breaks $HOME shorthand in content
const prefix = conversion._computePathPrefix({
isGlobal: true,
isOpencode: true,
isWindowsHost: false,
resolvedTarget: '/home/u/.config/opencode',
homeDir: '/home/u',
});
assert.equal(prefix, '/home/u/.config/opencode/');
});
test('global target outside home → resolvedTarget/ form', () => {
const prefix = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: '/opt/custom-cursor',
homeDir: '/home/u',
});
assert.equal(prefix, '/opt/custom-cursor/');
});
test('isWindowsHost tripwire — Windows paths collapse to $HOME/ same as POSIX (no-op today)', () => {
// Documents CURRENT behavior: isWindowsHost is accepted but not branched on.
// Both win32=true and win32=false return '$HOME/.cursor/' for a home-relative target.
// If a future Windows-specific branch is added, this tripwire fails and forces
// an explicit decision about what to return on Windows.
const withWindows = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: true,
resolvedTarget: 'C:/Users/matte/.cursor',
homeDir: 'C:/Users/matte',
});
const withoutWindows = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: 'C:/Users/matte/.cursor',
homeDir: 'C:/Users/matte',
});
assert.equal(withWindows, '$HOME/.cursor/');
assert.strictEqual(withWindows, withoutWindows);
});
test('backslash-style resolvedTarget is normalized to forward slashes (#1615 regression)', () => {
// path.join on Windows produces backslashes; the returned prefix is
// substituted into markdown @-references which must use POSIX paths.
// Without normalization the backslashes leak into workflow file content
// and break substring checks on Windows CI.
const prefix = conversion._computePathPrefix({
isGlobal: false,
isOpencode: false,
isWindowsHost: true,
resolvedTarget: 'C:\\Users\\runner\\AppData\\Local\\Temp\\gsd-1615-windsurf',
homeDir: 'C:\\Users\\runner',
});
assert.strictEqual(prefix, 'C:/Users/runner/AppData/Local/Temp/gsd-1615-windsurf/');
assert.ok(!prefix.includes('\\'), `prefix must not contain backslashes: ${prefix}`);
});
});
// ---------------------------------------------------------------------------
// _applyRuntimeRewrites with injected attribution
// ---------------------------------------------------------------------------
describe('_applyRuntimeRewrites — attribution injection', () => {
const PREFIX = '$HOME/.cursor/';
test('attribution=null removes Co-Authored-By line', () => {
const content = '# Hello\n\nSome text\n\nCo-Authored-By: Claude\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, null);
assert.ok(!result.includes('Co-Authored-By:'), 'Co-Authored-By should be removed');
});
test('attribution=undefined leaves Co-Authored-By unchanged', () => {
const content = '# Hello\n\nCo-Authored-By: Claude\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, undefined);
assert.ok(result.includes('Co-Authored-By: Claude'), 'Co-Authored-By should be preserved when attribution=undefined');
});
test('attribution=string replaces Co-Authored-By value', () => {
const content = '# Hello\n\nCo-Authored-By: OldName\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, 'NewName <new@example.com>');
assert.ok(result.includes('Co-Authored-By: NewName <new@example.com>'), 'Co-Authored-By should be replaced');
});
test('cursor runtime replaces ~/.claude/ paths', () => {
const content = 'See ~/.claude/skills/ for more info\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', '/home/u/.cursor/', false, undefined);
assert.ok(result.includes('/home/u/.cursor/skills/'), 'cursor should replace ~/.claude/ with pathPrefix');
});
});
// ---------------------------------------------------------------------------
// rewriteStagedSkillBodies — behavioral filesystem test
// ---------------------------------------------------------------------------
describe('rewriteStagedSkillBodies', () => {
test('rewrites .md files in-place for cursor runtime', () => {
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-staged-'));
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-config-'));
try {
// Create a skill dir with a SKILL.md referencing ~/.claude/skills/foo
// NOTE: the rewrite engine handles path replacement and attribution only.
// Bash→Shell conversion is done by the stage-1 skill converter, not the engine.
const skillDir = path.join(stagedDir, 'gsd-test-skill');
fs.mkdirSync(skillDir, { recursive: true });
const content = '# Test\n\nSee ~/.claude/skills/foo\n\nAlso ~/.cursor/skills/bar\n';
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
// Call with injected homedir + platform for determinism
conversion.rewriteStagedSkillBodies(stagedDir, {
runtime: 'cursor',
configDir,
scope: 'global',
homedir: () => '/home/u',
platform: 'linux',
});
const result = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
// cursor rewrites ~/.claude/ → pathPrefix
// configDir is a tmpdir, not under /home/u, so prefix = resolvedTarget + '/'
// Mirror the engine's backslash→slash normalization so the assertion holds on Windows.
const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/');
assert.ok(result.includes(`${resolvedTarget}/skills/foo`), `Should replace ~/.claude/skills/ with ${resolvedTarget}/skills/`);
// cursor also rewrites ~/.cursor/ → pathPrefix
assert.ok(result.includes(`${resolvedTarget}/skills/bar`), `Should replace ~/.cursor/skills/ with ${resolvedTarget}/skills/`);
} finally {
cleanup(stagedDir);
cleanup(configDir);
}
});
test('with injected homedir: global under home uses $HOME prefix', () => {
// Real absolute path so Windows path.resolve does not re-root a POSIX literal onto a drive.
// The dir need not exist — the engine only string-processes it.
const HOME = path.resolve(os.tmpdir(), 'gsd-1511-fake-home');
const configDir = path.join(HOME, '.cursor');
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-staged-'));
try {
const skillDir = path.join(stagedDir, 'gsd-help');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'Use ~/.claude/skills/ here\n');
conversion.rewriteStagedSkillBodies(stagedDir, {
runtime: 'cursor',
configDir,
scope: 'global',
homedir: () => HOME,
platform: process.platform,
});
const result = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
assert.ok(result.includes('$HOME/.cursor/skills/'), 'Should use $HOME shorthand when configDir is under homedir');
} finally {
cleanup(stagedDir);
}
});
test('non-existent stagedDir is a no-op', () => {
assert.doesNotThrow(() => {
conversion.rewriteStagedSkillBodies('/nonexistent/dir', {
runtime: 'cursor',
configDir: '/tmp/fake',
scope: 'global',
});
});
});
});
// ---------------------------------------------------------------------------
// rewriteStagedCommandBodies — returns temp dir, does not mutate source
// ---------------------------------------------------------------------------
describe('rewriteStagedCommandBodies', () => {
test('returns a temp dir (not the source dir) with rewritten content', () => {
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-cmd-'));
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-config-'));
let tempDir;
try {
// NOTE: rewrite engine handles path replacement + attribution, NOT tool renames.
fs.writeFileSync(path.join(stagedDir, 'help.md'), '# Help\n\nSee ~/.claude/skills/\n\nSee ~/.cursor/skills/\n');
tempDir = conversion.rewriteStagedCommandBodies(stagedDir, {
runtime: 'cursor',
configDir,
scope: 'global',
homedir: () => '/home/u',
platform: 'linux',
});
assert.notEqual(tempDir, stagedDir, 'must return a different dir, never the source');
assert.ok(fs.existsSync(tempDir), 'returned tempDir should exist');
const result = fs.readFileSync(path.join(tempDir, 'help.md'), 'utf8');
// Source dir should be unchanged
const source = fs.readFileSync(path.join(stagedDir, 'help.md'), 'utf8');
assert.ok(source.includes('~/.claude/skills/'), 'source file must not be mutated');
// configDir is /tmp/... (not under /home/u), so prefix = resolvedTarget + '/'
const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/');
assert.ok(result.includes(`${resolvedTarget}/skills/`), 'output should have cursor path rewrite applied');
// ~/.cursor/ also rewrites to prefix
assert.ok(!result.includes('~/.cursor/'), 'output should have ~/.cursor/ replaced too');
} finally {
cleanup(stagedDir);
cleanup(configDir);
if (tempDir && tempDir !== stagedDir) {
cleanup(tempDir);
}
}
});
test('non-existent stagedDir returns stagedDir unchanged (safe)', () => {
const result = conversion.rewriteStagedCommandBodies('/nonexistent/dir', {
runtime: 'cursor',
configDir: '/tmp/fake',
scope: 'global',
});
assert.equal(result, '/nonexistent/dir', 'should return input path unchanged for missing dir');
});
});
// ---------------------------------------------------------------------------
// Error-path: applyRuntimeContentRewritesForCommandsInPlace must rm the tempDir
// on any exception and NOT leave an orphaned gsd-cmd-rewrites-* directory.
// ---------------------------------------------------------------------------
describe('applyRuntimeContentRewritesForCommandsInPlace — error-path tempDir cleanup', () => {
test('rmSync is called on the tempDir when readFileSync throws (deterministic monkeypatch)', () => {
// Asserting the injected error propagates proves the throw happens AFTER the tempDir is
// created (the function creates tempDir, then reads .md), so the catch's rmSync cleanup
// is genuinely exercised — deterministic on every platform/uid.
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-error-path-'));
fs.writeFileSync(path.join(stagedDir, 'x.md'), '# test\n');
const before = new Set(
fs.readdirSync(os.tmpdir()).filter(n => n.startsWith('gsd-cmd-rewrites-'))
);
const origReadFileSync = fs.readFileSync;
let leaked = [];
try {
fs.readFileSync = () => { throw new Error('injected read failure'); };
assert.throws(
() => conversion.applyRuntimeContentRewritesForCommandsInPlace(stagedDir, 'cursor', '/tmp/x/', false),
/injected read failure/,
);
// Restore before any further fs use so the snapshot read is trustworthy.
fs.readFileSync = origReadFileSync;
const after = fs.readdirSync(os.tmpdir()).filter(n => n.startsWith('gsd-cmd-rewrites-'));
leaked = after.filter(n => !before.has(n));
assert.deepStrictEqual(leaked, [], `tempDir not cleaned up on error: ${leaked.join(',')}`);
} finally {
// Idempotent restore — guard against early-throw paths above.
fs.readFileSync = origReadFileSync;
// Clean up the staged dir created for this test.
cleanup(stagedDir);
// Clean up any genuinely leaked gsd-cmd-rewrites-* dirs so the runner stays clean.
for (const n of leaked) {
cleanup(path.join(os.tmpdir(), n));
}
}
});
});
// ---------------------------------------------------------------------------
// Guard: runtime-artifact-layout no longer exports getInstallExports
// ---------------------------------------------------------------------------
describe('layout module no longer exports getInstallExports', () => {
test('getInstallExports is not on the layout module export', () => {
process.env['GSD_TEST_MODE'] = '1';
const layout = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
assert.equal(
typeof layout.getInstallExports,
'undefined',
'getInstallExports should have been removed from runtime-artifact-layout exports (ADR-1508 Phase 2)',
);
});
});
// ---------------------------------------------------------------------------
// DEFECT.GENERATIVE-FIX: single-owner reference-identity guard (#1511)
// Proves install.js binds to the conversion module's implementation, not a
// duplicate local copy. If these fail, a duplicate body was re-introduced.
// ---------------------------------------------------------------------------
describe('single-owner reference-identity guard (ADR-1508 / #1511 Phase 2)', () => {
let install;
let conversionCjs;
before(() => {
process.env['GSD_TEST_MODE'] = '1';
install = require('../bin/install.js');
conversionCjs = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
});
test('install.computePathPrefix === conversion._computePathPrefix (single implementation)', () => {
assert.strictEqual(
install.computePathPrefix,
conversionCjs._computePathPrefix,
'install.js must bind computePathPrefix from conversion (not a duplicate body)',
);
});
test('install.applyRuntimeContentRewritesInPlace === conversion.applyRuntimeContentRewritesInPlace (single walk loop)', () => {
assert.strictEqual(
install.applyRuntimeContentRewritesInPlace,
conversionCjs.applyRuntimeContentRewritesInPlace,
'install.js must bind applyRuntimeContentRewritesInPlace from conversion (not a duplicate walk loop)',
);
});
test('install.applyRuntimeContentRewritesForCommandsInPlace === conversion.applyRuntimeContentRewritesForCommandsInPlace (single copy+rewrite loop)', () => {
assert.strictEqual(
install.applyRuntimeContentRewritesForCommandsInPlace,
conversionCjs.applyRuntimeContentRewritesForCommandsInPlace,
'install.js must bind applyRuntimeContentRewritesForCommandsInPlace from conversion (not a duplicate copy+rewrite loop)',
);
});
test('install._applyRuntimeRewrites === conversion._applyRuntimeRewrites (single switch engine)', () => {
assert.strictEqual(
install._applyRuntimeRewrites,
conversionCjs._applyRuntimeRewrites,
'install.js must bind _applyRuntimeRewrites from conversion (not a local shim)',
);
});
// #1675 (ADR-1508): the augment converter family is single-sourced in the
// conversion module. install.js must re-bind (not re-define) these so there
// is exactly one body — the generative-drift hazard the dedup removes.
test('install.convertClaudeToAugmentMarkdown === conversion.convertClaudeToAugmentMarkdown (single converter)', () => {
assert.strictEqual(
install.convertClaudeToAugmentMarkdown,
conversionCjs.convertClaudeToAugmentMarkdown,
'install.js must bind convertClaudeToAugmentMarkdown from conversion (not a duplicate body)',
);
});
test('install.convertClaudeCommandToAugmentSkill === conversion.convertClaudeCommandToAugmentSkill (single converter)', () => {
assert.strictEqual(
install.convertClaudeCommandToAugmentSkill,
conversionCjs.convertClaudeCommandToAugmentSkill,
'install.js must bind convertClaudeCommandToAugmentSkill from conversion (not a duplicate body)',
);
});
test('install.convertClaudeAgentToAugmentAgent === conversion.convertClaudeAgentToAugmentAgent (single converter)', () => {
assert.strictEqual(
install.convertClaudeAgentToAugmentAgent,
conversionCjs.convertClaudeAgentToAugmentAgent,
'install.js must bind convertClaudeAgentToAugmentAgent from conversion (not a duplicate body)',
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
@@ -11461,421 +11053,6 @@ describe('processAttribution (relocated to runtime-artifact-conversion)', () =>
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/enh-1511-rewrite-engine-relocation.test.cjs — consolidation epic #1969 (B1 #1970)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:enh-1511-rewrite-engine-relocation (consolidation epic #1969 B1 #1970)", () => {
'use strict';
/**
* Tests for ADR-1508 Phase 2: rewrite engine relocation to runtime-artifact-conversion.
* Issue #1511 — verifies the deep public seam signatures and behavior.
*
* Tests are behavioral (no source-grep). All filesystem operations use tmp dirs.
*/
const { describe, test, before } = 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');
let conversion;
before(() => {
process.env['GSD_TEST_MODE'] = '1';
conversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
});
// ---------------------------------------------------------------------------
// _computePathPrefix unit tests
// ---------------------------------------------------------------------------
describe('_computePathPrefix', () => {
test('global under home → $HOME/... form', () => {
const prefix = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: '/home/u/.cursor',
homeDir: '/home/u',
});
assert.equal(prefix, '$HOME/.cursor/');
});
test('non-global → resolvedTarget/ form', () => {
const prefix = conversion._computePathPrefix({
isGlobal: false,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: '/project/.cursor',
homeDir: '/home/u',
});
assert.equal(prefix, '/project/.cursor/');
});
test('global opencode skips $HOME shorthand', () => {
// OpenCode uses ~/.config/opencode which breaks $HOME shorthand in content
const prefix = conversion._computePathPrefix({
isGlobal: true,
isOpencode: true,
isWindowsHost: false,
resolvedTarget: '/home/u/.config/opencode',
homeDir: '/home/u',
});
assert.equal(prefix, '/home/u/.config/opencode/');
});
test('global target outside home → resolvedTarget/ form', () => {
const prefix = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: '/opt/custom-cursor',
homeDir: '/home/u',
});
assert.equal(prefix, '/opt/custom-cursor/');
});
test('isWindowsHost tripwire — Windows paths collapse to $HOME/ same as POSIX (no-op today)', () => {
// Documents CURRENT behavior: isWindowsHost is accepted but not branched on.
// Both win32=true and win32=false return '$HOME/.cursor/' for a home-relative target.
// If a future Windows-specific branch is added, this tripwire fails and forces
// an explicit decision about what to return on Windows.
const withWindows = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: true,
resolvedTarget: 'C:/Users/matte/.cursor',
homeDir: 'C:/Users/matte',
});
const withoutWindows = conversion._computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: false,
resolvedTarget: 'C:/Users/matte/.cursor',
homeDir: 'C:/Users/matte',
});
assert.equal(withWindows, '$HOME/.cursor/');
assert.strictEqual(withWindows, withoutWindows);
});
test('backslash-style resolvedTarget is normalized to forward slashes (#1615 regression)', () => {
// path.join on Windows produces backslashes; the returned prefix is
// substituted into markdown @-references which must use POSIX paths.
// Without normalization the backslashes leak into workflow file content
// and break substring checks on Windows CI.
const prefix = conversion._computePathPrefix({
isGlobal: false,
isOpencode: false,
isWindowsHost: true,
resolvedTarget: 'C:\\Users\\runner\\AppData\\Local\\Temp\\gsd-1615-windsurf',
homeDir: 'C:\\Users\\runner',
});
assert.strictEqual(prefix, 'C:/Users/runner/AppData/Local/Temp/gsd-1615-windsurf/');
assert.ok(!prefix.includes('\\'), `prefix must not contain backslashes: ${prefix}`);
});
});
// ---------------------------------------------------------------------------
// _applyRuntimeRewrites with injected attribution
// ---------------------------------------------------------------------------
describe('_applyRuntimeRewrites — attribution injection', () => {
const PREFIX = '$HOME/.cursor/';
test('attribution=null removes Co-Authored-By line', () => {
const content = '# Hello\n\nSome text\n\nCo-Authored-By: Claude\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, null);
assert.ok(!result.includes('Co-Authored-By:'), 'Co-Authored-By should be removed');
});
test('attribution=undefined leaves Co-Authored-By unchanged', () => {
const content = '# Hello\n\nCo-Authored-By: Claude\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, undefined);
assert.ok(result.includes('Co-Authored-By: Claude'), 'Co-Authored-By should be preserved when attribution=undefined');
});
test('attribution=string replaces Co-Authored-By value', () => {
const content = '# Hello\n\nCo-Authored-By: OldName\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, 'NewName <new@example.com>');
assert.ok(result.includes('Co-Authored-By: NewName <new@example.com>'), 'Co-Authored-By should be replaced');
});
test('cursor runtime replaces ~/.claude/ paths', () => {
const content = 'See ~/.claude/skills/ for more info\n';
const result = conversion._applyRuntimeRewrites(content, 'cursor', '/home/u/.cursor/', false, undefined);
assert.ok(result.includes('/home/u/.cursor/skills/'), 'cursor should replace ~/.claude/ with pathPrefix');
});
});
// ---------------------------------------------------------------------------
// rewriteStagedSkillBodies — behavioral filesystem test
// ---------------------------------------------------------------------------
describe('rewriteStagedSkillBodies', () => {
test('rewrites .md files in-place for cursor runtime', () => {
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-staged-'));
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-config-'));
try {
// Create a skill dir with a SKILL.md referencing ~/.claude/skills/foo
// NOTE: the rewrite engine handles path replacement and attribution only.
// Bash→Shell conversion is done by the stage-1 skill converter, not the engine.
const skillDir = path.join(stagedDir, 'gsd-test-skill');
fs.mkdirSync(skillDir, { recursive: true });
const content = '# Test\n\nSee ~/.claude/skills/foo\n\nAlso ~/.cursor/skills/bar\n';
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
// Call with injected homedir + platform for determinism
conversion.rewriteStagedSkillBodies(stagedDir, {
runtime: 'cursor',
configDir,
scope: 'global',
homedir: () => '/home/u',
platform: 'linux',
});
const result = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
// cursor rewrites ~/.claude/ → pathPrefix
// configDir is a tmpdir, not under /home/u, so prefix = resolvedTarget + '/'
// Mirror the engine's backslash→slash normalization so the assertion holds on Windows.
const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/');
assert.ok(result.includes(`${resolvedTarget}/skills/foo`), `Should replace ~/.claude/skills/ with ${resolvedTarget}/skills/`);
// cursor also rewrites ~/.cursor/ → pathPrefix
assert.ok(result.includes(`${resolvedTarget}/skills/bar`), `Should replace ~/.cursor/skills/ with ${resolvedTarget}/skills/`);
} finally {
cleanup(stagedDir);
cleanup(configDir);
}
});
test('with injected homedir: global under home uses $HOME prefix', () => {
// Real absolute path so Windows path.resolve does not re-root a POSIX literal onto a drive.
// The dir need not exist — the engine only string-processes it.
const HOME = path.resolve(os.tmpdir(), 'gsd-1511-fake-home');
const configDir = path.join(HOME, '.cursor');
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-staged-'));
try {
const skillDir = path.join(stagedDir, 'gsd-help');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'Use ~/.claude/skills/ here\n');
conversion.rewriteStagedSkillBodies(stagedDir, {
runtime: 'cursor',
configDir,
scope: 'global',
homedir: () => HOME,
platform: process.platform,
});
const result = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
assert.ok(result.includes('$HOME/.cursor/skills/'), 'Should use $HOME shorthand when configDir is under homedir');
} finally {
cleanup(stagedDir);
}
});
test('non-existent stagedDir is a no-op', () => {
assert.doesNotThrow(() => {
conversion.rewriteStagedSkillBodies('/nonexistent/dir', {
runtime: 'cursor',
configDir: '/tmp/fake',
scope: 'global',
});
});
});
});
// ---------------------------------------------------------------------------
// rewriteStagedCommandBodies — returns temp dir, does not mutate source
// ---------------------------------------------------------------------------
describe('rewriteStagedCommandBodies', () => {
test('returns a temp dir (not the source dir) with rewritten content', () => {
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-cmd-'));
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-config-'));
let tempDir;
try {
// NOTE: rewrite engine handles path replacement + attribution, NOT tool renames.
fs.writeFileSync(path.join(stagedDir, 'help.md'), '# Help\n\nSee ~/.claude/skills/\n\nSee ~/.cursor/skills/\n');
tempDir = conversion.rewriteStagedCommandBodies(stagedDir, {
runtime: 'cursor',
configDir,
scope: 'global',
homedir: () => '/home/u',
platform: 'linux',
});
assert.notEqual(tempDir, stagedDir, 'must return a different dir, never the source');
assert.ok(fs.existsSync(tempDir), 'returned tempDir should exist');
const result = fs.readFileSync(path.join(tempDir, 'help.md'), 'utf8');
// Source dir should be unchanged
const source = fs.readFileSync(path.join(stagedDir, 'help.md'), 'utf8');
assert.ok(source.includes('~/.claude/skills/'), 'source file must not be mutated');
// configDir is /tmp/... (not under /home/u), so prefix = resolvedTarget + '/'
const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/');
assert.ok(result.includes(`${resolvedTarget}/skills/`), 'output should have cursor path rewrite applied');
// ~/.cursor/ also rewrites to prefix
assert.ok(!result.includes('~/.cursor/'), 'output should have ~/.cursor/ replaced too');
} finally {
cleanup(stagedDir);
cleanup(configDir);
if (tempDir && tempDir !== stagedDir) {
cleanup(tempDir);
}
}
});
test('non-existent stagedDir returns stagedDir unchanged (safe)', () => {
const result = conversion.rewriteStagedCommandBodies('/nonexistent/dir', {
runtime: 'cursor',
configDir: '/tmp/fake',
scope: 'global',
});
assert.equal(result, '/nonexistent/dir', 'should return input path unchanged for missing dir');
});
});
// ---------------------------------------------------------------------------
// Error-path: applyRuntimeContentRewritesForCommandsInPlace must rm the tempDir
// on any exception and NOT leave an orphaned gsd-cmd-rewrites-* directory.
// ---------------------------------------------------------------------------
describe('applyRuntimeContentRewritesForCommandsInPlace — error-path tempDir cleanup', () => {
test('rmSync is called on the tempDir when readFileSync throws (deterministic monkeypatch)', () => {
// Asserting the injected error propagates proves the throw happens AFTER the tempDir is
// created (the function creates tempDir, then reads .md), so the catch's rmSync cleanup
// is genuinely exercised — deterministic on every platform/uid.
const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-test-error-path-'));
fs.writeFileSync(path.join(stagedDir, 'x.md'), '# test\n');
const before = new Set(
fs.readdirSync(os.tmpdir()).filter(n => n.startsWith('gsd-cmd-rewrites-'))
);
const origReadFileSync = fs.readFileSync;
let leaked = [];
try {
fs.readFileSync = () => { throw new Error('injected read failure'); };
assert.throws(
() => conversion.applyRuntimeContentRewritesForCommandsInPlace(stagedDir, 'cursor', '/tmp/x/', false),
/injected read failure/,
);
// Restore before any further fs use so the snapshot read is trustworthy.
fs.readFileSync = origReadFileSync;
const after = fs.readdirSync(os.tmpdir()).filter(n => n.startsWith('gsd-cmd-rewrites-'));
leaked = after.filter(n => !before.has(n));
assert.deepStrictEqual(leaked, [], `tempDir not cleaned up on error: ${leaked.join(',')}`);
} finally {
// Idempotent restore — guard against early-throw paths above.
fs.readFileSync = origReadFileSync;
// Clean up the staged dir created for this test.
cleanup(stagedDir);
// Clean up any genuinely leaked gsd-cmd-rewrites-* dirs so the runner stays clean.
for (const n of leaked) {
cleanup(path.join(os.tmpdir(), n));
}
}
});
});
// ---------------------------------------------------------------------------
// Guard: runtime-artifact-layout no longer exports getInstallExports
// ---------------------------------------------------------------------------
describe('layout module no longer exports getInstallExports', () => {
test('getInstallExports is not on the layout module export', () => {
process.env['GSD_TEST_MODE'] = '1';
const layout = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
assert.equal(
typeof layout.getInstallExports,
'undefined',
'getInstallExports should have been removed from runtime-artifact-layout exports (ADR-1508 Phase 2)',
);
});
});
// ---------------------------------------------------------------------------
// DEFECT.GENERATIVE-FIX: single-owner reference-identity guard (#1511)
// Proves install.js binds to the conversion module's implementation, not a
// duplicate local copy. If these fail, a duplicate body was re-introduced.
// ---------------------------------------------------------------------------
describe('single-owner reference-identity guard (ADR-1508 / #1511 Phase 2)', () => {
let install;
let conversionCjs;
before(() => {
process.env['GSD_TEST_MODE'] = '1';
install = require('../bin/install.js');
conversionCjs = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
});
test('install.computePathPrefix === conversion._computePathPrefix (single implementation)', () => {
assert.strictEqual(
install.computePathPrefix,
conversionCjs._computePathPrefix,
'install.js must bind computePathPrefix from conversion (not a duplicate body)',
);
});
test('install.applyRuntimeContentRewritesInPlace === conversion.applyRuntimeContentRewritesInPlace (single walk loop)', () => {
assert.strictEqual(
install.applyRuntimeContentRewritesInPlace,
conversionCjs.applyRuntimeContentRewritesInPlace,
'install.js must bind applyRuntimeContentRewritesInPlace from conversion (not a duplicate walk loop)',
);
});
test('install.applyRuntimeContentRewritesForCommandsInPlace === conversion.applyRuntimeContentRewritesForCommandsInPlace (single copy+rewrite loop)', () => {
assert.strictEqual(
install.applyRuntimeContentRewritesForCommandsInPlace,
conversionCjs.applyRuntimeContentRewritesForCommandsInPlace,
'install.js must bind applyRuntimeContentRewritesForCommandsInPlace from conversion (not a duplicate copy+rewrite loop)',
);
});
test('install._applyRuntimeRewrites === conversion._applyRuntimeRewrites (single switch engine)', () => {
assert.strictEqual(
install._applyRuntimeRewrites,
conversionCjs._applyRuntimeRewrites,
'install.js must bind _applyRuntimeRewrites from conversion (not a local shim)',
);
});
// #1675 (ADR-1508): the augment converter family is single-sourced in the
// conversion module. install.js must re-bind (not re-define) these so there
// is exactly one body — the generative-drift hazard the dedup removes.
test('install.convertClaudeToAugmentMarkdown === conversion.convertClaudeToAugmentMarkdown (single converter)', () => {
assert.strictEqual(
install.convertClaudeToAugmentMarkdown,
conversionCjs.convertClaudeToAugmentMarkdown,
'install.js must bind convertClaudeToAugmentMarkdown from conversion (not a duplicate body)',
);
});
test('install.convertClaudeCommandToAugmentSkill === conversion.convertClaudeCommandToAugmentSkill (single converter)', () => {
assert.strictEqual(
install.convertClaudeCommandToAugmentSkill,
conversionCjs.convertClaudeCommandToAugmentSkill,
'install.js must bind convertClaudeCommandToAugmentSkill from conversion (not a duplicate body)',
);
});
test('install.convertClaudeAgentToAugmentAgent === conversion.convertClaudeAgentToAugmentAgent (single converter)', () => {
assert.strictEqual(
install.convertClaudeAgentToAugmentAgent,
conversionCjs.convertClaudeAgentToAugmentAgent,
'install.js must bind convertClaudeAgentToAugmentAgent from conversion (not a duplicate body)',
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────

View File

@@ -98,6 +98,8 @@ describe('#1575 — golden-parity: surface path matches install path for descrip
installContent,
`${runtime}/${fileName}: surface content must be byte-identical to install content`,
);
}
});
}
test('cursor with non-undefined attribution: surface agents byte-identical to install agents (M2 coverage)', (t) => {
@@ -124,8 +126,6 @@ describe('#1575 — golden-parity: surface path matches install path for descrip
`cursor/${fileName}: content must be byte-identical with non-undefined attribution`);
}
});
});
}
test('copilot: agents installed as .agent.md (filename rename parity)', (t) => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1575-copilot-rename-'));