Skip to content

Commit 32c52a1

Browse files
committed
test: add tests for config schema, cost utilities, and embeddings detector
1 parent a15681d commit 32c52a1

File tree

3 files changed

+529
-3
lines changed

3 files changed

+529
-3
lines changed

tests/config.test.ts

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
parseConfig,
4+
getDefaultConfig,
5+
getDefaultModelForProvider,
6+
EMBEDDING_MODELS,
7+
} from "../src/config/schema.js";
8+
9+
describe("config schema", () => {
10+
describe("parseConfig", () => {
11+
it("should return defaults for undefined input", () => {
12+
const config = parseConfig(undefined);
13+
14+
expect(config.embeddingProvider).toBe("auto");
15+
expect(config.embeddingModel).toBe("auto");
16+
expect(config.scope).toBe("project");
17+
expect(config.include).toHaveLength(10);
18+
expect(config.exclude).toHaveLength(13);
19+
});
20+
21+
it("should return defaults for null input", () => {
22+
const config = parseConfig(null);
23+
24+
expect(config.embeddingProvider).toBe("auto");
25+
expect(config.indexing.autoIndex).toBe(false);
26+
});
27+
28+
it("should return defaults for non-object input", () => {
29+
expect(parseConfig("string").embeddingProvider).toBe("auto");
30+
expect(parseConfig(123).embeddingProvider).toBe("auto");
31+
expect(parseConfig([]).embeddingProvider).toBe("auto");
32+
});
33+
34+
it("should parse valid embeddingProvider values", () => {
35+
expect(parseConfig({ embeddingProvider: "openai" }).embeddingProvider).toBe("openai");
36+
expect(parseConfig({ embeddingProvider: "google" }).embeddingProvider).toBe("google");
37+
expect(parseConfig({ embeddingProvider: "ollama" }).embeddingProvider).toBe("ollama");
38+
expect(parseConfig({ embeddingProvider: "github-copilot" }).embeddingProvider).toBe("github-copilot");
39+
expect(parseConfig({ embeddingProvider: "auto" }).embeddingProvider).toBe("auto");
40+
});
41+
42+
it("should fallback to auto for invalid embeddingProvider", () => {
43+
expect(parseConfig({ embeddingProvider: "invalid" }).embeddingProvider).toBe("auto");
44+
expect(parseConfig({ embeddingProvider: 123 }).embeddingProvider).toBe("auto");
45+
expect(parseConfig({ embeddingProvider: null }).embeddingProvider).toBe("auto");
46+
});
47+
48+
it("should parse valid scope values", () => {
49+
expect(parseConfig({ scope: "project" }).scope).toBe("project");
50+
expect(parseConfig({ scope: "global" }).scope).toBe("global");
51+
});
52+
53+
it("should fallback to project for invalid scope", () => {
54+
expect(parseConfig({ scope: "invalid" }).scope).toBe("project");
55+
expect(parseConfig({ scope: 123 }).scope).toBe("project");
56+
});
57+
58+
it("should parse embeddingModel as string", () => {
59+
expect(parseConfig({ embeddingModel: "custom-model" }).embeddingModel).toBe("custom-model");
60+
});
61+
62+
it("should fallback to auto for non-string embeddingModel", () => {
63+
expect(parseConfig({ embeddingModel: 123 }).embeddingModel).toBe("auto");
64+
expect(parseConfig({ embeddingModel: null }).embeddingModel).toBe("auto");
65+
});
66+
67+
it("should parse include as string array", () => {
68+
const config = parseConfig({ include: ["**/*.ts", "**/*.js"] });
69+
expect(config.include).toEqual(["**/*.ts", "**/*.js"]);
70+
});
71+
72+
it("should fallback to defaults for non-array include", () => {
73+
expect(parseConfig({ include: "string" }).include).toHaveLength(10);
74+
expect(parseConfig({ include: 123 }).include).toHaveLength(10);
75+
});
76+
77+
it("should fallback to defaults for include with non-string items", () => {
78+
expect(parseConfig({ include: [123, 456] }).include).toHaveLength(10);
79+
expect(parseConfig({ include: ["valid", 123] }).include).toHaveLength(10);
80+
});
81+
82+
it("should parse exclude as string array", () => {
83+
const config = parseConfig({ exclude: ["**/node_modules/**"] });
84+
expect(config.exclude).toEqual(["**/node_modules/**"]);
85+
});
86+
87+
describe("indexing config", () => {
88+
it("should parse boolean indexing options", () => {
89+
const config = parseConfig({
90+
indexing: {
91+
autoIndex: true,
92+
watchFiles: false,
93+
semanticOnly: true,
94+
autoGc: false,
95+
},
96+
});
97+
98+
expect(config.indexing.autoIndex).toBe(true);
99+
expect(config.indexing.watchFiles).toBe(false);
100+
expect(config.indexing.semanticOnly).toBe(true);
101+
expect(config.indexing.autoGc).toBe(false);
102+
});
103+
104+
it("should fallback to defaults for non-boolean indexing options", () => {
105+
const config = parseConfig({
106+
indexing: {
107+
autoIndex: "true",
108+
watchFiles: 1,
109+
},
110+
});
111+
112+
expect(config.indexing.autoIndex).toBe(false);
113+
expect(config.indexing.watchFiles).toBe(true);
114+
});
115+
116+
it("should parse numeric indexing options", () => {
117+
const config = parseConfig({
118+
indexing: {
119+
maxFileSize: 2000000,
120+
maxChunksPerFile: 50,
121+
retries: 5,
122+
retryDelayMs: 2000,
123+
gcIntervalDays: 14,
124+
gcOrphanThreshold: 200,
125+
},
126+
});
127+
128+
expect(config.indexing.maxFileSize).toBe(2000000);
129+
expect(config.indexing.maxChunksPerFile).toBe(50);
130+
expect(config.indexing.retries).toBe(5);
131+
expect(config.indexing.retryDelayMs).toBe(2000);
132+
expect(config.indexing.gcIntervalDays).toBe(14);
133+
expect(config.indexing.gcOrphanThreshold).toBe(200);
134+
});
135+
136+
it("should enforce minimum of 1 for maxChunksPerFile", () => {
137+
expect(parseConfig({ indexing: { maxChunksPerFile: 0 } }).indexing.maxChunksPerFile).toBe(1);
138+
expect(parseConfig({ indexing: { maxChunksPerFile: -5 } }).indexing.maxChunksPerFile).toBe(1);
139+
});
140+
141+
it("should enforce minimum of 1 for gcIntervalDays", () => {
142+
expect(parseConfig({ indexing: { gcIntervalDays: 0 } }).indexing.gcIntervalDays).toBe(1);
143+
expect(parseConfig({ indexing: { gcIntervalDays: -1 } }).indexing.gcIntervalDays).toBe(1);
144+
});
145+
146+
it("should enforce minimum of 0 for gcOrphanThreshold", () => {
147+
expect(parseConfig({ indexing: { gcOrphanThreshold: -10 } }).indexing.gcOrphanThreshold).toBe(0);
148+
});
149+
150+
it("should handle non-object indexing", () => {
151+
expect(parseConfig({ indexing: "invalid" }).indexing.autoIndex).toBe(false);
152+
expect(parseConfig({ indexing: null }).indexing.autoIndex).toBe(false);
153+
});
154+
});
155+
156+
describe("search config", () => {
157+
it("should parse search options", () => {
158+
const config = parseConfig({
159+
search: {
160+
maxResults: 50,
161+
minScore: 0.2,
162+
includeContext: false,
163+
hybridWeight: 0.7,
164+
contextLines: 10,
165+
},
166+
});
167+
168+
expect(config.search.maxResults).toBe(50);
169+
expect(config.search.minScore).toBe(0.2);
170+
expect(config.search.includeContext).toBe(false);
171+
expect(config.search.hybridWeight).toBe(0.7);
172+
expect(config.search.contextLines).toBe(10);
173+
});
174+
175+
it("should clamp hybridWeight to 0-1 range", () => {
176+
expect(parseConfig({ search: { hybridWeight: -0.5 } }).search.hybridWeight).toBe(0);
177+
expect(parseConfig({ search: { hybridWeight: 1.5 } }).search.hybridWeight).toBe(1);
178+
expect(parseConfig({ search: { hybridWeight: 0.5 } }).search.hybridWeight).toBe(0.5);
179+
});
180+
181+
it("should clamp contextLines to 0-50 range", () => {
182+
expect(parseConfig({ search: { contextLines: -5 } }).search.contextLines).toBe(0);
183+
expect(parseConfig({ search: { contextLines: 100 } }).search.contextLines).toBe(50);
184+
expect(parseConfig({ search: { contextLines: 25 } }).search.contextLines).toBe(25);
185+
});
186+
187+
it("should handle non-object search", () => {
188+
expect(parseConfig({ search: "invalid" }).search.maxResults).toBe(20);
189+
});
190+
});
191+
});
192+
193+
describe("getDefaultConfig", () => {
194+
it("should return expected default values", () => {
195+
const config = getDefaultConfig();
196+
197+
expect(config.embeddingProvider).toBe("auto");
198+
expect(config.embeddingModel).toBe("auto");
199+
expect(config.scope).toBe("project");
200+
expect(config.include).toContain("**/*.{ts,tsx,js,jsx,mjs,cjs}");
201+
expect(config.exclude).toContain("**/node_modules/**");
202+
});
203+
});
204+
205+
describe("getDefaultModelForProvider", () => {
206+
it("should return correct model for github-copilot", () => {
207+
const model = getDefaultModelForProvider("github-copilot");
208+
expect(model.provider).toBe("github-copilot");
209+
expect(model.model).toBe("text-embedding-3-small");
210+
expect(model.dimensions).toBe(1536);
211+
});
212+
213+
it("should return correct model for openai", () => {
214+
const model = getDefaultModelForProvider("openai");
215+
expect(model.provider).toBe("openai");
216+
expect(model.model).toBe("text-embedding-3-small");
217+
});
218+
219+
it("should return correct model for google", () => {
220+
const model = getDefaultModelForProvider("google");
221+
expect(model.provider).toBe("google");
222+
expect(model.model).toBe("text-embedding-004");
223+
expect(model.dimensions).toBe(768);
224+
});
225+
226+
it("should return correct model for ollama", () => {
227+
const model = getDefaultModelForProvider("ollama");
228+
expect(model.provider).toBe("ollama");
229+
expect(model.model).toBe("nomic-embed-text");
230+
});
231+
232+
it("should return github-copilot model for auto (default case)", () => {
233+
const model = getDefaultModelForProvider("auto");
234+
expect(model.provider).toBe("github-copilot");
235+
});
236+
});
237+
238+
describe("EMBEDDING_MODELS", () => {
239+
it("should have expected models defined", () => {
240+
expect(EMBEDDING_MODELS).toHaveProperty("github-copilot/text-embedding-3-small");
241+
expect(EMBEDDING_MODELS).toHaveProperty("openai/text-embedding-3-small");
242+
expect(EMBEDDING_MODELS).toHaveProperty("openai/text-embedding-3-large");
243+
expect(EMBEDDING_MODELS).toHaveProperty("google/text-embedding-004");
244+
expect(EMBEDDING_MODELS).toHaveProperty("ollama/nomic-embed-text");
245+
expect(EMBEDDING_MODELS).toHaveProperty("ollama/mxbai-embed-large");
246+
});
247+
248+
it("should have correct cost for free providers", () => {
249+
expect(EMBEDDING_MODELS["github-copilot/text-embedding-3-small"].costPer1MTokens).toBe(0);
250+
expect(EMBEDDING_MODELS["google/text-embedding-004"].costPer1MTokens).toBe(0);
251+
expect(EMBEDDING_MODELS["ollama/nomic-embed-text"].costPer1MTokens).toBe(0);
252+
});
253+
254+
it("should have non-zero cost for paid providers", () => {
255+
expect(EMBEDDING_MODELS["openai/text-embedding-3-small"].costPer1MTokens).toBeGreaterThan(0);
256+
expect(EMBEDDING_MODELS["openai/text-embedding-3-large"].costPer1MTokens).toBeGreaterThan(0);
257+
});
258+
});
259+
});

0 commit comments

Comments
 (0)