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:
@@ -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)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user