Skip to content

Commit 44358d3

Browse files
committed
test(ai-usage): close branch-coverage gaps in the embeddings/buildAiUsage patch
adapters.ts's new embedding-error handler never exercised its non-Error-thrown branch, and buildAiUsage's model/costUsd/effort branches are structurally unreachable through its 3 real call sites (none ever pass costUsd/effort, and model is always defined) -- add a direct unit test via a new __selfHostAiInternals export, matching this repo's existing internals-export convention for testing private helpers.
1 parent 7351558 commit 44358d3

3 files changed

Lines changed: 29 additions & 1 deletion

File tree

src/selfhost/ai.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,3 +1446,5 @@ export function resolveAiReviewerPlan(
14461446
export function withAdvisoryAiEnv(env: Env, useAdvisory: boolean): Env {
14471447
return useAdvisory && env.AI_ADVISORY ? { ...env, AI: env.AI_ADVISORY } : env;
14481448
}
1449+
1450+
export const __selfHostAiInternals = { buildAiUsage };

test/unit/review-adapters.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,22 @@ describe("reviewInferenceAdapter: delegates to env.AI", () => {
221221
}>();
222222
expect(row).toMatchObject({ feature: "embeddings", model: "@cf/baai/bge-m3", status: "error", detail: "embed_provider_unreachable" });
223223
});
224+
225+
it("records a failed embedding call with the literal fallback detail when the provider throws a non-Error value", async () => {
226+
const ai = {
227+
run: vi.fn(async () => {
228+
throw "not an Error instance";
229+
}),
230+
};
231+
const env = createTestEnv({ AI: ai as unknown as Ai });
232+
const adapter = reviewInferenceAdapter(env, ai as unknown as Ai);
233+
await expect(adapter.run("@cf/baai/bge-m3", { text: ["hello"] })).rejects.toBe("not an Error instance");
234+
const row = await env.DB.prepare("select feature, model, status, detail from ai_usage_events order by rowid desc limit 1").first<{
235+
feature: string;
236+
model: string;
237+
status: string;
238+
detail: string | null;
239+
}>();
240+
expect(row).toMatchObject({ feature: "embeddings", model: "@cf/baai/bge-m3", status: "error", detail: "embedding_failed" });
241+
});
224242
});

test/unit/selfhost-ai.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "
22
import { tmpdir } from "node:os";
33
import { delimiter, join } from "node:path";
44
import { afterEach, describe, expect, it, vi } from "vitest";
5-
import { assertNoLegacySharedAiEnv, buildProvider, claudeErrorStatus, codexErrorFromStdout, createAnthropicAi, createChainAi, createClaudeCodeAi, createCodexAi, createOpenAiCompatibleAi, createSelfHostAi, extractCliText, extractCliUsage, isAiProviderHealthy, markAiProviderUnhealthyAtBoot, resetAiProviderCircuitBreakerForTest, resetAiProviderHealthForTest, resolveAiReviewerPlan, resolveClaudeCliTimeoutMs, resolveClaudeFirstOutputTimeoutMs, resolveCodexAuthPath, resolveCodexCliTimeoutMs, resolveCodexEffort, resolveCodexFirstOutputTimeoutMs, resolveEffort, resolveModel, resolveProviderNames, resolveRequiredCliProviders, resolveSubscriptionCliPath, redactSecrets, routeProviders, shouldMarkAiProviderUnhealthyAtBoot, subscriptionCliEnv, withAdvisoryAiEnv } from "../../src/selfhost/ai";
5+
import { assertNoLegacySharedAiEnv, buildProvider, claudeErrorStatus, codexErrorFromStdout, createAnthropicAi, createChainAi, createClaudeCodeAi, createCodexAi, createOpenAiCompatibleAi, createSelfHostAi, extractCliText, extractCliUsage, isAiProviderHealthy, markAiProviderUnhealthyAtBoot, resetAiProviderCircuitBreakerForTest, resetAiProviderHealthForTest, resolveAiReviewerPlan, resolveClaudeCliTimeoutMs, resolveClaudeFirstOutputTimeoutMs, resolveCodexAuthPath, resolveCodexCliTimeoutMs, resolveCodexEffort, resolveCodexFirstOutputTimeoutMs, resolveEffort, resolveModel, resolveProviderNames, resolveRequiredCliProviders, resolveSubscriptionCliPath, redactSecrets, routeProviders, shouldMarkAiProviderUnhealthyAtBoot, subscriptionCliEnv, withAdvisoryAiEnv, __selfHostAiInternals } from "../../src/selfhost/ai";
66
import { labelSelfHostReviewerModel, labelSelfHostReviewerModels } from "../../src/selfhost/ai-config";
77
import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics";
88

@@ -215,6 +215,14 @@ describe("createOpenAiCompatibleAi (#979)", () => {
215215
expect(out).toEqual({ data: [[0.1]], usage: { model: "bge-m3", inputTokens: 7, totalTokens: 7 } });
216216
});
217217

218+
it("buildAiUsage omits every undefined field and includes every defined one (direct unit test — costUsd/effort/an-undefined-model are never both exercised through the 3 real call sites)", () => {
219+
const { buildAiUsage } = __selfHostAiInternals;
220+
expect(buildAiUsage({})).toEqual({});
221+
expect(
222+
buildAiUsage({ provider: "ollama", model: "m", inputTokens: 1, outputTokens: 2, totalTokens: 3, costUsd: 0.5, effort: "medium" }),
223+
).toEqual({ provider: "ollama", model: "m", inputTokens: 1, outputTokens: 2, totalTokens: 3, costUsd: 0.5, effort: "medium" });
224+
});
225+
218226
it("throws on a non-OK embeddings response, including the response body detail (#4996: previously thrown away)", async () => {
219227
vi.stubGlobal("fetch", vi.fn(async () => new Response("bad request: input exceeds max length", { status: 400 })));
220228
await expect(createOpenAiCompatibleAi({ baseUrl: "http://x/v1" }).run("m", { text: ["a"] })).rejects.toThrow(

0 commit comments

Comments
 (0)