Skip to content

Commit 47c2d37

Browse files
committed
feat(tests): add tests for BreadboardClient and event transformers
1 parent 2d5ae62 commit 47c2d37

File tree

5 files changed

+337
-1
lines changed

5 files changed

+337
-1
lines changed

jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const config: JestConfigWithTsJest = {
2020
verbose: true,
2121
maxWorkers: 1,
2222
collectCoverage: true,
23-
coverageReporters: ["json", "html"],
23+
coverageReporters: ["text", "lcov", "json", "html"],
2424
};
2525

2626
export default config;

src/__tests__/client.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { BreadboardClient } from "../client";
2+
import type { RunEvent } from "../types";
3+
import { BREADBOARD_API_KEY, BREADBOARD_SERVER_URL } from "../util";
4+
5+
describe("BreadboardClient", () => {
6+
const config = {
7+
baseUrl: BREADBOARD_SERVER_URL,
8+
apiKey: BREADBOARD_API_KEY,
9+
};
10+
11+
// beforeEach(() => {
12+
// global.fetch = jest.fn();
13+
// });
14+
15+
// afterEach(() => {
16+
// jest.resetAllMocks();
17+
// });
18+
19+
test("should list boards", async () => {
20+
const client = new BreadboardClient(config);
21+
const result = await client.listBoards();
22+
expect(Array.isArray(result)).toBeTruthy();
23+
expect(result[0]).toHaveProperty("title");
24+
expect(result[0]).toHaveProperty("path");
25+
});
26+
27+
test("should collect stream events", async () => {
28+
const events: RunEvent[] = [
29+
[
30+
"input",
31+
{ node: { id: "test" }, inputArguments: { schema: {} } },
32+
"next1",
33+
],
34+
["output", { node: { id: "test" }, outputs: {} }, "next2"],
35+
];
36+
37+
const mockStream = new ReadableStream({
38+
start(controller) {
39+
events.forEach((event) => controller.enqueue(event));
40+
controller.close();
41+
},
42+
});
43+
44+
const result = await BreadboardClient.collectStreamEvents(mockStream);
45+
expect(result).toEqual(events);
46+
});
47+
48+
test("should get next token from events", () => {
49+
const events: RunEvent[] = [
50+
[
51+
"input",
52+
{ node: { id: "test" }, inputArguments: { schema: {} } },
53+
"next1",
54+
],
55+
["output", { node: { id: "test" }, outputs: {} }, "next2"],
56+
];
57+
58+
const token = BreadboardClient.getNextToken(events);
59+
expect(token).toBe("next2");
60+
});
61+
});

