6943b6d4d8
model-resolver.test.ts and prometheus-agent-config-builder.test.ts use
spyOn(shared, 'log') but do not own the logger module. When other test
files in the same bun test process call mock.module('../shared/logger'),
the import cache is poisoned and the spyOn targets a stale binding.
Add a lightweight mock.module call at the top of each file so the
auto-detection in run-ci-tests.ts picks them up as isolated targets.
This ensures each file gets its own module instance and the spy
captures all calls correctly.
Fixes the flaky CI failure pattern where resolveModelWithFallback and
buildPrometheusAgentConfig tests pass locally (separate bun process)
but fail in the shared CI batch.
204 lines
7.5 KiB
TypeScript
204 lines
7.5 KiB
TypeScript
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 categoryResolver from "./category-config-resolver";
|
|
import type { CategoryConfig } from "../config/schema";
|
|
|
|
describe("buildPrometheusAgentConfig", () => {
|
|
let fetchAvailableModelsSpy: ReturnType<typeof spyOn>;
|
|
let readConnectedProvidersCacheSpy: ReturnType<typeof spyOn>;
|
|
let resolveCategoryConfigSpy: ReturnType<typeof spyOn>;
|
|
let logSpy: ReturnType<typeof spyOn>;
|
|
|
|
beforeEach(() => {
|
|
fetchAvailableModelsSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set());
|
|
readConnectedProvidersCacheSpy = spyOn(shared, "readConnectedProvidersCache").mockReturnValue(null);
|
|
resolveCategoryConfigSpy = spyOn(categoryResolver, "resolveCategoryConfig").mockImplementation(
|
|
(category) => ({ model: `${category}/default-model` } as CategoryConfig)
|
|
);
|
|
logSpy = spyOn(shared, "log").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
fetchAvailableModelsSpy.mockRestore();
|
|
readConnectedProvidersCacheSpy.mockRestore();
|
|
resolveCategoryConfigSpy.mockRestore();
|
|
logSpy.mockRestore();
|
|
});
|
|
|
|
describe("#given no explicit Prometheus model configured", () => {
|
|
describe("#when currentModel is NOT in Prometheus fallback chain", () => {
|
|
test("falls through to fallback chain instead of using currentModel as override", async () => {
|
|
// given - currentModel is a model NOT in Prometheus fallback chain
|
|
// Prometheus chain: claude-opus-4-6, gpt-5.4, glm-5, gemini-3.1-pro
|
|
const currentModel = "some-provider/gpt-5.3-codex";
|
|
|
|
// when
|
|
await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: undefined,
|
|
userCategories: undefined,
|
|
currentModel,
|
|
});
|
|
|
|
// then - should NOT have resolved via override (currentModel)
|
|
// The model should fall through to fallback chain
|
|
const lastLogCall = logSpy.mock.calls[logSpy.mock.calls.length - 1];
|
|
const lastLogMessage = lastLogCall?.[0] as string;
|
|
expect(lastLogMessage).not.toContain("UI selection");
|
|
expect(lastLogMessage).not.toContain("config override");
|
|
});
|
|
});
|
|
|
|
describe("#when currentModel IS in Prometheus fallback chain", () => {
|
|
test("preserves currentModel as uiSelectedModel for claude-opus-4-6", async () => {
|
|
// given - currentModel matches a Prometheus fallback chain entry
|
|
const currentModel = "anthropic/claude-opus-4-6";
|
|
|
|
// when - should not throw and should produce a valid config
|
|
const result = await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: undefined,
|
|
userCategories: undefined,
|
|
currentModel,
|
|
});
|
|
|
|
// then - config should be produced (currentModel accepted as valid)
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
test("accepts gpt-5.4 from fallback chain", async () => {
|
|
const result = await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: undefined,
|
|
userCategories: undefined,
|
|
currentModel: "openai/gpt-5.4",
|
|
});
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
test("accepts glm-5 from fallback chain", async () => {
|
|
const result = await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: undefined,
|
|
userCategories: undefined,
|
|
currentModel: "opencode-go/glm-5",
|
|
});
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
test("accepts gemini-3.1-pro from fallback chain", async () => {
|
|
const result = await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: undefined,
|
|
userCategories: undefined,
|
|
currentModel: "google/gemini-3.1-pro",
|
|
});
|
|
expect(result).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("#given explicit Prometheus model configured via plugin override", () => {
|
|
test("explicit config wins over currentModel and fallback chain", async () => {
|
|
// given
|
|
const currentModel = "anthropic/claude-opus-4-6";
|
|
const explicitModel = "custom-provider/custom-model";
|
|
|
|
// when
|
|
await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: { model: explicitModel },
|
|
userCategories: undefined,
|
|
currentModel,
|
|
});
|
|
|
|
// then - should resolve via config override, not UI selection
|
|
const configOverrideLog = logSpy.mock.calls.find(
|
|
(call) => (call[0] as string).includes("config override")
|
|
);
|
|
expect(configOverrideLog).toBeDefined();
|
|
expect(configOverrideLog?.[1]).toEqual({ model: explicitModel });
|
|
});
|
|
});
|
|
|
|
describe("#given category with model configured", () => {
|
|
test("category model wins when no explicit override", async () => {
|
|
// given
|
|
const currentModel = "anthropic/claude-opus-4-6";
|
|
const categoryModel = "category-provider/category-model";
|
|
|
|
resolveCategoryConfigSpy.mockReturnValue({
|
|
model: categoryModel,
|
|
} as CategoryConfig);
|
|
|
|
// when
|
|
await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: { category: "test-category" },
|
|
userCategories: { "test-category": { model: categoryModel } },
|
|
currentModel,
|
|
});
|
|
|
|
// then - should resolve via category default
|
|
const categoryDefaultLog = logSpy.mock.calls.find(
|
|
(call) => (call[0] as string).includes("category default")
|
|
);
|
|
expect(categoryDefaultLog).toBeDefined();
|
|
});
|
|
|
|
test("explicit model override wins over category model", async () => {
|
|
// given
|
|
const categoryModel = "category-provider/category-model";
|
|
const explicitModel = "explicit-provider/explicit-model";
|
|
|
|
resolveCategoryConfigSpy.mockReturnValue({
|
|
model: categoryModel,
|
|
} as CategoryConfig);
|
|
|
|
// when
|
|
await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: {
|
|
category: "test-category",
|
|
model: explicitModel,
|
|
},
|
|
userCategories: { "test-category": { model: categoryModel } },
|
|
currentModel: undefined,
|
|
});
|
|
|
|
// then - should resolve via config override, not category default
|
|
const configOverrideLog = logSpy.mock.calls.find(
|
|
(call) => (call[0] as string).includes("config override")
|
|
);
|
|
expect(configOverrideLog).toBeDefined();
|
|
expect(configOverrideLog?.[1]).toEqual({ model: explicitModel });
|
|
});
|
|
});
|
|
|
|
describe("#given no currentModel and no explicit config", () => {
|
|
test("falls through to fallback chain", async () => {
|
|
// given - no currentModel, no explicit config
|
|
readConnectedProvidersCacheSpy.mockReturnValue(["anthropic"]);
|
|
|
|
// when
|
|
await buildPrometheusAgentConfig({
|
|
configAgentPlan: undefined,
|
|
pluginPrometheusOverride: undefined,
|
|
userCategories: undefined,
|
|
currentModel: undefined,
|
|
});
|
|
|
|
// then - should resolve via fallback chain
|
|
const fallbackChainLog = logSpy.mock.calls.find(
|
|
(call) => (call[0] as string).includes("fallback chain")
|
|
);
|
|
expect(fallbackChainLog).toBeDefined();
|
|
});
|
|
});
|
|
});
|