* docs: align PR-FLOW push gate from gsd-test-summary to gsd-test gsd-test is the application (open-gsd/gsd-test-runner); gsd-test-summary is the legacy local wrapper. RULESET.PR-FLOW.docker-before-push now names gsd-test as the pre-push gate (exit 0 / verdict outcome 'passed'). * docs: drop WORKTREE.SEAM local node --test rule; align PROC dispatch to gsd-test - Remove WORKTREE.SEAM.execution-rule (prefer local node --test) — contradicts CLAUDE.md 'NEVER run node --test locally'; CLAUDE.md wins. - PROC.PARALLEL-FIX-DISPATCH: gsd-test-summary --both -> gsd-test (app; --both was a legacy wrapper flag). * feat(#1682): OpenCode companion-MCP binding (mcp.gsd) — Phase 5 Slice 1a configureOpencodePermissions registers the Phase-4 companion MCP server (gsd-mcp-server) as opencode mcp.gsd, so OpenCode connects to GSD's command (point 1) + state-IO (point 5) with no bespoke plugin (ADR-1239 Phase D). Idempotent + non-clobbering (add-if-absent; respects a user-defined mcp.gsd). Local-stdio schema per OpenCode config (packages/core/src/config/mcp.ts); `-p @opengsd/gsd-core` resolves the bin (name != package) under npx. Tests: registers-on-object-config; does-not-clobber-user-entry. * docs(changeset): OpenCode companion-MCP binding (#1682) * fix(#1682): use PACKAGE_NAME single-source (#516) + refresh opencode golden parity - mcp.gsd command: replace hardcoded '@opengsd/gsd-core' literal with PACKAGE_NAME from gsd-core/bin/lib/package-identity.cjs (#516 single-source). - opencode golden-install-parity fixture: refresh opencode.json hash for the added mcp.gsd block (configureOpencodePermissions output change). * docs(changeset): add docs-exempt marker (Phase 5 slice) * docs(changeset): backfill PR #1929
117 lines
4.3 KiB
JavaScript
117 lines
4.3 KiB
JavaScript
// allow-test-rule: architectural-invariant
|
|
// The finishInstall test asserts the call-site passes configDir (not a hardcoded
|
|
// path) — a load-bearing wiring invariant. All other tests call the exported
|
|
// configureOpencodePermissions function directly and assert on typed config state.
|
|
// Migrated from pending-migration-to-typed-ir per #455.
|
|
|
|
/**
|
|
* Regression tests for OpenCode permission config handling.
|
|
*
|
|
* Ensures the installer does not crash when opencode.json uses the valid
|
|
* top-level string form: "permission": "allow", and that path-specific
|
|
* permissions are written against the actual resolved install directory.
|
|
*/
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
const { configureOpencodePermissions } = require('../bin/install.js');
|
|
const { PACKAGE_NAME } = require('../gsd-core/bin/lib/package-identity.cjs');
|
|
|
|
const installSrc = fs.readFileSync(path.join(__dirname, '..', 'bin', 'install.js'), 'utf8');
|
|
|
|
const envKeys = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG', 'XDG_CONFIG_HOME'];
|
|
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
|
|
|
function restoreEnv(snapshot) {
|
|
for (const key of envKeys) {
|
|
if (snapshot[key] === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = snapshot[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('configureOpencodePermissions', () => {
|
|
let configDir;
|
|
|
|
beforeEach(() => {
|
|
configDir = createTempDir('gsd-opencode-');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(configDir);
|
|
restoreEnv(originalEnv);
|
|
});
|
|
|
|
test('does not crash or rewrite top-level string permissions', () => {
|
|
const configPath = path.join(configDir, 'opencode.json');
|
|
const original = JSON.stringify({
|
|
$schema: 'https://opencode.ai/config.json',
|
|
permission: 'allow',
|
|
skills: { paths: ['/tmp/skills'] },
|
|
}, null, 2) + '\n';
|
|
|
|
fs.writeFileSync(configPath, original);
|
|
process.env.OPENCODE_CONFIG_DIR = configDir;
|
|
|
|
assert.doesNotThrow(() => configureOpencodePermissions(true, configDir));
|
|
assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original);
|
|
});
|
|
|
|
test('adds path-specific read and external_directory permissions for object configs', () => {
|
|
const configPath = path.join(configDir, 'opencode.json');
|
|
fs.writeFileSync(configPath, JSON.stringify({ permission: {} }, null, 2) + '\n');
|
|
process.env.OPENCODE_CONFIG_DIR = configDir;
|
|
|
|
configureOpencodePermissions(true, configDir);
|
|
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
const gsdPath = `${configDir.replace(/\\/g, '/')}/gsd-core/*`;
|
|
|
|
assert.strictEqual(config.permission.read[gsdPath], 'allow');
|
|
assert.strictEqual(config.permission.external_directory[gsdPath], 'allow');
|
|
});
|
|
|
|
test('registers the companion MCP server (mcp.gsd) for object configs (#1682)', () => {
|
|
const configPath = path.join(configDir, 'opencode.json');
|
|
fs.writeFileSync(configPath, JSON.stringify({ permission: {} }, null, 2) + '\n');
|
|
process.env.OPENCODE_CONFIG_DIR = configDir;
|
|
|
|
configureOpencodePermissions(true, configDir);
|
|
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
assert.deepEqual(config.mcp.gsd, {
|
|
type: 'local',
|
|
command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
|
|
enabled: true,
|
|
});
|
|
});
|
|
|
|
test('does not clobber a user-defined mcp.gsd entry (#1682)', () => {
|
|
const configPath = path.join(configDir, 'opencode.json');
|
|
const userMcp = { type: 'local', command: ['node', '/custom/server.js'], enabled: false };
|
|
fs.writeFileSync(configPath, JSON.stringify({ permission: {}, mcp: { gsd: userMcp } }, null, 2) + '\n');
|
|
process.env.OPENCODE_CONFIG_DIR = configDir;
|
|
|
|
configureOpencodePermissions(true, configDir);
|
|
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
// User's own mcp.gsd is preserved untouched (Hyrum's Law — non-clobbering).
|
|
assert.deepEqual(config.mcp.gsd, userMcp);
|
|
});
|
|
|
|
test('finishInstall passes the actual config dir to OpenCode permissions', () => {
|
|
assert.ok(
|
|
installSrc.includes('configureOpencodePermissions(isGlobal, configDir);'),
|
|
'OpenCode permission config uses actual install dir'
|
|
);
|
|
});
|
|
});
|