test(agent-config): add regression tests for agent merge priority order
This commit is contained in:
committed by
YeonGyu-Kim
parent
1d8f8a03ca
commit
76c5356a80
@@ -14,7 +14,7 @@ import {
|
||||
discoverProjectClaudeSkills,
|
||||
discoverUserClaudeSkills,
|
||||
} from "../features/opencode-skill-loader";
|
||||
import { loadProjectAgents, loadUserAgents } from "../features/claude-code-agent-loader";
|
||||
import { loadProjectAgents, loadUserAgents, loadOpencodeGlobalAgents, loadOpencodeProjectAgents } from "../features/claude-code-agent-loader";
|
||||
import type { PluginComponents } from "./plugin-components-loader";
|
||||
import { reorderAgentsByPriority } from "./agent-priority-order";
|
||||
import { remapAgentKeysToDisplayNames } from "./agent-key-remapper";
|
||||
@@ -139,6 +139,9 @@ export async function applyAgentConfig(params: {
|
||||
disableOmoEnv,
|
||||
);
|
||||
|
||||
const opencodeGlobalAgents = loadOpencodeGlobalAgents();
|
||||
const opencodeProjectAgents = loadOpencodeProjectAgents(params.ctx.directory);
|
||||
|
||||
const disabledAgentNames = new Set(
|
||||
(migratedDisabledAgents ?? []).map(a => a.toLowerCase())
|
||||
);
|
||||
@@ -257,6 +260,14 @@ export async function applyAgentConfig(params: {
|
||||
pluginAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeGlobalAgents = filterProtectedAgentOverrides(
|
||||
opencodeGlobalAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeProjectAgents = filterProtectedAgentOverrides(
|
||||
opencodeProjectAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
|
||||
params.config.agent = {
|
||||
...agentConfig,
|
||||
@@ -265,9 +276,12 @@ export async function applyAgentConfig(params: {
|
||||
([key]) => key !== "sisyphus" && key !== "hephaestus" && key !== "atlas",
|
||||
),
|
||||
),
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
// Precedence: later entries override earlier (project > global > user > plugin)
|
||||
...filterDisabledAgents(filteredPluginAgents),
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredOpencodeGlobalAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
...filterDisabledAgents(filteredOpencodeProjectAgents),
|
||||
...filteredConfigAgents,
|
||||
build: { ...migratedBuild, mode: "subagent", hidden: true },
|
||||
...(planDemoteConfig ? { plan: planDemoteConfig } : {}),
|
||||
@@ -288,6 +302,14 @@ export async function applyAgentConfig(params: {
|
||||
pluginAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeGlobalAgents = filterProtectedAgentOverrides(
|
||||
opencodeGlobalAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
const filteredOpencodeProjectAgents = filterProtectedAgentOverrides(
|
||||
opencodeProjectAgents,
|
||||
protectedBuiltinAgentNames,
|
||||
);
|
||||
|
||||
const defaultedConfigAgents = configAgent
|
||||
? Object.fromEntries(
|
||||
@@ -302,9 +324,12 @@ export async function applyAgentConfig(params: {
|
||||
|
||||
params.config.agent = {
|
||||
...builtinAgents,
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
// Precedence: later entries override earlier (project > global > user > plugin)
|
||||
...filterDisabledAgents(filteredPluginAgents),
|
||||
...filterDisabledAgents(filteredUserAgents),
|
||||
...filterDisabledAgents(filteredOpencodeGlobalAgents),
|
||||
...filterDisabledAgents(filteredProjectAgents),
|
||||
...filterDisabledAgents(filteredOpencodeProjectAgents),
|
||||
...defaultedConfigAgents,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ beforeEach(async () => {
|
||||
|
||||
spyOn(agentLoader, "loadUserAgents" as any).mockReturnValue({})
|
||||
spyOn(agentLoader, "loadProjectAgents" as any).mockReturnValue({})
|
||||
spyOn(agentLoader, "loadOpencodeGlobalAgents" as any).mockReturnValue({})
|
||||
spyOn(agentLoader, "loadOpencodeProjectAgents" as any).mockReturnValue({})
|
||||
|
||||
spyOn(mcpLoader, "loadMcpConfigs" as any).mockResolvedValue({ servers: {} })
|
||||
setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {})
|
||||
@@ -118,6 +120,8 @@ afterEach(() => {
|
||||
;(skillLoader.discoverOpencodeProjectSkills as any)?.mockRestore?.()
|
||||
;(agentLoader.loadUserAgents as any)?.mockRestore?.()
|
||||
;(agentLoader.loadProjectAgents as any)?.mockRestore?.()
|
||||
;(agentLoader.loadOpencodeGlobalAgents as any)?.mockRestore?.()
|
||||
;(agentLoader.loadOpencodeProjectAgents as any)?.mockRestore?.()
|
||||
;(mcpLoader.loadMcpConfigs as any)?.mockRestore?.()
|
||||
setAdditionalAllowedMcpEnvVarsSpy?.mockRestore()
|
||||
;(pluginLoader.loadAllPluginComponents as any)?.mockRestore?.()
|
||||
@@ -1596,3 +1600,173 @@ describe("disable_omo_env pass-through", () => {
|
||||
expect(disableOmoEnv).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Agent merge priority — project-local overrides global", () => {
|
||||
test("project-local Claude agent overrides global Claude agent with same name", async () => {
|
||||
// #given — same agent name in both global (user) and project scopes
|
||||
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(user) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the global agent",
|
||||
},
|
||||
})
|
||||
;(agentLoader.loadProjectAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(project) project version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the project agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
const config: Record<string, unknown> = {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
agent: {},
|
||||
}
|
||||
const handler = createConfigHandler({
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginConfig,
|
||||
modelCacheState: {
|
||||
anthropicContext1MEnabled: false,
|
||||
modelContextLimitsCache: new Map(),
|
||||
},
|
||||
})
|
||||
|
||||
// #when
|
||||
await handler(config)
|
||||
|
||||
// #then — project version wins
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(project) project version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the project agent")
|
||||
})
|
||||
|
||||
test("opencode project agent overrides opencode global agent with same name", async () => {
|
||||
// #given — same agent name in opencode global vs opencode project
|
||||
;(agentLoader.loadOpencodeGlobalAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(opencode) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the opencode global agent",
|
||||
},
|
||||
})
|
||||
;(agentLoader.loadOpencodeProjectAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(opencode-project) project version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the opencode project agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
const config: Record<string, unknown> = {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
agent: {},
|
||||
}
|
||||
const handler = createConfigHandler({
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginConfig,
|
||||
modelCacheState: {
|
||||
anthropicContext1MEnabled: false,
|
||||
modelContextLimitsCache: new Map(),
|
||||
},
|
||||
})
|
||||
|
||||
// #when
|
||||
await handler(config)
|
||||
|
||||
// #then — opencode project version wins over opencode global
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(opencode-project) project version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the opencode project agent")
|
||||
})
|
||||
|
||||
test("project Claude agent overrides opencode global agent with same name", async () => {
|
||||
// #given — project-scope Claude agent vs global-scope opencode agent
|
||||
;(agentLoader.loadOpencodeGlobalAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(opencode) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the opencode global agent",
|
||||
},
|
||||
})
|
||||
;(agentLoader.loadProjectAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(project) project version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the project Claude agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
const config: Record<string, unknown> = {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
agent: {},
|
||||
}
|
||||
const handler = createConfigHandler({
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginConfig,
|
||||
modelCacheState: {
|
||||
anthropicContext1MEnabled: false,
|
||||
modelContextLimitsCache: new Map(),
|
||||
},
|
||||
})
|
||||
|
||||
// #when
|
||||
await handler(config)
|
||||
|
||||
// #then — project-scope wins over global-scope regardless of format
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(project) project version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the project Claude agent")
|
||||
})
|
||||
|
||||
test("plugin agents have lowest priority — overridden by all other sources", async () => {
|
||||
// #given — same agent in plugin, global, and project scopes
|
||||
;(pluginLoader.loadAllPluginComponents as any).mockResolvedValue({
|
||||
commands: {},
|
||||
skills: {},
|
||||
agents: {
|
||||
"my-custom-agent": {
|
||||
description: "plugin version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the plugin agent",
|
||||
},
|
||||
},
|
||||
mcpServers: {},
|
||||
hooksConfigs: [],
|
||||
plugins: [],
|
||||
errors: [],
|
||||
})
|
||||
;(agentLoader.loadUserAgents as any).mockReturnValue({
|
||||
"my-custom-agent": {
|
||||
description: "(user) global version",
|
||||
mode: "subagent",
|
||||
prompt: "I am the user agent",
|
||||
},
|
||||
})
|
||||
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
const config: Record<string, unknown> = {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
agent: {},
|
||||
}
|
||||
const handler = createConfigHandler({
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginConfig,
|
||||
modelCacheState: {
|
||||
anthropicContext1MEnabled: false,
|
||||
modelContextLimitsCache: new Map(),
|
||||
},
|
||||
})
|
||||
|
||||
// #when
|
||||
await handler(config)
|
||||
|
||||
// #then — user (global) agent overrides plugin agent
|
||||
const agentConfig = config.agent as Record<string, { description?: string; prompt?: string }>
|
||||
expect(agentConfig["my-custom-agent"]?.description).toBe("(user) global version")
|
||||
expect(agentConfig["my-custom-agent"]?.prompt).toBe("I am the user agent")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user