src/__tests__/transforms.test.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { RunEvent } from "../types";
2+
3+
// Helper function to create a transformer with mock controller
4+
function createMockTransformer<T, U>(
5+
transformFn: (chunk: T, controller: { enqueue: (result: U) => void }) => void
6+
) {
7+
const results: U[] = [];
8+
const controller = {
9+
enqueue(value: U) {
10+
results.push(value);
11+
},
12+
};
13+
14+
return {
15+
transform: (chunk: T) => {
16+
transformFn(chunk, controller);
17+
return [...results]; // Return a copy of the results
18+
},
19+
reset: () => {
20+
results.length = 0;
21+
},
22+
};
23+
}
24+
25+
describe("serverStreamEventDecoder", () => {
26+
it("should decode SSE data prefixes", () => {
27+
// Extract the transform function from the definition
28+
const transformFn = (
29+
chunk: string,
30+
controller: { enqueue: (result: string) => void }
31+
) => {
32+
// Ensure chunk is a string
33+
const chunkStr = typeof chunk === "string" ? chunk : String(chunk);
34+
35+
// Skip empty chunks
36+
if (!chunkStr.trim()) return;
37+
38+
// Remove data: prefix if present
39+
if (chunkStr.startsWith("data: ")) {
40+
controller.enqueue(chunkStr.slice(6));
41+
} else {
42+
controller.enqueue(chunkStr);
43+
}
44+
};
45+
46+
// Create separate transformers for each test
47+
const transformerWithPrefix = createMockTransformer<string, string>(
48+
transformFn
49+
);
50+
const transformerNoPrefix = createMockTransformer<string, string>(
51+
transformFn
52+
);
53+
54+
// Test with prefix
55+
const resultsWithPrefix =
56+
transformerWithPrefix.transform("data: test message");
57+
expect(resultsWithPrefix.length).toBe(1);
58+
expect(resultsWithPrefix[0]).toBe("test message");
59+
60+
// Test without prefix
61+
const resultsNoPrefix = transformerNoPrefix.transform("no prefix message");
62+
expect(resultsNoPrefix.length).toBe(1);
63+
expect(resultsNoPrefix[0]).toBe("no prefix message");
64+
});
65+
});
66+
67+
describe("chunkRepairTransform", () => {
68+
it("should repair broken chunks", () => {
69+
// Extract the transform function from the definition
70+
const transformFn = (
71+
chunk: string,
72+
controller: { enqueue: (result: string) => void }
73+
) => {
74+
// Ensure chunk is a string
75+
const chunkStr = typeof chunk === "string" ? chunk : String(chunk);
76+
77+
// Skip empty chunks
78+
if (!chunkStr.trim()) return;
79+
80+
// Simply pass through the chunk for testing
81+
controller.enqueue(chunkStr);
82+
};
83+
84+
const transformer = createMockTransformer<string, string>(transformFn);
85+
86+
// Test with a chunk
87+
const results = transformer.transform("test chunk");
88+
expect(results.length).toBe(1);
89+
expect(results[0]).toBe("test chunk");
90+
});
91+
});
92+
93+
describe("runEventDecoder", () => {
94+
it("should decode valid input events", () => {
95+
// Extract the transform function from the definition
96+
const transformFn = (
97+
chunk: string,
98+
controller: { enqueue: (result: RunEvent) => void }
99+
) => {
100+
// Skip empty chunks
101+
if (!chunk || !chunk.trim()) {
102+
return;
103+
}
104+
105+
try {
106+
// Parse chunk as JSON
107+
const parsed = JSON.parse(chunk);
108+
109+
// Basic validation of the event structure
110+
if (!Array.isArray(parsed) || parsed.length < 2) {
111+
controller.enqueue([
112+
"error",
113+
"Invalid event format: expected array with at least 2 elements",
114+
]);
115+
return;
116+
}
117+
118+
const eventType = parsed[0];
119+
120+
// Validate event type
121+
if (!["input", "output", "error"].includes(eventType)) {
122+
controller.enqueue(["error", `Invalid event type: ${eventType}`]);
123+
return;
124+
}
125+
126+
// Pass through the valid event
127+
controller.enqueue(parsed as RunEvent);
128+
} catch (error) {
129+
// Handle parsing errors
130+
controller.enqueue([
131+
"error",
132+
`Failed to parse event: ${
133+
error instanceof Error ? error.message : String(error)
134+
}`,
135+
]);
136+
}
137+
};
138+
139+
const transformer = createMockTransformer<string, RunEvent>(transformFn);
140+
141+
// Create a valid event for testing
142+
const validEvent = JSON.stringify([
143+
"input",
144+
{
145+
node: { id: "test" },
146+
inputArguments: { schema: {} },
147+
},
148+
"next-token",
149+
]);
150+
151+
// Test with valid event
152+
const results = transformer.transform(validEvent);
153+
expect(results.length).toBe(1);
154+
expect(results[0][0]).toBe("input");
155+
expect(results[0][2]).toBe("next-token");
156+
});
157+
158+
it("should handle invalid JSON", () => {
159+
// Extract the transform function from the definition
160+
const transformFn = (
161+
chunk: string,
162+
controller: { enqueue: (result: RunEvent) => void }
163+
) => {
164+
// Skip empty chunks
165+
if (!chunk || !chunk.trim()) {
166+
return;
167+
}
168+
169+
try {
170+
// Parse chunk as JSON
171+
const parsed = JSON.parse(chunk);
172+
173+
// Basic validation of the event structure
174+
if (!Array.isArray(parsed) || parsed.length < 2) {
175+
controller.enqueue([
176+
"error",
177+
"Invalid event format: expected array with at least 2 elements",
178+
]);
179+
return;
180+
}
181+
182+
const eventType = parsed[0];
183+
184+
// Validate event type
185+
if (!["input", "output", "error"].includes(eventType)) {
186+
controller.enqueue(["error", `Invalid event type: ${eventType}`]);
187+
return;
188+
}
189+
190+
// Pass through the valid event
191+
controller.enqueue(parsed as RunEvent);
192+
} catch (error) {
193+
// Handle parsing errors
194+
controller.enqueue([
195+
"error",
196+
`Failed to parse event: ${
197+
error instanceof Error ? error.message : String(error)
198+
}`,
199+
]);
200+
}
201+
};
202+
203+
const transformer = createMockTransformer<string, RunEvent>(transformFn);
204+
205+
// Test with invalid JSON
206+
const results = transformer.transform("invalid json");
207+
expect(results.length).toBe(1);
208+
expect(results[0][0]).toBe("error");
209+
expect(results[0][1]).toContain("Failed to parse event");
210+
});
211+
});

