summaryrefslogtreecommitdiffhomepage
path: root/packages/trace-replay/src/fixture.test.ts
blob: cca658511d8115e4e98f16da1cbca4d1c8ba3f78 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { loadFixture, parseFixture, saveFixture, serializeFixture } from "./fixture.js";
import type { HttpExchangeFixture } from "./types.js";

const fixture: HttpExchangeFixture = {
	request: {
		method: "POST",
		url: "https://api.example.com/v1/chat",
		headers: { "content-type": "application/json" },
		body: '{"model":"gpt-4"}',
	},
	response: {
		status: 200,
		statusText: "OK",
		headers: { "content-type": "application/json" },
		body: '{"choices":[]}',
	},
	meta: { model: "gpt-4", capturedAt: 1700000000000, label: null },
};

const minimalFixture: HttpExchangeFixture = {
	request: { method: "GET", url: "https://x", headers: {}, body: null },
	response: { status: 204, headers: {}, body: "" },
};

describe("serializeFixture", () => {
	it("produces valid JSON", () => {
		const json = serializeFixture(fixture);
		expect(() => JSON.parse(json)).not.toThrow();
	});

	it("round-trips through parseFixture", () => {
		const json = serializeFixture(fixture);
		const parsed = parseFixture(json);
		expect(parsed).toEqual(fixture);
	});

	it("round-trips minimal fixture", () => {
		const json = serializeFixture(minimalFixture);
		const parsed = parseFixture(json);
		expect(parsed).toEqual(minimalFixture);
	});

	it("produces pretty-printed JSON with trailing newline", () => {
		const json = serializeFixture(fixture);
		expect(json).toContain("\n");
		expect(json.endsWith("\n")).toBe(true);
	});
});

describe("parseFixture", () => {
	it("throws on invalid JSON", () => {
		expect(() => parseFixture("not json")).toThrow("Invalid JSON");
	});

	it("throws on non-object", () => {
		expect(() => parseFixture('"hello"')).toThrow("Fixture must be an object");
	});

	it("throws on null", () => {
		expect(() => parseFixture("null")).toThrow("Fixture must be an object");
	});

	it("throws on missing request", () => {
		expect(() => parseFixture('{"response":{"status":200,"headers":{},"body":""}}')).toThrow(
			"request",
		);
	});

	it("throws on missing response", () => {
		expect(() =>
			parseFixture('{"request":{"method":"GET","url":"x","headers":{},"body":null}}'),
		).toThrow("response");
	});

	it("throws on non-string request.method", () => {
		const bad = {
			request: { method: 123, url: "x", headers: {}, body: null },
			response: { status: 200, headers: {}, body: "" },
		};
		expect(() => parseFixture(JSON.stringify(bad))).toThrow("request.method must be a string");
	});

	it("throws on non-number response.status", () => {
		const bad = {
			request: { method: "GET", url: "x", headers: {}, body: null },
			response: { status: "200", headers: {}, body: "" },
		};
		expect(() => parseFixture(JSON.stringify(bad))).toThrow("response.status must be a number");
	});

	it("throws on non-string response.body", () => {
		const bad = {
			request: { method: "GET", url: "x", headers: {}, body: null },
			response: { status: 200, headers: {}, body: 123 },
		};
		expect(() => parseFixture(JSON.stringify(bad))).toThrow("response.body must be a string");
	});

	it("throws on non-string header value", () => {
		const bad = {
			request: { method: "GET", url: "x", headers: { bad: 123 }, body: null },
			response: { status: 200, headers: {}, body: "" },
		};
		expect(() => parseFixture(JSON.stringify(bad))).toThrow("headers.bad must be a string");
	});

	it("throws on invalid meta value", () => {
		const bad = {
			request: { method: "GET", url: "x", headers: {}, body: null },
			response: { status: 200, headers: {}, body: "" },
			meta: { bad: {} },
		};
		expect(() => parseFixture(JSON.stringify(bad))).toThrow("meta.bad must be");
	});

	it("accepts valid meta with null/string/number/boolean", () => {
		const withMeta = {
			...minimalFixture,
			meta: { a: "s", b: 1, c: true, d: null },
		};
		const parsed = parseFixture(JSON.stringify(withMeta));
		expect(parsed.meta).toEqual({ a: "s", b: 1, c: true, d: null });
	});

	it("accepts fixture without meta", () => {
		const parsed = parseFixture(JSON.stringify(minimalFixture));
		expect(parsed.meta).toBeUndefined();
	});
});

let tmpDir: string;

beforeEach(async () => {
	tmpDir = await mkdtemp(join(tmpdir(), "trace-replay-test-"));
});

afterEach(async () => {
	await rm(tmpDir, { recursive: true, force: true });
});

describe("saveFixture / loadFixture", () => {
	it("writes and reads back a fixture (real fs)", () => {
		const path = join(tmpDir, "test-fixture.json");
		saveFixture(path, fixture);
		const loaded = loadFixture(path);
		expect(loaded).toEqual(fixture);
	});

	it("writes and reads back a minimal fixture", () => {
		const path = join(tmpDir, "minimal.json");
		saveFixture(path, minimalFixture);
		const loaded = loadFixture(path);
		expect(loaded).toEqual(minimalFixture);
	});

	it("loadFixture throws on malformed file", () => {
		const path = join(tmpDir, "bad.json");
		const { writeFileSync } = require("node:fs") as typeof import("node:fs");
		writeFileSync(path, "not json", "utf-8");
		expect(() => loadFixture(path)).toThrow("Invalid JSON");
	});
});