* fix(#2831): expand HOME in OpenCode skill/template paths OpenCode does not shell-expand $HOME in @file references on any platform — the literal `@$HOME/...` path is resolved relative to the config command/ dir, producing `command/$HOME/...` (file not found). The previous fix for #2376 only guarded Windows; extend to all platforms. Closes #2831 * test(#2831): assert behavior via exported computePathPrefix, not source grep Addresses CodeRabbit review on PR #2842: - Extracts pathPrefix logic into a named, test-exported computePathPrefix helper in bin/install.js (no behavior change at the call site). - Rewrites bug-2376 and bug-2831 regression tests to call the exported function directly instead of regex-matching install.js source text, per the repo's no-source-grep testing standard. - Wraps temp-dir test setup in try/finally so cleanup runs on assertion failures (no leaked tmp dirs).
150 lines
5.3 KiB
JavaScript
150 lines
5.3 KiB
JavaScript
/**
|
|
* Regression test for #2831: OpenCode @file references contain literal `$HOME`
|
|
* which OpenCode does not expand — `@$HOME/.config/opencode/...` is resolved
|
|
* as a path relative to the config command/ dir, producing
|
|
* `command/$HOME/.config/opencode/...` (file not found).
|
|
*
|
|
* Root cause: install.js pathPrefix used `$HOME`-relative paths for OpenCode on
|
|
* non-Windows hosts (only Windows was guarded by #2376). OpenCode's `@file`
|
|
* include syntax does NOT shell-expand `$HOME` on any platform.
|
|
*
|
|
* Fix: pathPrefix must use the absolute path for OpenCode on all platforms.
|
|
*
|
|
* Tests exercise install.js's exported `computePathPrefix` directly (no source
|
|
* grepping) and additionally simulate the `copyFlattenedCommands` substitution
|
|
* pipeline on a temp tree to verify no `$HOME` literal leaks into emitted files.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
let computePathPrefix;
|
|
|
|
before(() => {
|
|
process.env.GSD_TEST_MODE = '1';
|
|
delete require.cache[require.resolve('../bin/install.js')];
|
|
({ computePathPrefix } = require('../bin/install.js'));
|
|
});
|
|
|
|
after(() => {
|
|
delete process.env.GSD_TEST_MODE;
|
|
});
|
|
|
|
describe('bug-2831: OpenCode pathPrefix uses absolute path on all platforms', () => {
|
|
test('computePathPrefix is exported by install.js', () => {
|
|
assert.equal(typeof computePathPrefix, 'function');
|
|
});
|
|
|
|
test('OpenCode on macOS: pathPrefix is absolute (no $HOME)', () => {
|
|
const pathPrefix = computePathPrefix({
|
|
isGlobal: true,
|
|
isOpencode: true,
|
|
isWindowsHost: false,
|
|
homeDir: '/Users/alice',
|
|
resolvedTarget: '/Users/alice/.config/opencode',
|
|
});
|
|
assert.strictEqual(pathPrefix, '/Users/alice/.config/opencode/');
|
|
assert.ok(!pathPrefix.includes('$HOME'));
|
|
});
|
|
|
|
test('OpenCode on Linux: pathPrefix is absolute (no $HOME)', () => {
|
|
const pathPrefix = computePathPrefix({
|
|
isGlobal: true,
|
|
isOpencode: true,
|
|
isWindowsHost: false,
|
|
homeDir: '/home/bob',
|
|
resolvedTarget: '/home/bob/.config/opencode',
|
|
});
|
|
assert.strictEqual(pathPrefix, '/home/bob/.config/opencode/');
|
|
assert.ok(!pathPrefix.includes('$HOME'));
|
|
});
|
|
|
|
test('OpenCode on Windows: pathPrefix is absolute (preserves #2376)', () => {
|
|
const pathPrefix = computePathPrefix({
|
|
isGlobal: true,
|
|
isOpencode: true,
|
|
isWindowsHost: true,
|
|
homeDir: 'C:/Users/carol',
|
|
resolvedTarget: 'C:/Users/carol/.config/opencode',
|
|
});
|
|
assert.strictEqual(pathPrefix, 'C:/Users/carol/.config/opencode/');
|
|
});
|
|
|
|
test('Claude Code on macOS: pathPrefix still uses $HOME (unaffected)', () => {
|
|
const pathPrefix = computePathPrefix({
|
|
isGlobal: true,
|
|
isOpencode: false,
|
|
isWindowsHost: false,
|
|
homeDir: '/Users/alice',
|
|
resolvedTarget: '/Users/alice/.claude',
|
|
});
|
|
assert.strictEqual(pathPrefix, '$HOME/.claude/');
|
|
});
|
|
|
|
test('Local install (non-global): pathPrefix uses absolute path regardless of runtime', () => {
|
|
const pathPrefix = computePathPrefix({
|
|
isGlobal: false,
|
|
isOpencode: false,
|
|
isWindowsHost: false,
|
|
homeDir: '/Users/alice',
|
|
resolvedTarget: '/Users/alice/projects/foo/.claude',
|
|
});
|
|
assert.strictEqual(pathPrefix, '/Users/alice/projects/foo/.claude/');
|
|
});
|
|
|
|
test('Substitution pipeline simulation: OpenCode emits no @$HOME literal', () => {
|
|
// This validates the same regex substitution pipeline used by
|
|
// copyFlattenedCommands when writing OpenCode command files. We invoke the
|
|
// real exported computePathPrefix; the regex passes mirror the install.js
|
|
// call sites (globalClaudeRegex / globalClaudeHomeRegex).
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2831-'));
|
|
try {
|
|
const srcRoot = path.join(tmp, 'src');
|
|
const targetRoot = path.join(tmp, 'home', '.config', 'opencode');
|
|
const srcCmdDir = path.join(srcRoot, 'commands', 'gsd');
|
|
fs.mkdirSync(srcCmdDir, { recursive: true });
|
|
fs.mkdirSync(targetRoot, { recursive: true });
|
|
|
|
const srcFile = path.join(srcCmdDir, 'autonomous.md');
|
|
fs.writeFileSync(
|
|
srcFile,
|
|
'---\nname: autonomous\n---\n<execution_context>\n@~/.claude/get-shit-done/workflows/autonomous.md\n@$HOME/.claude/get-shit-done/references/ui-brand.md\n</execution_context>\n'
|
|
);
|
|
|
|
const homeDir = path.join(tmp, 'home').replace(/\\/g, '/');
|
|
const resolvedTarget = targetRoot.replace(/\\/g, '/');
|
|
const pathPrefix = computePathPrefix({
|
|
isGlobal: true,
|
|
isOpencode: true,
|
|
isWindowsHost: false,
|
|
homeDir,
|
|
resolvedTarget,
|
|
});
|
|
|
|
let content = fs.readFileSync(srcFile, 'utf8');
|
|
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
|
|
assert.ok(
|
|
!/@\$HOME\b/.test(content),
|
|
`output must not contain @$HOME literal; got:\n${content}`
|
|
);
|
|
assert.ok(
|
|
!/\$HOME\b/.test(content),
|
|
`output must not contain $HOME literal; got:\n${content}`
|
|
);
|
|
assert.ok(
|
|
content.includes(`@${resolvedTarget}/`),
|
|
`output should include absolute path with @ prefix; got:\n${content}`
|
|
);
|
|
} finally {
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|