1import { describe, expect, it } from "vite-plus/test";
2import * as Schema from "effect/Schema";
3
4import {
5 ProviderDriverKind,
6 ProviderInstanceConfig,
7 ProviderInstanceConfigMap,
8 ProviderInstanceId,
9 ProviderInstanceRef,
10} from "./providerInstance.ts";
11
12const decodeProviderDriverKind = Schema.decodeUnknownSync(ProviderDriverKind);
13const decodeProviderInstanceId = Schema.decodeUnknownSync(ProviderInstanceId);
14const decodeProviderInstanceRef = Schema.decodeUnknownSync(ProviderInstanceRef);
15const decodeProviderInstanceConfig = Schema.decodeUnknownSync(ProviderInstanceConfig);
16const decodeProviderInstanceConfigMap = Schema.decodeUnknownSync(ProviderInstanceConfigMap);
17
18describe("provider slug validation (shared by driver + instance ids)", () => {
19 const cases = [
20 { schemaName: "ProviderInstanceId", decode: decodeProviderInstanceId },
21 { schemaName: "ProviderDriverKind", decode: decodeProviderDriverKind },
22 ] as const;
23
24 describe.each(cases)("$schemaName", ({ decode }) => {
25 it.each(["codex", "codex_personal", "codex-work", "claudeAgent", "x", "abc123", "ollama"])(
26 "accepts %s",
27 (id) => {
28 expect(decode(id)).toBe(id);
29 },
30 );
31
32 it.each([
33 ["empty string", ""],
34 ["leading digit", "1codex"],
35 ["leading dash", "-codex"],
36 ["leading underscore", "_codex"],
37 ["whitespace inside", "codex personal"],
38 ["dot inside", "codex.personal"],
39 ["slash inside", "codex/personal"],
40 ])("rejects %s", (_label, value) => {
41 expect(() => decode(value)).toThrow();
42 });
43
44 it("trims surrounding whitespace before validating", () => {
45 expect(decode(" codex_work ")).toBe("codex_work");
46 });
47
48 it("rejects ids longer than 64 characters", () => {
49 const tooLong = "a".repeat(65);
50 expect(() => decode(tooLong)).toThrow();
51 const justRight = "a".repeat(64);
52 expect(decode(justRight)).toBe(justRight);
53 });
54 });
55});
56
57describe("ProviderInstanceRef", () => {
58 it("decodes a driver ref", () => {
59 const ref = decodeProviderInstanceRef({
60 instanceId: "codex_work",
61 driver: "codex",
62 });
63 expect(ref.instanceId).toBe("codex_work");
64 expect(ref.driver).toBe("codex");
65 });
66
67 it("decodes a fork-defined driver ref without complaint", () => {
68 const ref = decodeProviderInstanceRef({
69 instanceId: "ollama_local",
70 driver: "ollama",
71 });
72 expect(ref.instanceId).toBe("ollama_local");
73 expect(ref.driver).toBe("ollama");
74 });
75
76 it("rejects refs whose driver field is not a valid slug", () => {
77 expect(() =>
78 decodeProviderInstanceRef({
79 instanceId: "codex",
80 driver: "1nope",
81 }),
82 ).toThrow();
83 });
84});
85
86describe("ProviderInstanceConfig", () => {
87 it("accepts a minimal config envelope for a driver", () => {
88 const decoded = decodeProviderInstanceConfig({ driver: "codex" });
89 expect(decoded.driver).toBe("codex");
90 expect(decoded.displayName).toBeUndefined();
91 expect(decoded.enabled).toBeUndefined();
92 expect(decoded.config).toBeUndefined();
93 });
94
95 it("preserves driver-opaque config payloads verbatim", () => {
96 const opaqueConfig = { homePath: "~/.codex_personal", binaryPath: "codex" };
97 const decoded = decodeProviderInstanceConfig({
98 driver: "codex",
99 displayName: "Codex (personal)",
100 accentColor: "#dc2626",
101 enabled: true,
102 config: opaqueConfig,
103 });
104 expect(decoded.displayName).toBe("Codex (personal)");
105 expect(decoded.accentColor).toBe("#dc2626");
106 expect(decoded.enabled).toBe(true);
107 expect(decoded.config).toEqual(opaqueConfig);
108 });
109
110 it("trims provider instance envelope fields", () => {
111 const decoded = decodeProviderInstanceConfig({
112 driver: " codex ",
113 displayName: " Codex Personal ",
114 accentColor: " #dc2626 ",
115 environment: [{ name: " OPENROUTER_API_KEY ", value: " sk-or-test " }],
116 });
117
118 expect(decoded).toMatchObject({
119 driver: "codex",
120 displayName: "Codex Personal",
121 accentColor: "#dc2626",
122 environment: [{ name: "OPENROUTER_API_KEY", value: " sk-or-test " }],
123 });
124 });
125
126 it("decodes generic environment variables on the instance envelope", () => {
127 const decoded = decodeProviderInstanceConfig({
128 driver: "claudeAgent",
129 environment: [
130 { name: "ANTHROPIC_BASE_URL", value: "https://openrouter.ai/api", sensitive: false },
131 { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true },
132 { name: "ANTHROPIC_API_KEY", value: "", sensitive: false },
133 ],
134 });
135
136 expect(decoded.environment).toEqual([
137 { name: "ANTHROPIC_BASE_URL", value: "https://openrouter.ai/api", sensitive: false },
138 { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true },
139 { name: "ANTHROPIC_API_KEY", value: "", sensitive: false },
140 ]);
141 });
142
143 it("rejects invalid environment variable names", () => {
144 expect(() =>
145 decodeProviderInstanceConfig({
146 driver: "codex",
147 environment: [{ name: "HAS-DASH", value: "x", sensitive: false }],
148 }),
149 ).toThrow();
150 });
151
152 it("decodes envelopes that name an unknown driver and preserves their config opaquely", () => {
153 const opaqueConfig = { someUnknownKnob: 42, model: "llama3" };
154 const decoded = decodeProviderInstanceConfig({
155 driver: "ollama",
156 displayName: "Ollama",
157 enabled: true,
158 config: opaqueConfig,
159 });
160 expect(decoded.driver).toBe("ollama");
161 expect(decoded.config).toEqual(opaqueConfig);
162 });
163
164 it("rejects a blank displayName (must be trimmed non-empty)", () => {
165 expect(() => decodeProviderInstanceConfig({ driver: "codex", displayName: " " })).toThrow();
166 });
167
168 it("rejects driver values that do not satisfy the slug pattern", () => {
169 expect(() => decodeProviderInstanceConfig({ driver: "" })).toThrow();
170 expect(() => decodeProviderInstanceConfig({ driver: "has spaces" })).toThrow();
171 });
172});
173
174describe("ProviderInstanceConfigMap", () => {
175 it("decodes a multi-instance map mixing first-party and fork drivers", () => {
176 const decoded = decodeProviderInstanceConfigMap({
177 codex_personal: {
178 driver: "codex",
179 displayName: "Codex (personal)",
180 config: { homePath: "~/.codex_personal" },
181 },
182 codex_work: {
183 driver: "codex",
184 config: { homePath: "~/.codex_work" },
185 },
186 claudeAgent: { driver: "claudeAgent" },
187 ollama_local: { driver: "ollama", config: { endpoint: "http://localhost:11434" } },
188 });
189 expect(new Set(Object.keys(decoded))).toEqual(
190 new Set(["claudeAgent", "codex_personal", "codex_work", "ollama_local"]),
191 );
192 expect(decoded[ProviderInstanceId.make("codex_personal")]?.driver).toBe("codex");
193 expect(decoded[ProviderInstanceId.make("codex_work")]?.config).toEqual({
194 homePath: "~/.codex_work",
195 });
196 expect(decoded[ProviderInstanceId.make("ollama_local")]?.driver).toBe("ollama");
197 });
198
199 it("rejects keys that fail the instance-id pattern", () => {
200 expect(() =>
201 decodeProviderInstanceConfigMap({
202 "1codex": { driver: "codex" },
203 }),
204 ).toThrow();
205 });
206});