Skip to content

Commit 448304b

Browse files
committed
Add test suite
Unit tests for all modules plus integration tests covering text streaming, tool calls, multi-turn flows, error fixtures, journal verification, and fixture file loading.
1 parent 7a8c3d7 commit 448304b

8 files changed

Lines changed: 2471 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { loadFixtureFile, loadFixturesFromDir } from "../fixture-loader.js";
6+
7+
function makeTmpDir(): string {
8+
return mkdtempSync(join(tmpdir(), "fixture-loader-test-"));
9+
}
10+
11+
function writeJson(dir: string, name: string, content: unknown): string {
12+
const filePath = join(dir, name);
13+
writeFileSync(filePath, JSON.stringify(content), "utf-8");
14+
return filePath;
15+
}
16+
17+
describe("loadFixtureFile", () => {
18+
let tmpDir: string;
19+
20+
beforeEach(() => {
21+
tmpDir = makeTmpDir();
22+
});
23+
24+
afterEach(() => {
25+
rmSync(tmpDir, { recursive: true, force: true });
26+
});
27+
28+
it("loads a single fixture file with a userMessage match", () => {
29+
const filePath = writeJson(tmpDir, "greeting.json", {
30+
fixtures: [
31+
{
32+
match: { userMessage: "hello" },
33+
response: { content: "Hello!" },
34+
},
35+
],
36+
});
37+
38+
const fixtures = loadFixtureFile(filePath);
39+
expect(fixtures).toHaveLength(1);
40+
expect(fixtures[0].match.userMessage).toBe("hello");
41+
expect(fixtures[0].response).toEqual({ content: "Hello!" });
42+
});
43+
44+
it("loads all match fields correctly", () => {
45+
const filePath = writeJson(tmpDir, "full-match.json", {
46+
fixtures: [
47+
{
48+
match: { toolCallId: "call_123", toolName: "get_weather", model: "gpt-4" },
49+
response: { toolCalls: [{ name: "get_weather", arguments: "{}" }] },
50+
latency: 200,
51+
chunkSize: 10,
52+
},
53+
],
54+
});
55+
56+
const fixtures = loadFixtureFile(filePath);
57+
expect(fixtures).toHaveLength(1);
58+
const f = fixtures[0];
59+
expect(f.match.toolCallId).toBe("call_123");
60+
expect(f.match.toolName).toBe("get_weather");
61+
expect(f.match.model).toBe("gpt-4");
62+
expect(f.latency).toBe(200);
63+
expect(f.chunkSize).toBe(10);
64+
});
65+
66+
it("keeps userMessage as a string (no RegExp conversion)", () => {
67+
const filePath = writeJson(tmpDir, "string-match.json", {
68+
fixtures: [
69+
{
70+
match: { userMessage: "hello world" },
71+
response: { content: "Hi!" },
72+
},
73+
],
74+
});
75+
76+
const fixtures = loadFixtureFile(filePath);
77+
expect(typeof fixtures[0].match.userMessage).toBe("string");
78+
expect(fixtures[0].match.userMessage).toBe("hello world");
79+
});
80+
81+
it("omits latency and chunkSize when not present in JSON", () => {
82+
const filePath = writeJson(tmpDir, "no-optional.json", {
83+
fixtures: [
84+
{
85+
match: { userMessage: "hi" },
86+
response: { content: "hey" },
87+
},
88+
],
89+
});
90+
91+
const fixtures = loadFixtureFile(filePath);
92+
expect(fixtures[0].latency).toBeUndefined();
93+
expect(fixtures[0].chunkSize).toBeUndefined();
94+
});
95+
96+
it("warns and returns empty array for invalid JSON", () => {
97+
const filePath = join(tmpDir, "bad.json");
98+
writeFileSync(filePath, "{ not valid json", "utf-8");
99+
100+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
101+
const fixtures = loadFixtureFile(filePath);
102+
expect(fixtures).toHaveLength(0);
103+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Invalid JSON"), expect.anything());
104+
warn.mockRestore();
105+
});
106+
107+
it("warns and returns empty array when fixtures key is missing", () => {
108+
const filePath = writeJson(tmpDir, "no-fixtures.json", { something: [] });
109+
110+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
111+
const fixtures = loadFixtureFile(filePath);
112+
expect(fixtures).toHaveLength(0);
113+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("fixtures"));
114+
warn.mockRestore();
115+
});
116+
117+
it("warns and returns empty array when fixtures is not an array", () => {
118+
const filePath = writeJson(tmpDir, "bad-fixtures.json", { fixtures: "oops" });
119+
120+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
121+
const fixtures = loadFixtureFile(filePath);
122+
expect(fixtures).toHaveLength(0);
123+
warn.mockRestore();
124+
});
125+
126+
it("warns and returns empty array for a non-existent file", () => {
127+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
128+
const fixtures = loadFixtureFile(join(tmpDir, "does-not-exist.json"));
129+
expect(fixtures).toHaveLength(0);
130+
expect(warn).toHaveBeenCalledWith(
131+
expect.stringContaining("Could not read file"),
132+
expect.anything(),
133+
);
134+
warn.mockRestore();
135+
});
136+
});
137+
138+
describe("loadFixturesFromDir", () => {
139+
let tmpDir: string;
140+
141+
beforeEach(() => {
142+
tmpDir = makeTmpDir();
143+
});
144+
145+
afterEach(() => {
146+
rmSync(tmpDir, { recursive: true, force: true });
147+
});
148+
149+
it("loads multiple files and concatenates fixtures in alphabetical file order", () => {
150+
writeJson(tmpDir, "b-second.json", {
151+
fixtures: [{ match: { userMessage: "b" }, response: { content: "B" } }],
152+
});
153+
writeJson(tmpDir, "a-first.json", {
154+
fixtures: [{ match: { userMessage: "a" }, response: { content: "A" } }],
155+
});
156+
157+
const fixtures = loadFixturesFromDir(tmpDir);
158+
expect(fixtures).toHaveLength(2);
159+
expect(fixtures[0].match.userMessage).toBe("a");
160+
expect(fixtures[1].match.userMessage).toBe("b");
161+
});
162+
163+
it("returns all fixtures from multiple entries in one file", () => {
164+
writeJson(tmpDir, "multi.json", {
165+
fixtures: [
166+
{ match: { userMessage: "one" }, response: { content: "1" } },
167+
{ match: { userMessage: "two" }, response: { content: "2" } },
168+
{ match: { userMessage: "three" }, response: { content: "3" } },
169+
],
170+
});
171+
172+
const fixtures = loadFixturesFromDir(tmpDir);
173+
expect(fixtures).toHaveLength(3);
174+
});
175+
176+
it("skips invalid JSON files with a warning, still loads valid ones", () => {
177+
writeJson(tmpDir, "a-valid.json", {
178+
fixtures: [{ match: { userMessage: "ok" }, response: { content: "yes" } }],
179+
});
180+
writeFileSync(join(tmpDir, "b-invalid.json"), "{ bad json", "utf-8");
181+
182+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
183+
const fixtures = loadFixturesFromDir(tmpDir);
184+
expect(fixtures).toHaveLength(1);
185+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Invalid JSON"), expect.anything());
186+
warn.mockRestore();
187+
});
188+
189+
it("skips files without a fixtures array, still loads valid ones", () => {
190+
writeJson(tmpDir, "a-bad.json", { notFixtures: true });
191+
writeJson(tmpDir, "b-good.json", {
192+
fixtures: [{ match: { userMessage: "hi" }, response: { content: "hey" } }],
193+
});
194+
195+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
196+
const fixtures = loadFixturesFromDir(tmpDir);
197+
expect(fixtures).toHaveLength(1);
198+
expect(fixtures[0].match.userMessage).toBe("hi");
199+
warn.mockRestore();
200+
});
201+
202+
it("ignores non-.json files", () => {
203+
writeFileSync(join(tmpDir, "readme.txt"), "ignore me", "utf-8");
204+
writeFileSync(join(tmpDir, "notes.md"), "# ignore", "utf-8");
205+
writeJson(tmpDir, "actual.json", {
206+
fixtures: [{ match: { userMessage: "real" }, response: { content: "yes" } }],
207+
});
208+
209+
const fixtures = loadFixturesFromDir(tmpDir);
210+
expect(fixtures).toHaveLength(1);
211+
});
212+
213+
it("returns empty array for an empty directory", () => {
214+
const fixtures = loadFixturesFromDir(tmpDir);
215+
expect(fixtures).toHaveLength(0);
216+
});
217+
218+
it("returns empty array and warns for a non-existent directory", () => {
219+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
220+
const fixtures = loadFixturesFromDir(join(tmpDir, "does-not-exist"));
221+
expect(fixtures).toHaveLength(0);
222+
expect(warn).toHaveBeenCalledWith(
223+
expect.stringContaining("Could not read directory"),
224+
expect.anything(),
225+
);
226+
warn.mockRestore();
227+
});
228+
229+
it("loads fixtures from a nested subdirectory when given that path directly", () => {
230+
const subDir = join(tmpDir, "sub");
231+
mkdirSync(subDir);
232+
writeJson(subDir, "fixtures.json", {
233+
fixtures: [{ match: { userMessage: "nested" }, response: { content: "found" } }],
234+
});
235+
236+
const fixtures = loadFixturesFromDir(subDir);
237+
expect(fixtures).toHaveLength(1);
238+
expect(fixtures[0].match.userMessage).toBe("nested");
239+
});
240+
241+
it("warns and skips subdirectories, still loads sibling JSON files", () => {
242+
writeJson(tmpDir, "a-valid.json", {
243+
fixtures: [{ match: { userMessage: "top" }, response: { content: "yes" } }],
244+
});
245+
const subDir = join(tmpDir, "nested");
246+
mkdirSync(subDir);
247+
writeJson(subDir, "inner.json", {
248+
fixtures: [{ match: { userMessage: "deep" }, response: { content: "nope" } }],
249+
});
250+
251+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
252+
const fixtures = loadFixturesFromDir(tmpDir);
253+
expect(fixtures).toHaveLength(1);
254+
expect(fixtures[0].match.userMessage).toBe("top");
255+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Skipping subdirectory"));
256+
warn.mockRestore();
257+
});
258+
});

0 commit comments

Comments
 (0)