| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558 |
- import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
- import * as fs from 'node:fs';
- import * as os from 'node:os';
- import * as path from 'node:path';
- import type { ConfigLoadWarning } from './loader';
- import { loadAgentPrompt, loadPluginConfig } from './loader';
- // Test deepMerge indirectly through loadPluginConfig behavior
- // since deepMerge is not exported
- describe('loadPluginConfig', () => {
- let tempDir: string;
- let userConfigDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'loader-test-'));
- userConfigDir = path.join(tempDir, 'user-config');
- originalEnv = { ...process.env };
- // Isolate from real user config
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = userConfigDir;
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('returns empty config when no config files exist', () => {
- const projectDir = path.join(tempDir, 'project');
- fs.mkdirSync(projectDir, { recursive: true });
- const config = loadPluginConfig(projectDir);
- expect(config).toEqual({});
- });
- test('loads project config from .opencode directory', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: { model: 'test/model' },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('test/model');
- });
- test('loads autoUpdate flag when configured', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- autoUpdate: false,
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.autoUpdate).toBe(false);
- });
- test('ignores invalid config (schema violation or malformed JSON)', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- // Test 1: Invalid temperature (out of range)
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({ agents: { oracle: { temperature: 5 } } }),
- );
- expect(loadPluginConfig(projectDir)).toEqual({});
- // Test 2: Malformed JSON
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- '{ invalid json }',
- );
- expect(loadPluginConfig(projectDir)).toEqual({});
- });
- test('accepts prompt on built-in agents and rejects orchestratorPrompt on orchestrator in config files', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: {
- model: 'openai/gpt-5.5',
- prompt: 'This is now allowed for built-in agents.',
- },
- },
- }),
- );
- const loaded = loadPluginConfig(projectDir);
- expect(loaded.agents?.oracle?.prompt).toBe(
- 'This is now allowed for built-in agents.',
- );
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- orchestrator: {
- model: 'openai/gpt-5.5',
- orchestratorPrompt: 'This must be rejected.',
- },
- },
- }),
- );
- expect(loadPluginConfig(projectDir)).toEqual({});
- });
- test('respects OPENCODE_CONFIG_DIR for user config location', () => {
- const customDir = fs.mkdtempSync(
- path.join(os.tmpdir(), 'omc-opencode-config-'),
- );
- process.env.OPENCODE_CONFIG_DIR = customDir;
- // Write plugin config in the custom directory
- fs.writeFileSync(
- path.join(customDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: 'custom/model-from-opencode-config-dir' } },
- }),
- );
- const projectDir = path.join(tempDir, 'project');
- fs.mkdirSync(projectDir, { recursive: true });
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe(
- 'custom/model-from-opencode-config-dir',
- );
- fs.rmSync(customDir, { recursive: true, force: true });
- });
- test('falls back to default user config dir when OPENCODE_CONFIG_DIR has no config', () => {
- const customDir = fs.mkdtempSync(
- path.join(os.tmpdir(), 'omc-opencode-config-empty-'),
- );
- process.env.OPENCODE_CONFIG_DIR = customDir;
- const defaultConfigDir = path.join(userConfigDir, 'opencode');
- fs.mkdirSync(defaultConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(defaultConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: 'fallback/default-config' } },
- }),
- );
- const projectDir = path.join(tempDir, 'project');
- fs.mkdirSync(projectDir, { recursive: true });
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('fallback/default-config');
- fs.rmSync(customDir, { recursive: true, force: true });
- });
- });
- describe('onWarning callback', () => {
- let tempDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'onwarning-test-'));
- originalEnv = { ...process.env };
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = tempDir;
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('invalid schema calls onWarning with invalid-schema', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({ agents: { oracle: { temperature: 5 } } }),
- );
- const warnings: ConfigLoadWarning[] = [];
- const config = loadPluginConfig(projectDir, {
- onWarning: (warning) => warnings.push(warning),
- });
- expect(warnings).toHaveLength(1);
- expect(warnings[0]?.kind).toBe('invalid-schema');
- expect(warnings[0]?.path).toBe(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- );
- expect(warnings[0]?.message).toBe('Config does not match schema');
- expect(config).toEqual({});
- });
- test('invalid JSON calls onWarning with invalid-json', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- '{ invalid json }',
- );
- const warnings: ConfigLoadWarning[] = [];
- const config = loadPluginConfig(projectDir, {
- onWarning: (warning) => warnings.push(warning),
- });
- expect(warnings).toHaveLength(1);
- expect(warnings[0]?.kind).toBe('invalid-json');
- expect(warnings[0]?.path).toBe(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- );
- expect(config).toEqual({});
- });
- test('silent option suppresses console warnings', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- '{ invalid json }',
- );
- const warnSpy = spyOn(console, 'warn').mockImplementation(() => {});
- try {
- const warnings: ConfigLoadWarning[] = [];
- const config = loadPluginConfig(projectDir, {
- silent: true,
- onWarning: (warning) => warnings.push(warning),
- });
- expect(warnings).toHaveLength(1);
- expect(config).toEqual({});
- expect(warnSpy).not.toHaveBeenCalled();
- } finally {
- warnSpy.mockRestore();
- }
- });
- test('read error calls onWarning with read-error', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- const configPath = path.join(projectConfigDir, 'oh-my-opencode-slim.json');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(configPath, JSON.stringify({}));
- const originalReadFileSync = fs.readFileSync;
- const readSpy = spyOn(fs, 'readFileSync').mockImplementation(((
- ...args: Parameters<typeof fs.readFileSync>
- ) => {
- const [filePath] = args;
- if (filePath === configPath) {
- const error = new Error('Permission denied') as NodeJS.ErrnoException;
- error.code = 'EACCES';
- throw error;
- }
- return originalReadFileSync(...args);
- }) as typeof fs.readFileSync);
- try {
- const warnings: ConfigLoadWarning[] = [];
- const config = loadPluginConfig(projectDir, {
- onWarning: (warning) => warnings.push(warning),
- });
- expect(warnings).toHaveLength(1);
- expect(warnings[0]?.kind).toBe('read-error');
- expect(warnings[0]?.path).toBe(configPath);
- expect(warnings[0]?.message).toBe('Permission denied');
- expect(config).toEqual({});
- } finally {
- readSpy.mockRestore();
- }
- });
- test('missing preset calls onWarning with missing-preset', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'nonexistent',
- presets: { other: { oracle: { model: 'other' } } },
- agents: { oracle: { model: 'root' } },
- }),
- );
- const warnings: ConfigLoadWarning[] = [];
- const config = loadPluginConfig(projectDir, {
- onWarning: (warning) => warnings.push(warning),
- });
- expect(warnings).toHaveLength(1);
- expect(warnings[0]?.kind).toBe('missing-preset');
- expect(warnings[0]?.message).toContain('Preset "nonexistent" not found');
- expect(config.agents?.oracle?.model).toBe('root');
- });
- test('silent: true on missing preset still calls onWarning but not console.warn', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'nonexistent',
- presets: { other: { oracle: { model: 'other' } } },
- agents: { oracle: { model: 'root' } },
- }),
- );
- const warnSpy = spyOn(console, 'warn').mockImplementation(() => {});
- try {
- const warnings: ConfigLoadWarning[] = [];
- const config = loadPluginConfig(projectDir, {
- silent: true,
- onWarning: (warning) => warnings.push(warning),
- });
- expect(warnings).toHaveLength(1);
- expect(warnings[0]?.kind).toBe('missing-preset');
- expect(config.agents?.oracle?.model).toBe('root');
- expect(warnSpy).not.toHaveBeenCalled();
- } finally {
- warnSpy.mockRestore();
- }
- });
- test('valid config does not call onWarning', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({ agents: { oracle: { model: 'valid/model' } } }),
- );
- const warnings: ConfigLoadWarning[] = [];
- const config = loadPluginConfig(projectDir, {
- onWarning: (warning) => warnings.push(warning),
- });
- expect(warnings).toHaveLength(0);
- expect(config.agents?.oracle?.model).toBe('valid/model');
- });
- test('no options object does not break loadPluginConfig', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({ agents: { oracle: { model: 'model' } } }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('model');
- });
- });
- describe('deepMerge behavior', () => {
- let tempDir: string;
- let userConfigDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'merge-test-'));
- userConfigDir = path.join(tempDir, 'user-config');
- originalEnv = { ...process.env };
- // Set XDG_CONFIG_HOME to control user config location
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = userConfigDir;
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('merges nested agent configs from user and project', () => {
- // Create user config
- const userOpencodeDir = path.join(userConfigDir, 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: { model: 'user/oracle-model', temperature: 0.5 },
- explorer: { model: 'user/explorer-model' },
- },
- }),
- );
- // Create project config (should override/merge with user)
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: { temperature: 0.8 }, // Override temperature only
- designer: { model: 'project/designer-model' }, // Add new agent
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- // oracle: model from user, temperature from project
- expect(config.agents?.oracle?.model).toBe('user/oracle-model');
- expect(config.agents?.oracle?.temperature).toBe(0.8);
- // explorer: from user only
- expect(config.agents?.explorer?.model).toBe('user/explorer-model');
- // designer: from project only
- expect(config.agents?.designer?.model).toBe('project/designer-model');
- });
- test('merges nested tmux configs', () => {
- const userOpencodeDir = path.join(userConfigDir, 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- tmux: {
- enabled: true,
- layout: 'main-vertical',
- main_pane_size: 60,
- },
- }),
- );
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- tmux: {
- enabled: false, // Override enabled
- layout: 'tiled', // Override layout
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.tmux?.enabled).toBe(false); // From project (override)
- expect(config.tmux?.layout).toBe('tiled'); // From project
- expect(config.tmux?.main_pane_size).toBe(60); // From user (preserved)
- });
- test("preserves user tmux.enabled when project doesn't specify", () => {
- const userOpencodeDir = path.join(userConfigDir, 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- tmux: {
- enabled: true,
- layout: 'main-vertical',
- },
- }),
- );
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: 'test' } }, // No tmux override
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.tmux?.enabled).toBe(true); // Preserved from user
- expect(config.tmux?.layout).toBe('main-vertical'); // Preserved from user
- });
- test('project config overrides top-level arrays', () => {
- const userOpencodeDir = path.join(userConfigDir, 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- disabled_mcps: ['websearch'],
- }),
- );
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- disabled_mcps: ['context7'],
- }),
- );
- const config = loadPluginConfig(projectDir);
- // disabled_mcps should be from project (overwrites, not merges)
- expect(config.disabled_mcps).toEqual(['context7']);
- });
- test('handles missing user config gracefully', () => {
- // Don't create user config, only project
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: { model: 'project/model' },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('project/model');
- });
- test('handles missing project config gracefully', () => {
- const userOpencodeDir = path.join(userConfigDir, 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: { model: 'user/model' },
- },
- }),
- );
- // No project config
- const projectDir = path.join(tempDir, 'project');
- fs.mkdirSync(projectDir, { recursive: true });
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('user/model');
- });
- test('merges fallback timeout from user and project', () => {
- const userOpencodeDir = path.join(userConfigDir, 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- fallback: {
- enabled: true,
- },
- }),
- );
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- fallback: {
- enabled: false,
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- // Fallback deepMerge: project value wins over user value
- expect(config.fallback?.enabled).toBe(false);
- });
- });
- describe('preset resolution', () => {
- let tempDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'preset-test-'));
- originalEnv = { ...process.env };
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = path.join(tempDir, 'user-config');
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('backward compatibility: config with only agents works unchanged', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: 'direct-model' } },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('direct-model');
- expect(config.preset).toBeUndefined();
- });
- test("preset applied: preset + presets returns preset's agents", () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'fast',
- presets: {
- fast: { oracle: { model: 'fast-model' } },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('fast-model');
- });
- test('root agents override preset agents', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'fast',
- presets: {
- fast: {
- oracle: { model: 'fast-model', temperature: 0.1 },
- explorer: { model: 'explorer-model' },
- },
- },
- agents: {
- oracle: { temperature: 0.9 }, // Should override preset temperature
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('fast-model');
- expect(config.agents?.oracle?.temperature).toBe(0.9);
- expect(config.agents?.explorer?.model).toBe('explorer-model');
- });
- test('missing preset: preset set but not in presets -> returns empty/root agents', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'nonexistent',
- presets: {
- other: { oracle: { model: 'other' } },
- },
- agents: { oracle: { model: 'root' } },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('root');
- });
- test('preset only: no root agents, just preset works', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'dev',
- presets: {
- dev: { oracle: { model: 'dev-model' } },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('dev-model');
- });
- test('invalid preset shape: bad agent config in preset fails schema validation', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- // preset agents with invalid temperature
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'invalid',
- presets: {
- invalid: { oracle: { temperature: 5 } },
- },
- }),
- );
- // Should return empty config due to validation failure
- expect(loadPluginConfig(projectDir)).toEqual({});
- });
- test('nonexistent preset from config warns and falls back to root agents', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'nonexistent',
- presets: {
- other: { oracle: { model: 'other' } },
- },
- agents: { oracle: { model: 'root' } },
- }),
- );
- const consoleWarnSpy = spyOn(console, 'warn');
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('root');
- expect(consoleWarnSpy).toHaveBeenCalled();
- const warningMessage = consoleWarnSpy.mock.calls[0][0] as string;
- expect(warningMessage).toContain('Preset "nonexistent" not found');
- expect(warningMessage).toContain('Available presets: other');
- });
- test('nonexistent preset with no root agents returns empty agents', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'nonexistent',
- presets: {
- other: { oracle: { model: 'other' } },
- },
- }),
- );
- const consoleWarnSpy = spyOn(console, 'warn');
- const config = loadPluginConfig(projectDir);
- expect(config.agents).toBeUndefined();
- expect(consoleWarnSpy).toHaveBeenCalled();
- const warningMessage = consoleWarnSpy.mock.calls[0][0] as string;
- expect(warningMessage).toContain('Preset "nonexistent" not found');
- });
- test('options from preset are deep-merged with root agents', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'openai',
- presets: {
- openai: {
- oracle: {
- model: 'openai/gpt-5.5',
- options: { textVerbosity: 'low' },
- },
- },
- },
- agents: {
- oracle: {
- options: { reasoningEffort: 'medium' },
- },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('openai/gpt-5.5');
- // deepMerge should combine both option keys
- expect(config.agents?.oracle?.options).toEqual({
- textVerbosity: 'low',
- reasoningEffort: 'medium',
- });
- });
- test('options from preset only work without root agents', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'anthropic-thinking',
- presets: {
- 'anthropic-thinking': {
- oracle: {
- model: 'anthropic/claude-sonnet-4-6',
- options: {
- thinking: { type: 'enabled', budgetTokens: 16000 },
- },
- },
- },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('anthropic/claude-sonnet-4-6');
- expect(config.agents?.oracle?.options).toEqual({
- thinking: { type: 'enabled', budgetTokens: 16000 },
- });
- });
- test('root options override preset options for same key', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'concise',
- presets: {
- concise: {
- oracle: {
- model: 'openai/gpt-5.5',
- options: { textVerbosity: 'low' },
- },
- },
- },
- agents: {
- oracle: {
- options: { textVerbosity: 'high' },
- },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('openai/gpt-5.5');
- // root wins over preset for same key
- expect(config.agents?.oracle?.options).toEqual({
- textVerbosity: 'high',
- });
- });
- });
- describe('environment variable preset override', () => {
- let tempDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-preset-test-'));
- originalEnv = { ...process.env };
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = path.join(tempDir, 'user-config');
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('Env var overrides preset from config file', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'config-preset',
- presets: {
- 'config-preset': { oracle: { model: 'config-model' } },
- 'env-preset': { oracle: { model: 'env-model' } },
- },
- }),
- );
- process.env.OH_MY_OPENCODE_SLIM_PRESET = 'env-preset';
- const config = loadPluginConfig(projectDir);
- expect(config.preset).toBe('env-preset');
- expect(config.agents?.oracle?.model).toBe('env-model');
- });
- test('Env var works when config has no preset', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- presets: {
- 'env-preset': { oracle: { model: 'env-model' } },
- },
- }),
- );
- process.env.OH_MY_OPENCODE_SLIM_PRESET = 'env-preset';
- const config = loadPluginConfig(projectDir);
- expect(config.preset).toBe('env-preset');
- expect(config.agents?.oracle?.model).toBe('env-model');
- });
- test('Env var is ignored if empty string', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'config-preset',
- presets: {
- 'config-preset': { oracle: { model: 'config-model' } },
- },
- }),
- );
- process.env.OH_MY_OPENCODE_SLIM_PRESET = '';
- const config = loadPluginConfig(projectDir);
- expect(config.preset).toBe('config-preset');
- expect(config.agents?.oracle?.model).toBe('config-model');
- });
- test('Env var is ignored if undefined', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'config-preset',
- presets: {
- 'config-preset': { oracle: { model: 'config-model' } },
- },
- }),
- );
- delete process.env.OH_MY_OPENCODE_SLIM_PRESET;
- const config = loadPluginConfig(projectDir);
- expect(config.preset).toBe('config-preset');
- expect(config.agents?.oracle?.model).toBe('config-model');
- });
- test('Env var with nonexistent preset warns and falls back', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- preset: 'config-preset',
- presets: {
- 'config-preset': { oracle: { model: 'config-model' } },
- },
- agents: { oracle: { model: 'fallback' } },
- }),
- );
- process.env.OH_MY_OPENCODE_SLIM_PRESET = 'typo-preset';
- const consoleWarnSpy = spyOn(console, 'warn');
- const config = loadPluginConfig(projectDir);
- expect(config.preset).toBe('typo-preset');
- expect(config.agents?.oracle?.model).toBe('fallback');
- expect(consoleWarnSpy).toHaveBeenCalled();
- const calls = consoleWarnSpy.mock.calls as string[][];
- const warningMessage =
- calls.find((call) => call[0]?.includes('typo-preset'))?.[0] || '';
- expect(warningMessage).toContain('Preset "typo-preset" not found');
- expect(warningMessage).toContain('environment variable');
- expect(warningMessage).toContain('config-preset');
- });
- });
- describe('JSONC config support', () => {
- let tempDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonc-test-'));
- originalEnv = { ...process.env };
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = path.join(tempDir, 'user-config');
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('loads .jsonc file with single-line comments', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.jsonc'),
- `{
- // This is a comment
- "agents": {
- "oracle": { "model": "test/model" } // inline comment
- }
- }`,
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('test/model');
- });
- test('loads .jsonc file with multi-line comments', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.jsonc'),
- `{
- /* Multi-line
- comment block */
- "agents": {
- "explorer": { "model": "explorer-model" }
- }
- }`,
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.explorer?.model).toBe('explorer-model');
- });
- test('loads .jsonc file with trailing commas', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.jsonc'),
- `{
- "agents": {
- "oracle": { "model": "test-model", },
- },
- }`,
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('test-model');
- });
- test('prefers .jsonc over .json when both exist', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- // Create both files
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({ agents: { oracle: { model: 'json-model' } } }),
- );
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.jsonc'),
- `{
- // JSONC version
- "agents": { "oracle": { "model": "jsonc-model" } }
- }`,
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('jsonc-model');
- });
- test('falls back to .json when .jsonc does not exist', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- // Only create .json file
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({ agents: { oracle: { model: 'json-model' } } }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('json-model');
- });
- test('loads user config from .jsonc', () => {
- const userOpencodeDir = path.join(tempDir, 'user-config', 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.jsonc'),
- `{
- // User config with comments
- "agents": { "librarian": { "model": "user-librarian" } }
- }`,
- );
- const projectDir = path.join(tempDir, 'project');
- fs.mkdirSync(projectDir, { recursive: true });
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.librarian?.model).toBe('user-librarian');
- });
- test('merges user .jsonc with project .jsonc', () => {
- const userOpencodeDir = path.join(tempDir, 'user-config', 'opencode');
- fs.mkdirSync(userOpencodeDir, { recursive: true });
- fs.writeFileSync(
- path.join(userOpencodeDir, 'oh-my-opencode-slim.jsonc'),
- `{
- // User config
- "agents": {
- "oracle": { "model": "user-oracle", "temperature": 0.5 }
- }
- }`,
- );
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.jsonc'),
- `{
- // Project config
- "agents": { "oracle": { "temperature": 0.8 } }
- }`,
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('user-oracle');
- expect(config.agents?.oracle?.temperature).toBe(0.8);
- });
- test('handles complex JSONC with mixed comments and trailing commas', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.jsonc'),
- `{
- // Main configuration
- "preset": "dev",
- /* Presets definition */
- "presets": {
- "dev": {
- // Development agents
- "oracle": { "model": "dev-oracle", },
- "explorer": { "model": "dev-explorer", },
- },
- },
- "tmux": {
- "enabled": true, // Enable tmux
- "layout": "main-vertical",
- },
- }`,
- );
- const config = loadPluginConfig(projectDir);
- expect(config.preset).toBe('dev');
- expect(config.agents?.oracle?.model).toBe('dev-oracle');
- expect(config.agents?.explorer?.model).toBe('dev-explorer');
- expect(config.tmux?.enabled).toBe(true);
- expect(config.tmux?.layout).toBe('main-vertical');
- });
- });
- describe('loadAgentPrompt', () => {
- let tempDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prompt-test-'));
- originalEnv = { ...process.env };
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = tempDir;
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('returns empty object when no prompt files exist', () => {
- const result = loadAgentPrompt('oracle');
- expect(result).toEqual({});
- });
- test('loads replacement prompt from {agent}.md', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- fs.mkdirSync(promptsDir, { recursive: true });
- fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'replacement prompt');
- const result = loadAgentPrompt('oracle');
- expect(result.prompt).toBe('replacement prompt');
- expect(result.appendPrompt).toBeUndefined();
- });
- test('loads append prompt from {agent}_append.md', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- fs.mkdirSync(promptsDir, { recursive: true });
- fs.writeFileSync(
- path.join(promptsDir, 'oracle_append.md'),
- 'append prompt',
- );
- const result = loadAgentPrompt('oracle');
- expect(result.prompt).toBeUndefined();
- expect(result.appendPrompt).toBe('append prompt');
- });
- test('loads both replacement and append prompts', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- fs.mkdirSync(promptsDir, { recursive: true });
- fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'replacement prompt');
- fs.writeFileSync(
- path.join(promptsDir, 'oracle_append.md'),
- 'append prompt',
- );
- const result = loadAgentPrompt('oracle');
- expect(result.prompt).toBe('replacement prompt');
- expect(result.appendPrompt).toBe('append prompt');
- });
- test('handles file read errors gracefully', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- fs.mkdirSync(promptsDir, { recursive: true });
- const promptPath = path.join(promptsDir, 'error-agent.md');
- fs.writeFileSync(promptPath, 'content');
- const consoleWarnSpy = spyOn(console, 'warn');
- // Use a unique agent name and check for it specifically
- const originalReadFileSync = fs.readFileSync;
- const readSpy = spyOn(fs, 'readFileSync').mockImplementation(((
- ...args: Parameters<typeof fs.readFileSync>
- ) => {
- const [p] = args;
- if (typeof p === 'string' && p.includes('error-agent.md')) {
- throw new Error('Read error');
- }
- return originalReadFileSync(...args);
- }) as typeof fs.readFileSync);
- try {
- const result = loadAgentPrompt('error-agent');
- expect(result.prompt).toBeUndefined();
- const warningFound = consoleWarnSpy.mock.calls.some((call) =>
- (call[0] as string).includes('Error reading prompt file'),
- );
- expect(warningFound).toBe(true);
- } finally {
- readSpy.mockRestore();
- }
- });
- test('prefers preset prompt files over root prompts', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- const presetDir = path.join(promptsDir, 'test');
- fs.mkdirSync(presetDir, { recursive: true });
- fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'root replacement');
- fs.writeFileSync(path.join(presetDir, 'oracle.md'), 'preset replacement');
- fs.writeFileSync(
- path.join(promptsDir, 'oracle_append.md'),
- 'root append prompt',
- );
- fs.writeFileSync(
- path.join(presetDir, 'oracle_append.md'),
- 'preset append prompt',
- );
- const result = loadAgentPrompt('oracle', 'test');
- expect(result.prompt).toBe('preset replacement');
- expect(result.appendPrompt).toBe('preset append prompt');
- });
- test('falls back to root prompt files when preset files are missing', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- const presetDir = path.join(promptsDir, 'test');
- fs.mkdirSync(presetDir, { recursive: true });
- fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'root replacement');
- fs.writeFileSync(
- path.join(promptsDir, 'oracle_append.md'),
- 'root append prompt',
- );
- const result = loadAgentPrompt('oracle', 'test');
- expect(result.prompt).toBe('root replacement');
- expect(result.appendPrompt).toBe('root append prompt');
- });
- test('falls back independently between preset and root files', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- const presetDir = path.join(promptsDir, 'test');
- fs.mkdirSync(presetDir, { recursive: true });
- fs.writeFileSync(path.join(presetDir, 'oracle.md'), 'preset replacement');
- fs.writeFileSync(
- path.join(promptsDir, 'oracle_append.md'),
- 'root append prompt',
- );
- const result = loadAgentPrompt('oracle', 'test');
- expect(result.prompt).toBe('preset replacement');
- expect(result.appendPrompt).toBe('root append prompt');
- });
- test('ignores unsafe preset names for prompt lookup', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- fs.mkdirSync(promptsDir, { recursive: true });
- fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'root replacement');
- const result = loadAgentPrompt('oracle', '../test');
- expect(result.prompt).toBe('root replacement');
- expect(result.appendPrompt).toBeUndefined();
- });
- test('falls back to root when preset prompt file read fails', () => {
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- const presetDir = path.join(promptsDir, 'test');
- fs.mkdirSync(presetDir, { recursive: true });
- const presetPromptPath = path.join(presetDir, 'oracle.md');
- fs.writeFileSync(presetPromptPath, 'preset replacement');
- fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'root replacement');
- const consoleWarnSpy = spyOn(console, 'warn');
- const originalReadFileSync = fs.readFileSync;
- const readSpy = spyOn(fs, 'readFileSync').mockImplementation(((
- ...args: Parameters<typeof fs.readFileSync>
- ) => {
- const [p] = args;
- if (typeof p === 'string' && p === presetPromptPath) {
- throw new Error('Preset read error');
- }
- return originalReadFileSync(...args);
- }) as typeof fs.readFileSync);
- try {
- const result = loadAgentPrompt('oracle', 'test');
- expect(result.prompt).toBe('root replacement');
- expect(consoleWarnSpy).toHaveBeenCalled();
- } finally {
- readSpy.mockRestore();
- }
- });
- test('works with XDG_CONFIG_HOME environment variable', () => {
- const customConfigHome = path.join(tempDir, 'custom-xdg');
- process.env.XDG_CONFIG_HOME = customConfigHome;
- const promptsDir = path.join(
- customConfigHome,
- 'opencode',
- 'oh-my-opencode-slim',
- );
- fs.mkdirSync(promptsDir, { recursive: true });
- fs.writeFileSync(path.join(promptsDir, 'xdg-agent.md'), 'xdg prompt');
- const result = loadAgentPrompt('xdg-agent');
- expect(result.prompt).toBe('xdg prompt');
- });
- test('respects OPENCODE_CONFIG_DIR for prompt location', () => {
- const customDir = fs.mkdtempSync(
- path.join(os.tmpdir(), 'omc-prompt-config-'),
- );
- process.env.OPENCODE_CONFIG_DIR = customDir;
- const promptsDir = path.join(customDir, 'oh-my-opencode-slim');
- fs.mkdirSync(promptsDir, { recursive: true });
- fs.writeFileSync(
- path.join(promptsDir, 'oracle.md'),
- 'prompt from OPENCODE_CONFIG_DIR dir',
- );
- const result = loadAgentPrompt('oracle');
- expect(result.prompt).toBe('prompt from OPENCODE_CONFIG_DIR dir');
- fs.rmSync(customDir, { recursive: true, force: true });
- });
- test('falls back to default prompt dir when OPENCODE_CONFIG_DIR has no prompt', () => {
- const customDir = fs.mkdtempSync(
- path.join(os.tmpdir(), 'omc-prompt-config-empty-'),
- );
- process.env.OPENCODE_CONFIG_DIR = customDir;
- const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
- fs.mkdirSync(promptsDir, { recursive: true });
- fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'fallback prompt');
- const result = loadAgentPrompt('oracle');
- expect(result.prompt).toBe('fallback prompt');
- fs.rmSync(customDir, { recursive: true, force: true });
- });
- });
- describe('env variable interpolation', () => {
- let tempDir: string;
- let originalEnv: typeof process.env;
- beforeEach(() => {
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-interp-test-'));
- originalEnv = { ...process.env };
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = path.join(tempDir, 'user-config');
- });
- afterEach(() => {
- fs.rmSync(tempDir, { recursive: true, force: true });
- process.env = originalEnv;
- });
- test('basic substitution: {env:FOO} resolves to the value of FOO', () => {
- process.env.FOO = 'foo-model';
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: '{env:FOO}' } },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('foo-model');
- });
- test('multiple variables: multiple {env:...} tokens resolve', () => {
- process.env.FOO = 'foo-model';
- process.env.BAR = 'bar-model';
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: { model: '{env:FOO}' },
- explorer: { model: '{env:BAR}' },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('foo-model');
- expect(config.agents?.explorer?.model).toBe('bar-model');
- });
- test('undefined variable: {env:NONEXISTENT} resolves to empty string', () => {
- delete process.env.NONEXISTENT;
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: '{env:NONEXISTENT}' } },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('');
- });
- test('no interpolation needed: config without {env:...} works unchanged', () => {
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: 'foo-model' } },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('foo-model');
- });
- test('partial string: "prefix-{env:FOO}-suffix" resolves correctly', () => {
- process.env.FOO = 'foo';
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: { oracle: { model: 'prefix-{env:FOO}-suffix' } },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('prefix-foo-suffix');
- });
- test('works in JSONC files with comments', () => {
- process.env.FOO = 'foo-model';
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.jsonc'),
- `{
- // Use env variable for model
- "agents": { "oracle": { "model": "{env:FOO}" } }
- }`,
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('foo-model');
- });
- test('multiple env vars in a single value', () => {
- process.env.FOO = 'foo';
- process.env.BAR = 'bar';
- const projectDir = path.join(tempDir, 'project');
- const projectConfigDir = path.join(projectDir, '.opencode');
- fs.mkdirSync(projectConfigDir, { recursive: true });
- fs.writeFileSync(
- path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
- JSON.stringify({
- agents: {
- oracle: { model: '{env:FOO}/{env:BAR}' },
- },
- }),
- );
- const config = loadPluginConfig(projectDir);
- expect(config.agents?.oracle?.model).toBe('foo/bar');
- });
- });
|