Files
HealthLog/src/lib/ai/__tests__/resolve-chain.test.ts
T
Marc-André BombeckandClaude Opus 4.7 3048dd6637 style(prettier): sweep formatting across the tree
Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:05:40 +02:00

172 lines
5.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {
user: { findUnique: vi.fn(), update: vi.fn() },
appSettings: { findUnique: vi.fn() },
},
}));
vi.mock("@/lib/crypto", () => ({
decrypt: vi.fn((v: string) => `decrypted:${v}`),
encrypt: vi.fn((v: string) => `encrypted:${v}`),
}));
vi.mock("@/lib/ai/codex-oauth", () => ({
refreshDeviceTokens: vi.fn(),
encryptCodexCreds: vi.fn(),
decryptCodexCreds: vi.fn(),
}));
import { resolveProviderChain } from "../provider";
import { prisma } from "@/lib/db";
import { decryptCodexCreds } from "@/lib/ai/codex-oauth";
beforeEach(() => {
vi.resetAllMocks();
});
describe("resolveProviderChain", () => {
it("returns the default chain when user.aiProviderChain is null", async () => {
vi.mocked(decryptCodexCreds).mockReturnValue({
accessToken: "tok",
refreshToken: "ref",
accountId: "acct",
expiresAt: new Date(Date.now() + 3600_000),
});
vi.mocked(prisma.user.findUnique)
// user-level config read
.mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null,
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: null,
aiProviderChain: null,
} as never)
// codex tokens read
.mockResolvedValueOnce({
codexAccessTokenEncrypted: "enc-a",
codexRefreshTokenEncrypted: "enc-r",
codexTokenExpiresAt: new Date(Date.now() + 3600_000),
codexConnectionStatus: "connected",
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: "enc-admin",
adminAiModel: "gpt-4o-mini",
adminAiBaseUrl: "https://api.openai.com/v1",
} as never);
const chain = await resolveProviderChain("user-1");
// Default chain trims out entries with no creds; codex (tokens
// present) and admin-openai (key present) survive.
const types = chain.map((c) => c.providerType);
expect(types).toContain("codex");
expect(types).toContain("admin-openai");
// No anthropic/local/openai key configured → those entries dropped.
expect(types).not.toContain("anthropic");
expect(types).not.toContain("local");
expect(types).not.toContain("openai");
});
it("respects user-defined chain priority order", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: "enc-anth",
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
// user puts openai first, anthropic second; admin disabled
aiProviderChain: [
{ providerType: "openai", priority: 1, enabled: true },
{ providerType: "anthropic", priority: 2, enabled: true },
{ providerType: "admin-openai", priority: 3, enabled: false },
],
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: "enc-admin",
adminAiModel: "gpt-4o-mini",
adminAiBaseUrl: "https://api.openai.com/v1",
} as never);
const chain = await resolveProviderChain("user-1");
const types = chain.map((c) => c.providerType);
expect(types).toEqual(["openai", "anthropic"]);
});
it("filters out chain entries the user has not credentialed", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null, // no anthropic key
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
aiProviderChain: [
{ providerType: "anthropic", priority: 1, enabled: true },
{ providerType: "openai", priority: 2, enabled: true },
],
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: null,
adminAiModel: "gpt-4o-mini",
adminAiBaseUrl: "https://api.openai.com/v1",
} as never);
const chain = await resolveProviderChain("user-1");
const types = chain.map((c) => c.providerType);
expect(types).toEqual(["openai"]);
});
it("skips disabled entries even if the credential is present", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null,
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
aiProviderChain: [
{ providerType: "openai", priority: 1, enabled: false }, // disabled
{ providerType: "admin-openai", priority: 2, enabled: true },
],
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: "enc-admin",
adminAiModel: "gpt-4o-mini",
adminAiBaseUrl: "https://api.openai.com/v1",
} as never);
const chain = await resolveProviderChain("user-1");
const types = chain.map((c) => c.providerType);
expect(types).toEqual(["admin-openai"]);
});
it("returns an empty array when nothing is configured at all", async () => {
vi.mocked(prisma.user.findUnique)
.mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null,
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: null,
aiProviderChain: null,
} as never)
.mockResolvedValueOnce({
codexAccessTokenEncrypted: null,
codexRefreshTokenEncrypted: null,
codexTokenExpiresAt: null,
codexConnectionStatus: "disconnected",
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: null,
adminAiModel: "gpt-4o-mini",
adminAiBaseUrl: "https://api.openai.com/v1",
} as never);
const chain = await resolveProviderChain("user-1");
expect(chain).toEqual([]);
});
});