fix(#2831): expand HOME in OpenCode @file references on all platforms (#2842)

* 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).
This commit is contained in:
Tom Boucher
2026-04-29 07:56:51 -04:00
committed by GitHub
parent 7fae804296
commit a7f83ee663
3 changed files with 222 additions and 102 deletions

View File

@@ -5,114 +5,53 @@
* resolve @$HOME/... file references in installed command files.
*
* Fix: install.js must use the absolute path (not $HOME-relative) when installing
* for OpenCode on Windows.
* for OpenCode. (Generalized to all platforms in #2831 — OpenCode `@file`
* references are not shell-expanded on any platform.)
*/
'use strict';
const { describe, test } = require('node:test');
const { describe, test, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const INSTALL_JS_PATH = path.join(__dirname, '..', 'bin', 'install.js');
let computePathPrefix;
before(() => {
process.env.GSD_TEST_MODE = '1';
// Re-require fresh in case other tests already loaded it.
delete require.cache[require.resolve('../bin/install.js')];
({ computePathPrefix } = require('../bin/install.js'));
});
after(() => {
delete process.env.GSD_TEST_MODE;
});
describe('bug-2376: OpenCode on Windows must use absolute path, not $HOME', () => {
test('install.js exists', () => {
assert.ok(fs.existsSync(INSTALL_JS_PATH), 'bin/install.js should exist');
test('computePathPrefix is exported by install.js', () => {
assert.equal(typeof computePathPrefix, 'function');
});
test('install.js pathPrefix computation skips $HOME for OpenCode on Windows', () => {
const content = fs.readFileSync(INSTALL_JS_PATH, 'utf-8');
// The fix must include a Windows detection condition for OpenCode
const hasWindowsOpenCodeGuard = (
content.includes('isWindowsHost') ||
(content.includes("'win32'") && content.includes('isOpencode') && content.includes('pathPrefix'))
);
assert.ok(
hasWindowsOpenCodeGuard,
'install.js must include a Windows platform guard for OpenCode pathPrefix computation'
);
test('OpenCode on Windows: pathPrefix is absolute (no $HOME substitution)', () => {
const pathPrefix = computePathPrefix({
isGlobal: true,
isOpencode: true,
isWindowsHost: true,
resolvedTarget: 'C:/Users/user/.config/opencode',
homeDir: 'C:/Users/user',
});
assert.strictEqual(pathPrefix, 'C:/Users/user/.config/opencode/');
assert.ok(!pathPrefix.includes('$HOME'));
});
test('install.js pathPrefix guard excludes $HOME for OpenCode+Windows combination', () => {
const content = fs.readFileSync(INSTALL_JS_PATH, 'utf-8');
// The pathPrefix assignment must include a guard that prevents $HOME substitution
// for OpenCode on Windows (process.platform === 'win32').
const pathPrefixBlock = content.match(/const pathPrefix[\s\S]{0,500}resolvedTarget/);
assert.ok(pathPrefixBlock, 'pathPrefix assignment block should be present');
const block = pathPrefixBlock[0];
const excludesOpenCodeWindows = (
block.includes('isWindowsHost') ||
(block.includes('isOpencode') && block.includes('win32'))
);
assert.ok(
excludesOpenCodeWindows,
'pathPrefix computation must exclude $HOME substitution for OpenCode on Windows'
);
});
test('pathPrefix simulation: OpenCode on Windows uses absolute path', () => {
// Simulate the fixed pathPrefix computation for Windows+OpenCode
const isGlobal = true;
const isOpencode = true;
const isWindowsHost = true; // simulated Windows
const resolvedTarget = 'C:/Users/user/.config/opencode';
const homeDir = 'C:/Users/user';
const pathPrefix = isGlobal && resolvedTarget.startsWith(homeDir) && !(isOpencode && isWindowsHost)
? '$HOME' + resolvedTarget.slice(homeDir.length) + '/'
: `${resolvedTarget}/`;
assert.strictEqual(
pathPrefix,
'C:/Users/user/.config/opencode/',
'OpenCode on Windows should use absolute path, not $HOME-relative'
);
assert.ok(
!pathPrefix.includes('$HOME'),
'OpenCode on Windows pathPrefix must not contain $HOME'
);
});
test('pathPrefix simulation: OpenCode on Linux/macOS still uses $HOME', () => {
// Non-Windows OpenCode should still use $HOME (POSIX shells expand it)
const isGlobal = true;
const isOpencode = true;
const isWindowsHost = false; // simulated Linux/macOS
const homeDir = '/home/user';
const resolvedTarget = '/home/user/.config/opencode';
const pathPrefix = isGlobal && resolvedTarget.startsWith(homeDir) && !(isOpencode && isWindowsHost)
? '$HOME' + resolvedTarget.slice(homeDir.length) + '/'
: `${resolvedTarget}/`;
assert.strictEqual(
pathPrefix,
'$HOME/.config/opencode/',
'OpenCode on Linux/macOS should still use $HOME-relative path'
);
});
test('pathPrefix simulation: Claude Code on Windows still uses $HOME (unaffected)', () => {
// Claude Code on Windows is handled by Claude Code's own shell, which expands $HOME
const isGlobal = true;
const isOpencode = false; // Claude Code, not OpenCode
const isWindowsHost = true;
const homeDir = 'C:/Users/user';
const resolvedTarget = 'C:/Users/user/.claude';
const pathPrefix = isGlobal && resolvedTarget.startsWith(homeDir) && !(isOpencode && isWindowsHost)
? '$HOME' + resolvedTarget.slice(homeDir.length) + '/'
: `${resolvedTarget}/`;
assert.strictEqual(
pathPrefix,
'$HOME/.claude/',
'Claude Code on Windows should still use $HOME-relative path (Claude Code handles this)'
);
test('Claude Code on Windows: pathPrefix still uses $HOME (unaffected)', () => {
const pathPrefix = computePathPrefix({
isGlobal: true,
isOpencode: false,
isWindowsHost: true,
resolvedTarget: 'C:/Users/user/.claude',
homeDir: 'C:/Users/user',
});
assert.strictEqual(pathPrefix, '$HOME/.claude/');
});
});