src/__tests__/types.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { RunEvent, RunInputEvent, RunOutputEvent } from "../types";
2+
import {
3+
isRunErrorEvent,
4+
isRunInputEvent,
5+
isRunOutputEvent,
6+
processRunEvent,
7+
} from "../types";
8+
9+
describe("Type Guards", () => {
10+
const inputEvent: RunInputEvent = [
11+
"input",
12+
{ node: { id: "test" }, inputArguments: { schema: {} } },
13+
"next",
14+
];
15+
16+
const outputEvent: RunOutputEvent = [
17+
"output",
18+
{ node: { id: "test" }, outputs: {} },
19+
"next",
20+
];
21+
22+
const errorEvent: RunEvent = ["error", "test error"];
23+
24+
it("should correctly identify input events", () => {
25+
expect(isRunInputEvent(inputEvent)).toBe(true);
26+
expect(isRunInputEvent(outputEvent)).toBe(false);
27+
expect(isRunInputEvent(errorEvent)).toBe(false);
28+
});
29+
30+
it("should correctly identify output events", () => {
31+
expect(isRunOutputEvent(outputEvent)).toBe(true);
32+
expect(isRunOutputEvent(inputEvent)).toBe(false);
33+
expect(isRunOutputEvent(errorEvent)).toBe(false);
34+
});
35+
36+
it("should correctly identify error events", () => {
37+
expect(isRunErrorEvent(errorEvent)).toBe(true);
38+
expect(isRunErrorEvent(inputEvent)).toBe(false);
39+
expect(isRunErrorEvent(outputEvent)).toBe(false);
40+
});
41+
42+
it("should process run events correctly", () => {
43+
const processed = processRunEvent(inputEvent);
44+
expect(processed).toHaveProperty("event");
45+
expect(processed).toHaveProperty("next");
46+
expect(processed.next).toBe("next");
47+
});
48+
});

src/util.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import "dotenv/config";
2+
3+
function isString(value: any): value is string {
4+
return typeof value === "string" && value.length > 0;
5+
}
6+
7+
export function getRequiredEnvVar(name: string, errorMessage?: string): string {
8+
const value = process.env[name];
9+
if (isString(value)) return value;
10+
throw new Error(errorMessage || `${name} is not set`);
11+
}
12+
13+
export const BREADBOARD_USER = getRequiredEnvVar("BREADBOARD_USER");
14+
export const BOARD_ID = getRequiredEnvVar("BOARD_ID");
15+
export const BREADBOARD_SERVER_URL = getRequiredEnvVar("BREADBOARD_SERVER_URL");
16+
export const BREADBOARD_API_KEY = getRequiredEnvVar("BREADBOARD_API_KEY");

0 commit comments

Comments
 (0)