2026-04-28 10:44:53 +09:00
import { afterEach , describe , expect , it , mock } from "bun:test" ;
2026-04-05 14:26:54 +09:00
import { chmodSync , existsSync , mkdtempSync , mkdirSync , readFileSync , rmSync , writeFileSync } from "node:fs"
2026-04-03 17:45:02 +09:00
import { tmpdir } from "node:os"
import { join } from "node:path"
2026-04-10 15:53:25 +09:00
import { mergeConfigs , parseConfigPartially } from "./plugin-config" ;
2026-04-28 10:44:53 +09:00
import { OhMyOpenCodeConfigSchema , type OhMyOpenCodeConfig , type TeamModeConfig } from "./config" ;
2026-01-13 22:48:13 +09:00
2026-04-03 17:45:02 +09:00
const tempDirs : string [ ] = [ ]
2026-04-28 10:44:53 +09:00
type ConfigInput = Omit < Partial < OhMyOpenCodeConfig > , "team_mode" > & {
team_mode? : Partial < TeamModeConfig >
}
2026-04-03 17:45:02 +09:00
2026-04-28 10:44:53 +09:00
function createConfig ( config : ConfigInput ) : OhMyOpenCodeConfig {
2026-04-04 14:33:52 +09:00
return OhMyOpenCodeConfigSchema . parse ( config )
}
2026-04-10 15:53:25 +09:00
async function importFreshPluginConfigModule ( ) : Promise < typeof import ( "./plugin-config" ) > {
return import ( ` ./plugin-config?test= ${ Date . now ( ) } - ${ Math . random ( ) } ` )
}
2026-04-03 17:45:02 +09:00
afterEach ( ( ) = > {
mock . restore ( )
2026-04-28 10:44:53 +09:00
delete process . env . OPENCODE_CONFIG_DIR
2026-04-03 17:45:02 +09:00
for ( const dir of tempDirs . splice ( 0 ) ) {
rmSync ( dir , { recursive : true , force : true } )
}
} )
2026-04-28 10:44:53 +09:00
function createLoadPluginConfigTestContext ( prefix : string ) : {
rootDir : string
userConfigDir : string
projectDir : string
projectConfigDir : string
} {
const rootDir = mkdtempSync ( join ( tmpdir ( ) , prefix ) )
const userConfigDir = join ( rootDir , "user-config" )
const projectDir = join ( rootDir , "project" )
const projectConfigDir = join ( projectDir , ".opencode" )
tempDirs . push ( rootDir )
mkdirSync ( userConfigDir , { recursive : true } )
mkdirSync ( projectConfigDir , { recursive : true } )
return { rootDir , userConfigDir , projectDir , projectConfigDir }
}
function writeJsonFile ( filePath : string , value : Record < string , unknown > ) : void {
writeFileSync ( filePath , JSON . stringify ( value ) )
}
2026-01-13 22:48:13 +09:00
describe ( "mergeConfigs" , ( ) = > {
describe ( "categories merging" , ( ) = > {
2026-02-01 16:47:50 +09:00
// given base config has categories, override has different categories
// when merging configs
// then should deep merge categories, not override completely
2026-01-13 22:48:13 +09:00
it ( "should deep merge categories from base and override" , ( ) = > {
2026-04-04 14:33:52 +09:00
const base = createConfig ( {
2026-01-13 22:48:13 +09:00
categories : {
general : {
2026-03-07 02:33:32 +09:00
model : "openai/gpt-5.4" ,
2026-01-13 22:48:13 +09:00
temperature : 0.5 ,
} ,
quick : {
model : "anthropic/claude-haiku-4-5" ,
} ,
} ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
2026-04-04 14:33:52 +09:00
const override = createConfig ( {
2026-01-13 22:48:13 +09:00
categories : {
general : {
temperature : 0.3 ,
} ,
visual : {
2026-02-26 21:01:05 +09:00
model : "google/gemini-3.1-pro" ,
2026-01-13 22:48:13 +09:00
} ,
} ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
const result = mergeConfigs ( base , override ) ;
2026-02-01 16:47:50 +09:00
// then general.model should be preserved from base
2026-03-07 02:33:32 +09:00
expect ( result . categories ? . general ? . model ) . toBe ( "openai/gpt-5.4" ) ;
2026-02-01 16:47:50 +09:00
// then general.temperature should be overridden
2026-01-13 22:48:13 +09:00
expect ( result . categories ? . general ? . temperature ) . toBe ( 0.3 ) ;
2026-02-01 16:47:50 +09:00
// then quick should be preserved from base
2026-01-13 22:48:13 +09:00
expect ( result . categories ? . quick ? . model ) . toBe ( "anthropic/claude-haiku-4-5" ) ;
2026-02-01 16:47:50 +09:00
// then visual should be added from override
2026-02-26 21:01:05 +09:00
expect ( result . categories ? . visual ? . model ) . toBe ( "google/gemini-3.1-pro" ) ;
2026-01-13 22:48:13 +09:00
} ) ;
it ( "should preserve base categories when override has no categories" , ( ) = > {
2026-04-04 14:33:52 +09:00
const base = createConfig ( {
2026-01-13 22:48:13 +09:00
categories : {
general : {
2026-03-07 02:33:32 +09:00
model : "openai/gpt-5.4" ,
2026-01-13 22:48:13 +09:00
} ,
} ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
2026-04-04 14:33:52 +09:00
const override = createConfig ( { } ) ;
2026-01-13 22:48:13 +09:00
const result = mergeConfigs ( base , override ) ;
2026-03-07 02:33:32 +09:00
expect ( result . categories ? . general ? . model ) . toBe ( "openai/gpt-5.4" ) ;
2026-01-13 22:48:13 +09:00
} ) ;
it ( "should use override categories when base has no categories" , ( ) = > {
2026-04-04 14:33:52 +09:00
const base = createConfig ( { } ) ;
2026-01-13 22:48:13 +09:00
2026-04-04 14:33:52 +09:00
const override = createConfig ( {
2026-01-13 22:48:13 +09:00
categories : {
general : {
2026-03-07 02:33:32 +09:00
model : "openai/gpt-5.4" ,
2026-01-13 22:48:13 +09:00
} ,
} ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
const result = mergeConfigs ( base , override ) ;
2026-03-07 02:33:32 +09:00
expect ( result . categories ? . general ? . model ) . toBe ( "openai/gpt-5.4" ) ;
2026-01-13 22:48:13 +09:00
} ) ;
} ) ;
describe ( "existing behavior preservation" , ( ) = > {
it ( "should deep merge agents" , ( ) = > {
2026-04-04 14:33:52 +09:00
const base = createConfig ( {
2026-01-13 22:48:13 +09:00
agents : {
2026-04-24 16:17:44 +09:00
oracle : { model : "openai/gpt-5.5" } ,
2026-01-13 22:48:13 +09:00
} ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
2026-04-04 14:33:52 +09:00
const override = createConfig ( {
2026-01-13 22:48:13 +09:00
agents : {
oracle : { temperature : 0.5 } ,
explore : { model : "anthropic/claude-haiku-4-5" } ,
} ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
const result = mergeConfigs ( base , override ) ;
2026-04-24 16:17:44 +09:00
expect ( result . agents ? . oracle ) . toMatchObject ( { model : "openai/gpt-5.5" } ) ;
2026-01-13 22:48:13 +09:00
expect ( result . agents ? . oracle ? . temperature ) . toBe ( 0.5 ) ;
2026-03-14 13:46:56 +09:00
expect ( result . agents ? . explore ) . toMatchObject ( { model : "anthropic/claude-haiku-4-5" } ) ;
2026-01-13 22:48:13 +09:00
} ) ;
2026-04-28 10:44:53 +09:00
it ( "should deep merge team_mode" , ( ) = > {
const base = createConfig ( {
team_mode : {
enabled : false ,
tmux_visualization : false ,
max_parallel_members : 2 ,
} ,
} ) ;
const override = {
team_mode : {
enabled : true ,
} ,
} as OhMyOpenCodeConfig ;
const result = mergeConfigs ( base , override ) ;
expect ( result . team_mode ) . toMatchObject ( {
enabled : true ,
max_parallel_members : 2 ,
} ) ;
} ) ;
2026-01-13 22:48:13 +09:00
it ( "should merge disabled arrays without duplicates" , ( ) = > {
2026-04-04 14:33:52 +09:00
const base = createConfig ( {
2026-01-13 22:48:13 +09:00
disabled_hooks : [ "comment-checker" , "think-mode" ] ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
2026-04-04 14:33:52 +09:00
const override = createConfig ( {
2026-01-13 22:48:13 +09:00
disabled_hooks : [ "think-mode" , "session-recovery" ] ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-01-13 22:48:13 +09:00
const result = mergeConfigs ( base , override ) ;
expect ( result . disabled_hooks ) . toContain ( "comment-checker" ) ;
expect ( result . disabled_hooks ) . toContain ( "think-mode" ) ;
expect ( result . disabled_hooks ) . toContain ( "session-recovery" ) ;
expect ( result . disabled_hooks ? . length ) . toBe ( 3 ) ;
} ) ;
2026-03-13 21:35:46 +09:00
it ( "should union disabled_tools from base and override without duplicates" , ( ) = > {
2026-04-04 14:33:52 +09:00
const base = createConfig ( {
2026-03-13 21:35:46 +09:00
disabled_tools : [ "todowrite" , "interactive_bash" ] ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-03-13 21:35:46 +09:00
2026-04-04 14:33:52 +09:00
const override = createConfig ( {
2026-03-13 21:35:46 +09:00
disabled_tools : [ "interactive_bash" , "look_at" ] ,
2026-04-04 14:33:52 +09:00
} ) ;
2026-03-13 21:35:46 +09:00
const result = mergeConfigs ( base , override ) ;
expect ( result . disabled_tools ) . toContain ( "todowrite" ) ;
expect ( result . disabled_tools ) . toContain ( "interactive_bash" ) ;
expect ( result . disabled_tools ) . toContain ( "look_at" ) ;
expect ( result . disabled_tools ? . length ) . toBe ( 3 ) ;
} ) ;
2026-01-13 22:48:13 +09:00
} ) ;
} ) ;
2026-02-12 01:33:12 +05:30
2026-04-28 10:44:53 +09:00
2026-02-12 01:33:12 +05:30
describe ( "parseConfigPartially" , ( ) = > {
2026-03-14 13:46:56 +09:00
describe ( "disabled_hooks compatibility" , ( ) = > {
//#given a config with a future hook name unknown to this version
//#when validating against the full config schema
//#then should accept the hook name so runtime and schema stay aligned
it ( "should accept unknown disabled_hooks values for forward compatibility" , ( ) = > {
const result = OhMyOpenCodeConfigSchema . safeParse ( {
disabled_hooks : [ "future-hook-name" ] ,
} ) ;
expect ( result . success ) . toBe ( true ) ;
if ( result . success ) {
expect ( result . data . disabled_hooks ) . toEqual ( [ "future-hook-name" ] ) ;
}
} ) ;
} ) ;
2026-02-12 01:33:12 +05:30
describe ( "fully valid config" , ( ) = > {
//#given a config where all sections are valid
//#when parsing the config
//#then should return the full parsed config unchanged
it ( "should return the full config when everything is valid" , ( ) = > {
const rawConfig = {
agents : {
2026-04-24 16:17:44 +09:00
oracle : { model : "openai/gpt-5.5" } ,
2026-03-07 02:33:32 +09:00
momus : { model : "openai/gpt-5.4" } ,
2026-02-12 01:33:12 +05:30
} ,
disabled_hooks : [ "comment-checker" ] ,
} ;
const result = parseConfigPartially ( rawConfig ) ;
expect ( result ) . not . toBeNull ( ) ;
2026-04-24 16:17:44 +09:00
expect ( result ! . agents ? . oracle ) . toMatchObject ( { model : "openai/gpt-5.5" } ) ;
2026-03-14 13:46:56 +09:00
expect ( result ! . agents ? . momus ) . toMatchObject ( { model : "openai/gpt-5.4" } ) ;
2026-02-12 01:33:12 +05:30
expect ( result ! . disabled_hooks ) . toEqual ( [ "comment-checker" ] ) ;
} ) ;
} ) ;
describe ( "partially invalid config" , ( ) = > {
//#given a config where one section is invalid but others are valid
//#when parsing the config
//#then should return valid sections and skip invalid ones
it ( "should preserve valid agent overrides when another section is invalid" , ( ) = > {
const rawConfig = {
agents : {
2026-04-24 16:17:44 +09:00
oracle : { model : "openai/gpt-5.5" } ,
2026-03-07 02:33:32 +09:00
momus : { model : "openai/gpt-5.4" } ,
2026-02-12 01:33:12 +05:30
prometheus : {
permission : {
edit : { "*" : "ask" , ".sisyphus/**" : "allow" } ,
} ,
} ,
} ,
disabled_hooks : [ "comment-checker" ] ,
} ;
const result = parseConfigPartially ( rawConfig ) ;
expect ( result ) . not . toBeNull ( ) ;
expect ( result ! . disabled_hooks ) . toEqual ( [ "comment-checker" ] ) ;
2026-03-02 23:55:48 +09:00
expect ( result ! . agents ) . toBeUndefined ( ) ;
2026-02-12 01:33:12 +05:30
} ) ;
it ( "should preserve valid agents when a non-agent section is invalid" , ( ) = > {
const rawConfig = {
agents : {
2026-04-24 16:17:44 +09:00
oracle : { model : "openai/gpt-5.5" } ,
2026-02-12 01:33:12 +05:30
} ,
disabled_hooks : [ "not-a-real-hook" ] ,
} ;
const result = parseConfigPartially ( rawConfig ) ;
expect ( result ) . not . toBeNull ( ) ;
2026-04-24 16:17:44 +09:00
expect ( result ! . agents ? . oracle ) . toMatchObject ( { model : "openai/gpt-5.5" } ) ;
2026-02-21 16:24:18 +09:00
expect ( result ! . disabled_hooks ) . toEqual ( [ "not-a-real-hook" ] ) ;
2026-02-12 01:33:12 +05:30
} ) ;
} ) ;
describe ( "completely invalid config" , ( ) = > {
//#given a config where all sections are invalid
//#when parsing the config
//#then should return an empty object (not null)
it ( "should return empty object when all sections are invalid" , ( ) = > {
const rawConfig = {
agents : { oracle : { temperature : "not-a-number" } } ,
disabled_hooks : [ "not-a-real-hook" ] ,
} ;
const result = parseConfigPartially ( rawConfig ) ;
expect ( result ) . not . toBeNull ( ) ;
expect ( result ! . agents ) . toBeUndefined ( ) ;
2026-02-21 16:24:18 +09:00
expect ( result ! . disabled_hooks ) . toEqual ( [ "not-a-real-hook" ] ) ;
2026-02-12 01:33:12 +05:30
} ) ;
} ) ;
describe ( "empty config" , ( ) = > {
//#given an empty config object
//#when parsing the config
//#then should return an empty object (fast path - full parse succeeds)
it ( "should return empty object for empty input" , ( ) = > {
const result = parseConfigPartially ( { } ) ;
expect ( result ) . not . toBeNull ( ) ;
2026-03-28 15:24:18 +09:00
expect ( result ) . toEqual ( {
git_master : {
commit_footer : true ,
include_co_authored_by : true ,
git_env_prefix : "GIT_MASTER=1" ,
} ,
} ) ;
2026-02-12 01:33:12 +05:30
} ) ;
} ) ;
describe ( "unknown keys" , ( ) = > {
//#given a config with keys not in the schema
//#when parsing the config
//#then should silently ignore unknown keys and preserve valid ones
it ( "should ignore unknown keys and return valid sections" , ( ) = > {
const rawConfig = {
agents : {
2026-04-24 16:17:44 +09:00
oracle : { model : "openai/gpt-5.5" } ,
2026-02-12 01:33:12 +05:30
} ,
some_future_key : { foo : "bar" } ,
} ;
const result = parseConfigPartially ( rawConfig ) ;
expect ( result ) . not . toBeNull ( ) ;
2026-04-24 16:17:44 +09:00
expect ( result ! . agents ? . oracle ) . toMatchObject ( { model : "openai/gpt-5.5" } ) ;
2026-02-12 01:33:12 +05:30
expect ( ( result as Record < string , unknown > ) [ "some_future_key" ] ) . toBeUndefined ( ) ;
} ) ;
} ) ;
} ) ;
2026-04-03 17:45:02 +09:00
describe ( "loadPluginConfig" , ( ) = > {
2026-04-10 15:53:25 +09:00
it ( "should only honor mcp_env_allowlist from user config" , async ( ) = > {
2026-04-03 17:45:02 +09:00
// given
const rootDir = mkdtempSync ( join ( tmpdir ( ) , "omo-plugin-config-" ) )
const userConfigDir = join ( rootDir , "user-config" )
const projectDir = join ( rootDir , "project" )
const projectConfigDir = join ( projectDir , ".opencode" )
tempDirs . push ( rootDir )
mkdirSync ( userConfigDir , { recursive : true } )
mkdirSync ( projectConfigDir , { recursive : true } )
writeFileSync (
join ( userConfigDir , "oh-my-openagent.jsonc" ) ,
JSON . stringify ( { mcp_env_allowlist : [ "USER_ONLY_TOKEN" ] } )
)
writeFileSync (
join ( projectConfigDir , "oh-my-openagent.jsonc" ) ,
JSON . stringify ( { mcp_env_allowlist : [ "PROJECT_TOKEN" ] } )
)
2026-04-10 15:53:25 +09:00
process . env . OPENCODE_CONFIG_DIR = userConfigDir
2026-04-03 17:45:02 +09:00
// when
2026-04-10 15:53:25 +09:00
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
2026-04-03 17:45:02 +09:00
const config = loadPluginConfig ( projectDir , { } )
// then
expect ( config . mcp_env_allowlist ) . toEqual ( [ "USER_ONLY_TOKEN" ] )
} )
2026-04-04 01:32:17 +09:00
2026-04-10 15:53:25 +09:00
it ( "should ignore edits to the renamed legacy backup after migration" , async ( ) = > {
2026-04-04 01:32:17 +09:00
// given
const rootDir = mkdtempSync ( join ( tmpdir ( ) , "omo-plugin-config-legacy-" ) )
const userConfigDir = join ( rootDir , "user-config" )
const projectDir = join ( rootDir , "project" )
const projectConfigDir = join ( projectDir , ".opencode" )
const legacyConfigPath = join ( projectConfigDir , "oh-my-opencode.jsonc" )
const backupConfigPath = ` ${ legacyConfigPath } .bak `
const canonicalConfigPath = join ( projectConfigDir , "oh-my-openagent.jsonc" )
tempDirs . push ( rootDir )
mkdirSync ( userConfigDir , { recursive : true } )
mkdirSync ( projectConfigDir , { recursive : true } )
2026-04-24 16:17:44 +09:00
writeFileSync ( legacyConfigPath , JSON . stringify ( { agents : { oracle : { model : "openai/gpt-5.5" } } } ) )
2026-04-04 01:32:17 +09:00
2026-04-10 15:53:25 +09:00
process . env . OPENCODE_CONFIG_DIR = userConfigDir
2026-04-04 01:32:17 +09:00
// when
2026-04-10 15:53:25 +09:00
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
2026-04-04 01:32:17 +09:00
loadPluginConfig ( projectDir , { } )
writeFileSync ( backupConfigPath , JSON . stringify ( { agents : { oracle : { model : "openai/gpt-5-nano" } } } ) )
const reloadedConfig = loadPluginConfig ( projectDir , { } )
// then
expect ( existsSync ( legacyConfigPath ) ) . toBe ( false )
expect ( existsSync ( backupConfigPath ) ) . toBe ( true )
2026-04-24 16:17:44 +09:00
expect ( readFileSync ( canonicalConfigPath , "utf-8" ) ) . toContain ( '"openai/gpt-5.5"' )
expect ( reloadedConfig . agents ? . oracle ? . model ) . toBe ( "openai/gpt-5.5" )
2026-04-04 01:32:17 +09:00
} )
2026-04-04 14:33:52 +09:00
2026-04-10 15:53:25 +09:00
it ( "should still load config from legacy path when migration fails" , async ( ) = > {
2026-04-05 14:26:54 +09:00
// given - legacy config exists but canonical path is not writable
const rootDir = mkdtempSync ( join ( tmpdir ( ) , "omo-plugin-config-fail-" ) )
const userConfigDir = join ( rootDir , "user-config" )
const projectDir = join ( rootDir , "project" )
const projectConfigDir = join ( projectDir , ".opencode" )
const legacyConfigPath = join ( projectConfigDir , "oh-my-opencode.json" )
tempDirs . push ( rootDir )
mkdirSync ( userConfigDir , { recursive : true } )
mkdirSync ( projectConfigDir , { recursive : true } )
2026-04-24 16:17:44 +09:00
writeFileSync ( legacyConfigPath , JSON . stringify ( { agents : { oracle : { model : "openai/gpt-5.5" } } } ) )
2026-04-05 14:26:54 +09:00
// Make the directory read-only so migration write fails
// (simulates Windows file lock / permission issues)
if ( process . platform !== "win32" ) {
chmodSync ( projectConfigDir , 0 o555 )
}
2026-04-10 15:53:25 +09:00
process . env . OPENCODE_CONFIG_DIR = userConfigDir
2026-04-05 14:26:54 +09:00
// when
let config : OhMyOpenCodeConfig
try {
2026-04-10 15:53:25 +09:00
const fresh = await importFreshPluginConfigModule ( )
config = fresh . loadPluginConfig ( projectDir , { } )
2026-04-05 14:26:54 +09:00
} finally {
// Restore permissions for cleanup
if ( process . platform !== "win32" ) {
chmodSync ( projectConfigDir , 0 o755 )
}
}
// then - should still load the config from legacy path
2026-04-24 16:17:44 +09:00
expect ( config . agents ? . oracle ? . model ) . toBe ( "openai/gpt-5.5" )
2026-04-05 14:26:54 +09:00
} )
2026-04-10 15:53:25 +09:00
it ( "should load migrated legacy project config on the first load" , async ( ) = > {
2026-04-04 14:33:52 +09:00
// given
const rootDir = mkdtempSync ( join ( tmpdir ( ) , "omo-plugin-config-first-load-" ) )
const userConfigDir = join ( rootDir , "user-config" )
const projectDir = join ( rootDir , "project" )
const projectConfigDir = join ( projectDir , ".opencode" )
const legacyConfigPath = join ( projectConfigDir , "oh-my-opencode.jsonc" )
const canonicalConfigPath = join ( projectConfigDir , "oh-my-openagent.jsonc" )
tempDirs . push ( rootDir )
mkdirSync ( userConfigDir , { recursive : true } )
mkdirSync ( projectConfigDir , { recursive : true } )
2026-04-24 16:17:44 +09:00
writeFileSync ( legacyConfigPath , JSON . stringify ( { agents : { oracle : { model : "openai/gpt-5.5" } } } ) )
2026-04-04 14:33:52 +09:00
2026-04-10 15:53:25 +09:00
process . env . OPENCODE_CONFIG_DIR = userConfigDir
2026-04-04 14:33:52 +09:00
// when
2026-04-10 15:53:25 +09:00
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
2026-04-04 14:33:52 +09:00
const config = loadPluginConfig ( projectDir , { } )
// then
expect ( existsSync ( legacyConfigPath ) ) . toBe ( false )
expect ( existsSync ( canonicalConfigPath ) ) . toBe ( true )
2026-04-24 16:17:44 +09:00
expect ( config . agents ? . oracle ? . model ) . toBe ( "openai/gpt-5.5" )
2026-04-04 14:33:52 +09:00
} )
2026-04-12 18:18:11 +09:00
it ( "should preserve explicit user git_master settings when project config omits git_master" , async ( ) = > {
// given
const rootDir = mkdtempSync ( join ( tmpdir ( ) , "omo-plugin-config-git-master-user-" ) )
const userConfigDir = join ( rootDir , "user-config" )
const projectDir = join ( rootDir , "project" )
const projectConfigDir = join ( projectDir , ".opencode" )
tempDirs . push ( rootDir )
mkdirSync ( userConfigDir , { recursive : true } )
mkdirSync ( projectConfigDir , { recursive : true } )
writeFileSync (
join ( userConfigDir , "oh-my-openagent.jsonc" ) ,
JSON . stringify ( {
git_master : {
commit_footer : false ,
include_co_authored_by : false ,
} ,
} )
)
writeFileSync (
join ( projectConfigDir , "oh-my-openagent.jsonc" ) ,
JSON . stringify ( {
agents : {
2026-04-24 16:17:44 +09:00
hephaestus : { model : "openai/gpt-5.5" } ,
2026-04-12 18:18:11 +09:00
} ,
} )
)
process . env . OPENCODE_CONFIG_DIR = userConfigDir
// when
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
const config = loadPluginConfig ( projectDir , { } )
// then
expect ( config . git_master ) . toEqual ( {
commit_footer : false ,
include_co_authored_by : false ,
git_env_prefix : "GIT_MASTER=1" ,
} )
} )
it ( "should merge explicit git_master keys from user and project configs" , async ( ) = > {
// given
const rootDir = mkdtempSync ( join ( tmpdir ( ) , "omo-plugin-config-git-master-merge-" ) )
const userConfigDir = join ( rootDir , "user-config" )
const projectDir = join ( rootDir , "project" )
const projectConfigDir = join ( projectDir , ".opencode" )
tempDirs . push ( rootDir )
mkdirSync ( userConfigDir , { recursive : true } )
mkdirSync ( projectConfigDir , { recursive : true } )
writeFileSync (
join ( userConfigDir , "oh-my-openagent.jsonc" ) ,
JSON . stringify ( {
git_master : {
commit_footer : false ,
include_co_authored_by : false ,
} ,
} )
)
writeFileSync (
join ( projectConfigDir , "oh-my-openagent.jsonc" ) ,
JSON . stringify ( {
git_master : {
commit_footer : true ,
} ,
} )
)
process . env . OPENCODE_CONFIG_DIR = userConfigDir
// when
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
const config = loadPluginConfig ( projectDir , { } )
// then
expect ( config . git_master ) . toEqual ( {
commit_footer : true ,
include_co_authored_by : false ,
git_env_prefix : "GIT_MASTER=1" ,
} )
} )
2026-04-28 10:44:53 +09:00
describe ( "team_mode.tmux_visualization" , ( ) = > {
it ( "#given canonical user config enables team_mode and legacy config also exists #when loadPluginConfig runs #then tmux_visualization remains false" , async ( ) = > {
// given
const { userConfigDir , projectDir } = createLoadPluginConfigTestContext ( "omo-plugin-config-team-mode-user-" )
writeJsonFile ( join ( userConfigDir , "oh-my-openagent.json" ) , {
team_mode : {
enabled : true ,
} ,
} )
writeJsonFile ( join ( userConfigDir , "oh-my-opencode.json" ) , {
agents : {
oracle : {
model : "openai/gpt-5.4" ,
} ,
} ,
} )
process . env . OPENCODE_CONFIG_DIR = userConfigDir
// when
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
const config = loadPluginConfig ( projectDir , { } )
// then
expect ( config . team_mode ? . enabled ) . toBe ( true )
expect ( config . team_mode ? . tmux_visualization ) . toBe ( false )
} )
it ( "#given canonical user config lacks team_mode and legacy config only enables team_mode #when loadPluginConfig runs #then canonical config wins and tmux_visualization stays effectively false" , async ( ) = > {
// given
const { userConfigDir , projectDir } = createLoadPluginConfigTestContext ( "omo-plugin-config-team-mode-legacy-" )
writeJsonFile ( join ( userConfigDir , "oh-my-openagent.json" ) , {
hashline_edit : true ,
} )
writeJsonFile ( join ( userConfigDir , "oh-my-opencode.json" ) , {
team_mode : {
enabled : true ,
} ,
} )
process . env . OPENCODE_CONFIG_DIR = userConfigDir
// when
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
const config = loadPluginConfig ( projectDir , { } )
// then
expect ( config . team_mode ) . toBeUndefined ( )
expect ( config . team_mode ? . tmux_visualization ? ? false ) . toBe ( false )
} )
it ( "#given canonical user config lacks team_mode and legacy config sets tmux_visualization=true #when loadPluginConfig runs #then legacy team_mode is not promoted into the loaded config" , async ( ) = > {
// given
const { userConfigDir , projectDir } = createLoadPluginConfigTestContext ( "omo-plugin-config-team-mode-visualization-" )
writeJsonFile ( join ( userConfigDir , "oh-my-openagent.json" ) , {
hashline_edit : true ,
} )
writeJsonFile ( join ( userConfigDir , "oh-my-opencode.json" ) , {
team_mode : {
enabled : true ,
tmux_visualization : true ,
} ,
} )
process . env . OPENCODE_CONFIG_DIR = userConfigDir
// when
const { loadPluginConfig } = await importFreshPluginConfigModule ( )
const config = loadPluginConfig ( projectDir , { } )
// then
// This proves a concurrent canonical file suppresses the legacy team_mode subtree entirely.
expect ( config . team_mode ) . toBeUndefined ( )
} )
} )
2026-04-03 17:45:02 +09:00
} )