Files
openchamber/packages/web/server/lib/opencode/config-paths.test.js
T

117 lines
5.7 KiB
JavaScript

import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
const originalOpenCodeConfig = process.env.OPENCODE_CONFIG;
afterEach(() => {
if (originalXdgConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
else process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
if (originalOpenCodeConfig === undefined) delete process.env.OPENCODE_CONFIG;
else process.env.OPENCODE_CONFIG = originalOpenCodeConfig;
vi.resetModules();
});
async function loadOpenCodeModules() {
vi.resetModules();
return Promise.all([
import('./shared.js'),
import('./agents.js'),
import('./commands.js'),
import('./skills.js'),
import('./snippets.js'),
import('./plugins.js'),
import('./routes.js'),
]);
}
describe('OpenCode global config paths', () => {
it('derives all shared constants from a non-empty XDG_CONFIG_HOME', async () => {
const xdgConfigHome = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-xdg-'));
process.env.XDG_CONFIG_HOME = xdgConfigHome;
const [{ OPENCODE_CONFIG_DIR, AGENT_DIR, COMMAND_DIR, SKILL_DIR, CONFIG_FILE }] = await loadOpenCodeModules();
const configDir = path.join(xdgConfigHome, 'opencode');
expect(OPENCODE_CONFIG_DIR).toBe(configDir);
expect(AGENT_DIR).toBe(path.join(configDir, 'agents'));
expect(COMMAND_DIR).toBe(path.join(configDir, 'commands'));
expect(SKILL_DIR).toBe(path.join(configDir, 'skills'));
expect(CONFIG_FILE).toBe(path.join(configDir, 'config.json'));
fs.rmSync(xdgConfigHome, { recursive: true, force: true });
});
it.each([undefined, ' '])('falls back to ~/.config/opencode when XDG_CONFIG_HOME is %s', async (xdgConfigHome) => {
if (xdgConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
else process.env.XDG_CONFIG_HOME = xdgConfigHome;
const [{ OPENCODE_CONFIG_DIR }] = await loadOpenCodeModules();
expect(OPENCODE_CONFIG_DIR).toBe(path.join(os.homedir(), '.config', 'opencode'));
});
it('keeps global CRUD below XDG_CONFIG_HOME while project files stay in the project', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-config-paths-'));
const xdgConfigHome = path.join(root, 'xdg');
const projectDir = path.join(root, 'project');
process.env.XDG_CONFIG_HOME = xdgConfigHome;
delete process.env.OPENCODE_CONFIG;
const [, agents, commands, skills, snippets, plugins] = await loadOpenCodeModules();
agents.createAgent('global-agent', { description: 'Global', prompt: 'Global prompt' }, projectDir, 'user');
commands.createCommand('global-command', { description: 'Global', template: 'Global template' }, projectDir, 'user');
skills.createSkill('global-skill', { description: 'Global', instructions: 'Global instructions' }, projectDir, 'user');
snippets.createSnippet('global-snippet', { content: 'Global snippet' }, projectDir, 'global');
plugins.createPluginEntry({ spec: 'global-plugin', scope: 'user' }, projectDir);
const configDir = path.join(xdgConfigHome, 'opencode');
expect(fs.existsSync(path.join(configDir, 'agents', 'global-agent.md'))).toBe(true);
expect(fs.existsSync(path.join(configDir, 'commands', 'global-command.md'))).toBe(true);
expect(fs.existsSync(path.join(configDir, 'skills', 'global-skill', 'SKILL.md'))).toBe(true);
expect(fs.existsSync(path.join(configDir, 'snippet', 'global-snippet.md'))).toBe(true);
expect(JSON.parse(fs.readFileSync(path.join(configDir, 'config.json'), 'utf8')).plugin).toEqual(['global-plugin']);
agents.createAgent('project-agent', { description: 'Project', prompt: 'Project prompt' }, projectDir, 'project');
commands.createCommand('project-command', { description: 'Project', template: 'Project template' }, projectDir, 'project');
skills.createSkill('project-skill', { description: 'Project', instructions: 'Project instructions' }, projectDir, 'project');
expect(fs.existsSync(path.join(projectDir, '.opencode', 'agents', 'project-agent.md'))).toBe(true);
expect(fs.existsSync(path.join(projectDir, '.opencode', 'commands', 'project-command.md'))).toBe(true);
expect(fs.existsSync(path.join(projectDir, '.opencode', 'skills', 'project-skill', 'SKILL.md'))).toBe(true);
fs.rmSync(root, { recursive: true, force: true });
});
it('writes global AGENTS.md below XDG_CONFIG_HOME', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-agents-md-'));
process.env.XDG_CONFIG_HOME = path.join(root, 'xdg');
delete process.env.OPENCODE_CONFIG;
const [, , , , , , routes] = await loadOpenCodeModules();
const handlers = new Map();
const app = {
get(route, ...callbacks) { handlers.set(`GET ${route}`, callbacks.at(-1)); },
put(route, ...callbacks) { handlers.set(`PUT ${route}`, callbacks.at(-1)); },
post() {},
delete() {},
};
routes.registerOpenCodeRoutes(app, {});
const response = { json: vi.fn(), status: vi.fn(() => response) };
await handlers.get('GET /api/behavior/agents-md')({}, response);
expect(response.json).toHaveBeenLastCalledWith({
content: '', exists: false, path: path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'AGENTS.md'),
});
await handlers.get('PUT /api/behavior/agents-md')({ body: { content: 'Global behavior' } }, response);
expect(fs.readFileSync(path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'AGENTS.md'), 'utf8')).toBe('Global behavior');
await handlers.get('GET /api/behavior/agents-md')({}, response);
expect(response.json).toHaveBeenLastCalledWith({
content: 'Global behavior', exists: true, path: path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'AGENTS.md'),
});
fs.rmSync(root, { recursive: true, force: true });
});
});