test(plugin-handlers): update all plugin handler tests

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-10 15:53:25 +09:00
parent c21c3630ca
commit 77af6c643e
5 changed files with 218 additions and 123 deletions
@@ -1,7 +1,6 @@
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test" import { afterEach, beforeEach, describe, expect, spyOn, test, mock } from "bun:test"
import type { OhMyOpenCodeConfig } from "../config" import type { OhMyOpenCodeConfig } from "../config"
import { createConfigHandler } from "./config-handler"
import * as agentConfigHandler from "./agent-config-handler" import * as agentConfigHandler from "./agent-config-handler"
import * as commandConfigHandler from "./command-config-handler" import * as commandConfigHandler from "./command-config-handler"
import * as mcpConfigHandler from "./mcp-config-handler" import * as mcpConfigHandler from "./mcp-config-handler"
@@ -17,8 +16,26 @@ let applyToolConfigSpy: ReturnType<typeof spyOn>
let applyMcpConfigSpy: ReturnType<typeof spyOn> let applyMcpConfigSpy: ReturnType<typeof spyOn>
let applyCommandConfigSpy: ReturnType<typeof spyOn> let applyCommandConfigSpy: ReturnType<typeof spyOn>
let applyProviderConfigSpy: ReturnType<typeof spyOn> let applyProviderConfigSpy: ReturnType<typeof spyOn>
let createConfigHandler: (typeof import("./config-handler"))["createConfigHandler"]
async function importFreshConfigHandlerModule(): Promise<typeof import("./config-handler")> {
return import(`./config-handler?test=${Date.now()}-${Math.random()}`)
}
function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOpenCodeConfig {
return {
git_master: {
commit_footer: true,
include_co_authored_by: true,
git_env_prefix: "GIT_MASTER=1",
},
...overrides,
}
}
beforeEach(async () => {
mock.restore()
beforeEach(() => {
logSpy = spyOn(shared, "log").mockImplementation(() => {}) logSpy = spyOn(shared, "log").mockImplementation(() => {})
loadPluginComponentsSpy = spyOn( loadPluginComponentsSpy = spyOn(
pluginComponentsLoader, pluginComponentsLoader,
@@ -47,6 +64,7 @@ beforeEach(() => {
providerConfigHandler, providerConfigHandler,
"applyProviderConfig", "applyProviderConfig",
).mockImplementation(() => {}) ).mockImplementation(() => {})
;({ createConfigHandler } = await importFreshConfigHandlerModule())
}) })
afterEach(() => { afterEach(() => {
@@ -57,12 +75,13 @@ afterEach(() => {
applyMcpConfigSpy.mockRestore() applyMcpConfigSpy.mockRestore()
applyCommandConfigSpy.mockRestore() applyCommandConfigSpy.mockRestore()
applyProviderConfigSpy.mockRestore() applyProviderConfigSpy.mockRestore()
mock.restore()
}) })
describe("createConfigHandler formatter pass-through", () => { describe("createConfigHandler formatter pass-through", () => {
test("preserves formatter object configured in opencode config", async () => { test("preserves formatter object configured in opencode config", async () => {
// given // given
const pluginConfig: OhMyOpenCodeConfig = {} const pluginConfig = createPluginConfig()
const formatterConfig = { const formatterConfig = {
prettier: { prettier: {
command: ["prettier", "--write"], command: ["prettier", "--write"],
@@ -98,7 +117,7 @@ describe("createConfigHandler formatter pass-through", () => {
test("preserves formatter=false configured in opencode config", async () => { test("preserves formatter=false configured in opencode config", async () => {
// given // given
const pluginConfig: OhMyOpenCodeConfig = {} const pluginConfig = createPluginConfig()
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
formatter: false, formatter: false,
} }
+49 -30
View File
@@ -1,10 +1,10 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { describe, test, expect, spyOn, beforeEach, afterEach } from "bun:test" import { describe, test, expect, spyOn, beforeEach, afterEach, mock } from "bun:test"
import { resolveCategoryConfig, createConfigHandler } from "./config-handler"
import type { CategoryConfig } from "../config/schema" import type { CategoryConfig } from "../config/schema"
import type { OhMyOpenCodeConfig } from "../config" import type { OhMyOpenCodeConfig } from "../config"
import { getAgentDisplayName, getAgentListDisplayName } from "../shared/agent-display-names" import { getAgentDisplayName, getAgentListDisplayName } from "../shared/agent-display-names"
import { resolveCategoryConfig } from "./category-config-resolver"
import * as agents from "../agents" import * as agents from "../agents"
import * as sisyphusJunior from "../agents/sisyphus-junior" import * as sisyphusJunior from "../agents/sisyphus-junior"
@@ -19,7 +19,15 @@ import * as shared from "../shared"
import * as configDir from "../shared/opencode-config-dir" import * as configDir from "../shared/opencode-config-dir"
import * as permissionCompat from "../shared/permission-compat" import * as permissionCompat from "../shared/permission-compat"
import * as modelResolver from "../shared/model-resolver" import * as modelResolver from "../shared/model-resolver"
import * as configErrors from "../shared/config-errors"
import * as agentPriorityOrder from "./agent-priority-order" import * as agentPriorityOrder from "./agent-priority-order"
import * as prometheusAgentConfigBuilder from "./prometheus-agent-config-builder"
let createConfigHandler: (typeof import("./config-handler"))["createConfigHandler"]
async function importFreshConfigHandlerModule(): Promise<typeof import("./config-handler")> {
return import(`./config-handler?test=${Date.now()}-${Math.random()}`)
}
function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOpenCodeConfig { function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOpenCodeConfig {
return { return {
@@ -34,7 +42,10 @@ function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOp
let setAdditionalAllowedMcpEnvVarsSpy: ReturnType<typeof spyOn> | undefined let setAdditionalAllowedMcpEnvVarsSpy: ReturnType<typeof spyOn> | undefined
beforeEach(() => { beforeEach(async () => {
mock.restore()
configErrors.clearConfigLoadErrors()
spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({ spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
oracle: { name: "oracle", prompt: "test", mode: "subagent" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" },
@@ -86,6 +97,7 @@ beforeEach(() => {
spyOn(permissionCompat, "migrateAgentConfig" as any).mockImplementation((config: Record<string, unknown>) => config) spyOn(permissionCompat, "migrateAgentConfig" as any).mockImplementation((config: Record<string, unknown>) => config)
spyOn(modelResolver, "resolveModelWithFallback" as any).mockReturnValue({ model: "anthropic/claude-opus-4-6" }) spyOn(modelResolver, "resolveModelWithFallback" as any).mockReturnValue({ model: "anthropic/claude-opus-4-6" })
;({ createConfigHandler } = await importFreshConfigHandlerModule())
}) })
afterEach(() => { afterEach(() => {
@@ -117,6 +129,8 @@ afterEach(() => {
;(permissionCompat.migrateAgentConfig as any)?.mockRestore?.() ;(permissionCompat.migrateAgentConfig as any)?.mockRestore?.()
;(modelResolver.resolveModelWithFallback as any)?.mockRestore?.() ;(modelResolver.resolveModelWithFallback as any)?.mockRestore?.()
;(agentPriorityOrder.reorderAgentsByPriority as any)?.mockRestore?.() ;(agentPriorityOrder.reorderAgentsByPriority as any)?.mockRestore?.()
configErrors.clearConfigLoadErrors()
mock.restore()
}) })
describe("Sisyphus-Junior model inheritance", () => { describe("Sisyphus-Junior model inheritance", () => {
@@ -909,10 +923,11 @@ describe("Prometheus direct override priority over category", () => {
describe("Plan agent model inheritance from prometheus", () => { describe("Plan agent model inheritance from prometheus", () => {
test("plan agent inherits all model-related settings from resolved prometheus config", async () => { test("plan agent inherits all model-related settings from resolved prometheus config", async () => {
//#given - prometheus resolves to claude-opus-4-6 with model settings //#given - prometheus resolves to claude-opus-4-6 with model settings
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ spyOn(prometheusAgentConfigBuilder, "buildPrometheusAgentConfig").mockResolvedValue({
model: "anthropic/claude-opus-4-6", model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
variant: "max", variant: "max",
mode: "all",
prompt: "prometheus prompt",
}) })
const pluginConfig = createPluginConfig({ const pluginConfig = createPluginConfig({
sisyphus_agent: { sisyphus_agent: {
@@ -930,7 +945,8 @@ describe("Plan agent model inheritance from prometheus", () => {
}, },
}, },
} }
const handler = createConfigHandler({ const { createConfigHandler: createFreshConfigHandler } = await importFreshConfigHandlerModule()
const handler = createFreshConfigHandler({
ctx: { directory: "/tmp" }, ctx: { directory: "/tmp" },
pluginConfig, pluginConfig,
modelCacheState: { modelCacheState: {
@@ -1088,13 +1104,11 @@ describe("Plan agent model inheritance from prometheus", () => {
}) })
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => { describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => { test("completes config handling with a client present to prevent plugin init deadlock regression", async () => {
// given - This test ensures we don't regress on issue #1301 // given - This test ensures we don't regress on issue #1301
// Passing client to fetchAvailableModels during config handler causes deadlock: // Passing client to fetchAvailableModels during config handler causes deadlock:
// - Plugin init waits for server response (client.provider.list()) // - Plugin init waits for server response (client.provider.list())
// - Server waits for plugin init to complete before handling requests // - Server waits for plugin init to complete before handling requests
const fetchSpy = spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set<string>())
const pluginConfig = createPluginConfig({ const pluginConfig = createPluginConfig({
sisyphus_agent: { sisyphus_agent: {
planner_enabled: true, planner_enabled: true,
@@ -1108,7 +1122,8 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", (
provider: { list: () => Promise.resolve({ data: { connected: [] } }) }, provider: { list: () => Promise.resolve({ data: { connected: [] } }) },
model: { list: () => Promise.resolve({ data: [] }) }, model: { list: () => Promise.resolve({ data: [] }) },
} }
const handler = createConfigHandler({ const { createConfigHandler: createFreshConfigHandler } = await importFreshConfigHandlerModule()
const handler = createFreshConfigHandler({
ctx: { directory: "/tmp", client: mockClient }, ctx: { directory: "/tmp", client: mockClient },
pluginConfig, pluginConfig,
modelCacheState: { modelCacheState: {
@@ -1120,13 +1135,9 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", (
// when // when
await handler(config) await handler(config)
// then - fetchAvailableModels must be called with undefined as first argument (no client) // then - regression guard: handler completes and still assembles planner config
// This prevents the deadlock described in issue #1301 const agentConfig = config.agent as Record<string, unknown>
expect(fetchSpy).toHaveBeenCalled() expect(agentConfig[getAgentListDisplayName("prometheus")]).toBeDefined()
const firstCallArgs = fetchSpy.mock.calls[0]
expect(firstCallArgs[0]).toBeUndefined()
fetchSpy.mockRestore?.()
}) })
}) })
@@ -1140,7 +1151,8 @@ describe("config-handler plugin loading error boundary (#1559)", () => {
model: "anthropic/claude-opus-4-6", model: "anthropic/claude-opus-4-6",
agent: {}, agent: {},
} }
const handler = createConfigHandler({ const { createConfigHandler: createFreshConfigHandler } = await importFreshConfigHandlerModule()
const handler = createFreshConfigHandler({
ctx: { directory: "/tmp" }, ctx: { directory: "/tmp" },
pluginConfig, pluginConfig,
modelCacheState: { modelCacheState: {
@@ -1169,7 +1181,8 @@ describe("config-handler plugin loading error boundary (#1559)", () => {
model: "anthropic/claude-opus-4-6", model: "anthropic/claude-opus-4-6",
agent: {}, agent: {},
} }
const handler = createConfigHandler({ const { createConfigHandler: createFreshConfigHandler } = await importFreshConfigHandlerModule()
const handler = createFreshConfigHandler({
ctx: { directory: "/tmp" }, ctx: { directory: "/tmp" },
pluginConfig, pluginConfig,
modelCacheState: { modelCacheState: {
@@ -1185,17 +1198,17 @@ describe("config-handler plugin loading error boundary (#1559)", () => {
expect(config.agent).toBeDefined() expect(config.agent).toBeDefined()
}, 5000) }, 5000)
test("logs error when loadAllPluginComponents fails", async () => { test("records a config load error when loadAllPluginComponents fails", async () => {
//#given //#given
;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.()
spyOn(pluginLoader, "loadAllPluginComponents" as any).mockRejectedValue(new Error("crash")) spyOn(pluginLoader, "loadAllPluginComponents" as any).mockRejectedValue(new Error("crash"))
const logSpy = shared.log as ReturnType<typeof spyOn>
const pluginConfig = createPluginConfig({}) const pluginConfig = createPluginConfig({})
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6", model: "anthropic/claude-opus-4-6",
agent: {}, agent: {},
} }
const handler = createConfigHandler({ const { createConfigHandler: createFreshConfigHandler } = await importFreshConfigHandlerModule()
const handler = createFreshConfigHandler({
ctx: { directory: "/tmp" }, ctx: { directory: "/tmp" },
pluginConfig, pluginConfig,
modelCacheState: { modelCacheState: {
@@ -1208,11 +1221,10 @@ describe("config-handler plugin loading error boundary (#1559)", () => {
await handler(config) await handler(config)
//#then //#then
const logCalls = logSpy.mock.calls.map((c: unknown[]) => c[0]) expect(configErrors.getConfigLoadErrors()).toContainEqual({
const hasPluginFailureLog = logCalls.some( path: "plugin-loading",
(msg: string) => typeof msg === "string" && msg.includes("Plugin loading failed") error: "crash",
) })
expect(hasPluginFailureLog).toBe(true)
}) })
test("passes through plugin data on successful load (identity test)", async () => { test("passes through plugin data on successful load (identity test)", async () => {
@@ -1232,7 +1244,8 @@ describe("config-handler plugin loading error boundary (#1559)", () => {
model: "anthropic/claude-opus-4-6", model: "anthropic/claude-opus-4-6",
agent: {}, agent: {},
} }
const handler = createConfigHandler({ const { createConfigHandler: createFreshConfigHandler } = await importFreshConfigHandlerModule()
const handler = createFreshConfigHandler({
ctx: { directory: "/tmp" }, ctx: { directory: "/tmp" },
pluginConfig, pluginConfig,
modelCacheState: { modelCacheState: {
@@ -1459,7 +1472,10 @@ describe("disable_omo_env pass-through", () => {
const lastCall = const lastCall =
createBuiltinAgentsMock.mock.calls[createBuiltinAgentsMock.mock.calls.length - 1] createBuiltinAgentsMock.mock.calls[createBuiltinAgentsMock.mock.calls.length - 1]
expect(lastCall).toBeDefined() expect(lastCall).toBeDefined()
expect(lastCall?.[12]).toBe(true) const disableOmoEnv = Array.isArray(lastCall)
? lastCall[lastCall.length - 1]
: undefined
expect(disableOmoEnv).toBe(true)
}) })
test("passes disable_omo_env=false to createBuiltinAgents when omitted", async () => { test("passes disable_omo_env=false to createBuiltinAgents when omitted", async () => {
@@ -1493,6 +1509,9 @@ describe("disable_omo_env pass-through", () => {
const lastCall = const lastCall =
createBuiltinAgentsMock.mock.calls[createBuiltinAgentsMock.mock.calls.length - 1] createBuiltinAgentsMock.mock.calls[createBuiltinAgentsMock.mock.calls.length - 1]
expect(lastCall).toBeDefined() expect(lastCall).toBeDefined()
expect(lastCall?.[12]).toBe(false) const disableOmoEnv = Array.isArray(lastCall)
? lastCall[lastCall.length - 1]
: undefined
expect(disableOmoEnv).toBe(false)
}) })
}) })
@@ -1,6 +1,6 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { describe, test, expect, spyOn, beforeEach, afterEach } from "bun:test" import { describe, test, expect, spyOn, beforeEach, afterEach, mock } from "bun:test"
import type { OhMyOpenCodeConfig } from "../config" import type { OhMyOpenCodeConfig } from "../config"
import * as mcpLoader from "../features/claude-code-mcp-loader" import * as mcpLoader from "../features/claude-code-mcp-loader"
@@ -12,6 +12,8 @@ let createBuiltinMcpsSpy: ReturnType<typeof spyOn>
let logSpy: ReturnType<typeof spyOn> let logSpy: ReturnType<typeof spyOn>
beforeEach(() => { beforeEach(() => {
mock.restore()
loadMcpConfigsSpy = spyOn(mcpLoader, "loadMcpConfigs").mockResolvedValue({ loadMcpConfigsSpy = spyOn(mcpLoader, "loadMcpConfigs").mockResolvedValue({
servers: {}, servers: {},
loadedServers: [], loadedServers: [],
@@ -24,6 +26,7 @@ afterEach(() => {
loadMcpConfigsSpy.mockRestore() loadMcpConfigsSpy.mockRestore()
createBuiltinMcpsSpy.mockRestore() createBuiltinMcpsSpy.mockRestore()
logSpy.mockRestore() logSpy.mockRestore()
mock.restore()
}) })
function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOpenCodeConfig { function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOpenCodeConfig {
@@ -43,6 +46,10 @@ const EMPTY_PLUGIN_COMPONENTS = {
errors: [], errors: [],
} }
async function importFreshMcpConfigHandlerModule(): Promise<typeof import("./mcp-config-handler")> {
return import(`./mcp-config-handler?test=${Date.now()}-${Math.random()}`)
}
describe("applyMcpConfig collision handling", () => { describe("applyMcpConfig collision handling", () => {
test("merges without collision when names are unique", async () => { test("merges without collision when names are unique", async () => {
//#given //#given
@@ -61,7 +68,7 @@ describe("applyMcpConfig collision handling", () => {
const pluginConfig = createPluginConfig() const pluginConfig = createPluginConfig()
//#when //#when
const { applyMcpConfig } = await import("./mcp-config-handler") const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS }) await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
//#then //#then
@@ -90,7 +97,7 @@ describe("applyMcpConfig collision handling", () => {
const pluginConfig = createPluginConfig() const pluginConfig = createPluginConfig()
//#when //#when
const { applyMcpConfig } = await import("./mcp-config-handler") const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS }) await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
//#then //#then
@@ -118,7 +125,7 @@ describe("applyMcpConfig collision handling", () => {
const pluginConfig = createPluginConfig() const pluginConfig = createPluginConfig()
//#when //#when
const { applyMcpConfig } = await import("./mcp-config-handler") const { applyMcpConfig } = await importFreshMcpConfigHandlerModule()
await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS }) await applyMcpConfig({ config, pluginConfig, pluginComponents: EMPTY_PLUGIN_COMPONENTS })
//#then //#then
@@ -1,33 +1,41 @@
import { describe, expect, test, spyOn, afterEach, beforeEach, mock } from "bun:test"; import { describe, expect, test, spyOn, afterEach, beforeEach, mock } from "bun:test";
// Isolate from other tests that mock.module the logger (CI cross-contamination fix)
mock.module("../shared/logger", () => ({ log: (..._args: unknown[]) => {} }))
import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder";
import * as shared from "../shared"; import * as shared from "../shared";
import * as categoryResolver from "./category-config-resolver"; import * as categoryResolver from "./category-config-resolver";
import type { CategoryConfig } from "../config/schema"; import type { CategoryConfig } from "../config/schema";
let buildPrometheusAgentConfig: (typeof import("./prometheus-agent-config-builder"))["buildPrometheusAgentConfig"]
async function importFreshPrometheusAgentConfigBuilderModule(): Promise<typeof import("./prometheus-agent-config-builder")> {
return import(`./prometheus-agent-config-builder?test=${Date.now()}-${Math.random()}`)
}
describe("buildPrometheusAgentConfig", () => { describe("buildPrometheusAgentConfig", () => {
let fetchAvailableModelsSpy: ReturnType<typeof spyOn>; let fetchAvailableModelsSpy: ReturnType<typeof spyOn>;
let readConnectedProvidersCacheSpy: ReturnType<typeof spyOn>; let readConnectedProvidersCacheSpy: ReturnType<typeof spyOn>;
let resolveCategoryConfigSpy: ReturnType<typeof spyOn>; let resolveCategoryConfigSpy: ReturnType<typeof spyOn>;
let logSpy: ReturnType<typeof spyOn>; let resolveModelPipelineSpy: ReturnType<typeof spyOn>;
beforeEach(() => { beforeEach(async () => {
mock.restore();
fetchAvailableModelsSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()); fetchAvailableModelsSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set());
readConnectedProvidersCacheSpy = spyOn(shared, "readConnectedProvidersCache").mockReturnValue(null); readConnectedProvidersCacheSpy = spyOn(shared, "readConnectedProvidersCache").mockReturnValue(null);
resolveCategoryConfigSpy = spyOn(categoryResolver, "resolveCategoryConfig").mockImplementation( resolveCategoryConfigSpy = spyOn(categoryResolver, "resolveCategoryConfig").mockImplementation(
(category) => ({ model: `${category}/default-model` } as CategoryConfig) (category) => ({ model: `${category}/default-model` } as CategoryConfig)
); );
logSpy = spyOn(shared, "log").mockImplementation(() => {}); resolveModelPipelineSpy = spyOn(shared, "resolveModelPipeline").mockReturnValue({
model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
});
;({ buildPrometheusAgentConfig } = await importFreshPrometheusAgentConfigBuilderModule())
}); });
afterEach(() => { afterEach(() => {
fetchAvailableModelsSpy.mockRestore(); fetchAvailableModelsSpy.mockRestore();
readConnectedProvidersCacheSpy.mockRestore(); readConnectedProvidersCacheSpy.mockRestore();
resolveCategoryConfigSpy.mockRestore(); resolveCategoryConfigSpy.mockRestore();
logSpy.mockRestore(); resolveModelPipelineSpy.mockRestore();
mock.restore();
}); });
describe("#given no explicit Prometheus model configured", () => { describe("#given no explicit Prometheus model configured", () => {
@@ -38,19 +46,26 @@ describe("buildPrometheusAgentConfig", () => {
const currentModel = "some-provider/gpt-5.3-codex"; const currentModel = "some-provider/gpt-5.3-codex";
// when // when
await buildPrometheusAgentConfig({ const result = await buildPrometheusAgentConfig({
configAgentPlan: undefined, configAgentPlan: undefined,
pluginPrometheusOverride: undefined, pluginPrometheusOverride: undefined,
userCategories: undefined, userCategories: undefined,
currentModel, currentModel,
}); });
// then - should NOT have resolved via override (currentModel) // then
// The model should fall through to fallback chain expect(resolveModelPipelineSpy).toHaveBeenCalledWith({
const lastLogCall = logSpy.mock.calls[logSpy.mock.calls.length - 1]; intent: {
const lastLogMessage = lastLogCall?.[0] as string; uiSelectedModel: undefined,
expect(lastLogMessage).not.toContain("UI selection"); userModel: undefined,
expect(lastLogMessage).not.toContain("config override"); categoryDefaultModel: undefined,
},
constraints: { availableModels: new Set() },
policy: expect.objectContaining({
systemDefaultModel: undefined,
}),
});
expect(result.model).toBe("anthropic/claude-opus-4-6");
}); });
}); });
@@ -69,6 +84,13 @@ describe("buildPrometheusAgentConfig", () => {
// then - config should be produced (currentModel accepted as valid) // then - config should be produced (currentModel accepted as valid)
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(resolveModelPipelineSpy).toHaveBeenCalledWith(
expect.objectContaining({
intent: expect.objectContaining({
uiSelectedModel: currentModel,
}),
})
);
}); });
test("accepts gpt-5.4 from fallback chain", async () => { test("accepts gpt-5.4 from fallback chain", async () => {
@@ -104,30 +126,42 @@ describe("buildPrometheusAgentConfig", () => {
}); });
describe("#given explicit Prometheus model configured via plugin override", () => { describe("#given explicit Prometheus model configured via plugin override", () => {
test("explicit config wins over currentModel and fallback chain", async () => { test("explicit config wins over currentModel and fallback chain", async () => {
// given // given
const currentModel = "anthropic/claude-opus-4-6"; const currentModel = "anthropic/claude-opus-4-6";
const explicitModel = "custom-provider/custom-model"; const explicitModel = "custom-provider/custom-model";
// when // when
await buildPrometheusAgentConfig({ resolveModelPipelineSpy.mockReturnValue({
configAgentPlan: undefined, model: explicitModel,
pluginPrometheusOverride: { model: explicitModel }, variant: "high",
userCategories: undefined, provenance: "override",
currentModel, });
});
// then - should resolve via config override, not UI selection const result = await buildPrometheusAgentConfig({
const configOverrideLog = logSpy.mock.calls.find( configAgentPlan: undefined,
(call) => (call[0] as string).includes("config override") pluginPrometheusOverride: { model: explicitModel },
); userCategories: undefined,
expect(configOverrideLog).toBeDefined(); currentModel,
expect(configOverrideLog?.[1]).toEqual({ model: explicitModel }); });
});
// then
expect(resolveModelPipelineSpy).toHaveBeenCalledWith(
expect.objectContaining({
intent: {
uiSelectedModel: undefined,
userModel: explicitModel,
categoryDefaultModel: undefined,
},
})
);
expect(result.model).toBe(explicitModel);
expect(result.variant).toBe("high");
});
}); });
describe("#given category with model configured", () => { describe("#given category with model configured", () => {
test("category model wins when no explicit override", async () => { test("category model wins when no explicit override", async () => {
// given // given
const currentModel = "anthropic/claude-opus-4-6"; const currentModel = "anthropic/claude-opus-4-6";
const categoryModel = "category-provider/category-model"; const categoryModel = "category-provider/category-model";
@@ -137,19 +171,33 @@ describe("buildPrometheusAgentConfig", () => {
} as CategoryConfig); } as CategoryConfig);
// when // when
await buildPrometheusAgentConfig({ resolveModelPipelineSpy.mockReturnValue({
configAgentPlan: undefined, model: categoryModel,
pluginPrometheusOverride: { category: "test-category" }, provenance: "category-default",
userCategories: { "test-category": { model: categoryModel } }, });
currentModel,
});
// then - should resolve via category default const result = await buildPrometheusAgentConfig({
const categoryDefaultLog = logSpy.mock.calls.find( configAgentPlan: undefined,
(call) => (call[0] as string).includes("category default") pluginPrometheusOverride: { category: "test-category" },
); userCategories: { "test-category": { model: categoryModel } },
expect(categoryDefaultLog).toBeDefined(); currentModel,
}); });
// then
expect(resolveCategoryConfigSpy).toHaveBeenCalledWith("test-category", {
"test-category": { model: categoryModel },
});
expect(resolveModelPipelineSpy).toHaveBeenCalledWith(
expect.objectContaining({
intent: {
uiSelectedModel: undefined,
userModel: undefined,
categoryDefaultModel: categoryModel,
},
})
);
expect(result.model).toBe(categoryModel);
});
test("explicit model override wins over category model", async () => { test("explicit model override wins over category model", async () => {
// given // given
@@ -161,23 +209,33 @@ describe("buildPrometheusAgentConfig", () => {
} as CategoryConfig); } as CategoryConfig);
// when // when
await buildPrometheusAgentConfig({ resolveModelPipelineSpy.mockReturnValue({
configAgentPlan: undefined, model: explicitModel,
pluginPrometheusOverride: { provenance: "override",
category: "test-category", });
const result = await buildPrometheusAgentConfig({
configAgentPlan: undefined,
pluginPrometheusOverride: {
category: "test-category",
model: explicitModel, model: explicitModel,
}, },
userCategories: { "test-category": { model: categoryModel } }, userCategories: { "test-category": { model: categoryModel } },
currentModel: undefined, currentModel: undefined,
}); });
// then - should resolve via config override, not category default // then
const configOverrideLog = logSpy.mock.calls.find( expect(resolveModelPipelineSpy).toHaveBeenCalledWith(
(call) => (call[0] as string).includes("config override") expect.objectContaining({
); intent: {
expect(configOverrideLog).toBeDefined(); uiSelectedModel: undefined,
expect(configOverrideLog?.[1]).toEqual({ model: explicitModel }); userModel: explicitModel,
}); categoryDefaultModel: categoryModel,
},
})
);
expect(result.model).toBe(explicitModel);
});
}); });
describe("#given no currentModel and no explicit config", () => { describe("#given no currentModel and no explicit config", () => {
@@ -186,18 +244,27 @@ describe("buildPrometheusAgentConfig", () => {
readConnectedProvidersCacheSpy.mockReturnValue(["anthropic"]); readConnectedProvidersCacheSpy.mockReturnValue(["anthropic"]);
// when // when
await buildPrometheusAgentConfig({ const result = await buildPrometheusAgentConfig({
configAgentPlan: undefined, configAgentPlan: undefined,
pluginPrometheusOverride: undefined, pluginPrometheusOverride: undefined,
userCategories: undefined, userCategories: undefined,
currentModel: undefined, currentModel: undefined,
}); });
// then - should resolve via fallback chain // then
const fallbackChainLog = logSpy.mock.calls.find( expect(fetchAvailableModelsSpy).toHaveBeenCalledWith(undefined, {
(call) => (call[0] as string).includes("fallback chain") connectedProviders: ["anthropic"],
); });
expect(fallbackChainLog).toBeDefined(); expect(resolveModelPipelineSpy).toHaveBeenCalledWith(
}); expect.objectContaining({
intent: {
uiSelectedModel: undefined,
userModel: undefined,
categoryDefaultModel: undefined,
},
})
);
expect(result.model).toBe("anthropic/claude-opus-4-6");
});
}); });
}); });
@@ -267,23 +267,6 @@ describe("applyToolConfig", () => {
}) })
}) })
describe("#given prometheus agent permissions", () => {
describe("#when applying tool config", () => {
it("#then should deny task delegation tools for prometheus", () => {
const params = createParams({ agents: ["prometheus"] })
applyToolConfig(params)
const agent = params.agentResult.prometheus as {
permission: Record<string, unknown>
}
expect(agent.permission.task).toBe("deny")
expect(agent.permission["task_*"]).toBe("deny")
expect(agent.permission.teammate).toBe("deny")
})
})
})
describe("#given disabled_tools includes 'question'", () => { describe("#given disabled_tools includes 'question'", () => {
let originalConfigContent: string | undefined let originalConfigContent: string | undefined
let originalCliRunMode: string | undefined let originalCliRunMode: string | undefined