1import { describe, expect, it } from "vitest";
2
3import {
4 assertKeyRef,
5 DEFAULT_BUDGETS,
6 resolveAiConfig,
7 resolveKeyRef,
8 AiConfigSchema,
9 EnvDefaultsSchema,
10} from "./config.js";
11
12const fullWorkspace = {
13 enabled: true,
14 provider: "anthropic" as const,
15 chatModel: "claude-sonnet-4-20250514",
16 embedModel: "text-embedding-3-small",
17 budgets: { tokensPerMonth: 1_000_000, requestsPerMinute: 10, maxOutputTokens: 2048, dailySpendUsd: 5 },
18 policies: { redactPii: true, logPrompts: false, allowUserContentInPrompts: true },
19 fallback: { provider: "openai" as const, chatModel: "gpt-4o" },
20};
21
22describe("resolveAiConfig", () => {
23 it("uses env defaults when no config exists", () => {
24 const resolved = resolveAiConfig(null, null, {});
25 expect(resolved.provider).toBe("openai");
26 expect(resolved.chatModel).toBe("gpt-4o");
27 expect(resolved.embedModel).toBe("text-embedding-3-small");
28 expect(resolved.embedFallbackProvider).toBe("openai");
29 expect(resolved.enabled).toBe(true);
30 expect(resolved.budgets).toEqual(DEFAULT_BUDGETS);
31 });
32
33 it("workspace config overrides env per field", () => {
34 const resolved = resolveAiConfig(
35 { enabled: true, provider: "google", chatModel: "gemini-2.0-flash", embedModel: "text-embedding-004" },
36 null,
37 {},
38 );
39 expect(resolved.provider).toBe("google");
40 expect(resolved.chatModel).toBe("gemini-2.0-flash");
41 expect(resolved.embedModel).toBe("text-embedding-004");
42
43 expect(resolved.budgets).toEqual(DEFAULT_BUDGETS);
44 });
45
46 it("org policy overrides workspace (enterprise control)", () => {
47 const resolved = resolveAiConfig(
48 { ...fullWorkspace, provider: "anthropic", chatModel: "claude-sonnet-4-20250514" },
49 { provider: "openai", chatModel: "gpt-4o" },
50 {},
51 );
52
53 expect(resolved.provider).toBe("openai");
54 expect(resolved.chatModel).toBe("gpt-4o");
55
56 expect(resolved.embedModel).toBe(fullWorkspace.embedModel);
57 expect(resolved.budgets).toEqual(fullWorkspace.budgets);
58 });
59
60 it("per-field fallback: workspace sets only chatModel", () => {
61 const resolved = resolveAiConfig(
62 { enabled: true, provider: "openai", chatModel: "gpt-4o-mini", embedModel: "text-embedding-3-small" },
63 null,
64 { embedModel: "text-embedding-3-large" },
65 );
66
67 expect(resolved.chatModel).toBe("gpt-4o-mini");
68 expect(resolved.embedModel).toBe("text-embedding-3-small");
69
70 expect(resolved.provider).toBe("openai");
71 });
72
73 it("env provides a base URL for OSS", () => {
74 const resolved = resolveAiConfig(
75 { enabled: true, provider: "oss", chatModel: "llama-3.3-70b-instruct", embedModel: "nomic-embed-text" },
76 null,
77 { baseUrl: "http://ollama:11434/v1" },
78 );
79 expect(resolved.endpoint?.baseUrl).toBe("http://ollama:11434/v1");
80 });
81
82 it("disabled via org policy", () => {
83 const resolved = resolveAiConfig(fullWorkspace, { enabled: false }, {});
84 expect(resolved.enabled).toBe(false);
85 });
86
87 it("secretsRef from workspace, apiKeyRef from env", () => {
88 const r1 = resolveAiConfig({ ...fullWorkspace, secretsRef: "my-openai-key" }, null, {});
89 expect(r1.secretsRef).toBe("my-openai-key");
90 const r2 = resolveAiConfig(fullWorkspace, null, { apiKeyRef: "env:AI_API_KEY" });
91 expect(r2.secretsRef).toBe("env:AI_API_KEY");
92 });
93});
94
95describe("AiConfigSchema", () => {
96 it("rejects unknown providers", () => {
97 expect(() => AiConfigSchema.parse({ ...fullWorkspace, provider: "mistral" })).toThrow();
98 });
99
100 it("accepts a valid config", () => {
101 const parsed = AiConfigSchema.parse(fullWorkspace);
102 expect(parsed.provider).toBe("anthropic");
103 });
104});
105
106describe("EnvDefaultsSchema", () => {
107 it("defaults to openai / gpt-4o / text-embedding-3-small", () => {
108 const parsed = EnvDefaultsSchema.parse({});
109 expect(parsed.provider).toBe("openai");
110 expect(parsed.chatModel).toBe("gpt-4o");
111 expect(parsed.embedModel).toBe("text-embedding-3-small");
112 });
113});
114
115describe("key refs", () => {
116 it("rejects literal-looking keys", () => {
117 expect(() => assertKeyRef("sk-abc123")).toThrow();
118 expect(() => assertKeyRef("sk-ant-abc")).toThrow();
119 expect(() => assertKeyRef("AIzaSy")).toThrow();
120 });
121
122 it("accepts named refs and env: refs", () => {
123 expect(() => assertKeyRef("my-openai-key")).not.toThrow();
124 expect(() => assertKeyRef("env:AI_API_KEY")).not.toThrow();
125 });
126
127 it("resolves env:NAME against the environment", () => {
128 expect(resolveKeyRef("env:FOO", { FOO: "bar" })).toBe("bar");
129 expect(resolveKeyRef("env:MISSING", {})).toBeUndefined();
130
131 expect(resolveKeyRef("my-key", { "my-key": "x" })).toBeUndefined();
132 });
133});
134