Skip to content

Commit cbf76a0

Browse files
committed
feat(tests): add unit tests for utility functions and type guards
- Introduced tests for `getRequiredEnvVar` to validate environment variable retrieval and error handling. - Added tests for new type guard functions to ensure correct identification of event types. - Updated transforms to use testable functions for improved testability.
1 parent 8ab59a9 commit cbf76a0

10 files changed

+1911
-211
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"jest": "^29.7.0",
1212
"ts-jest": "^29.2.6",
1313
"ts-node": "^10.9.2",
14-
"typescript": "^5.8.2"
14+
"typescript": "^5.8.2",
15+
"web-streams-polyfill": "^4.1.0"
1516
},
1617
"keywords": [
1718
"breadboard",

src/__tests__/client.test.ts

+336-11
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,345 @@
11
import { BreadboardClient } from "../client";
22
import type { RunEvent } from "../types";
3-
import { BREADBOARD_API_KEY, BREADBOARD_SERVER_URL } from "../util";
3+
import {
4+
BOARD_ID,
5+
BREADBOARD_API_KEY,
6+
BREADBOARD_SERVER_URL,
7+
BREADBOARD_USER,
8+
} from "../util";
9+
10+
// Mock fetch for testing
11+
global.fetch = jest.fn();
412

513
describe("BreadboardClient", () => {
614
const config = {
715
baseUrl: BREADBOARD_SERVER_URL,
816
apiKey: BREADBOARD_API_KEY,
917
};
1018

11-
// beforeEach(() => {
12-
// global.fetch = jest.fn();
13-
// });
14-
15-
// afterEach(() => {
16-
// jest.resetAllMocks();
17-
// });
19+
beforeEach(() => {
20+
(global.fetch as jest.Mock).mockClear();
21+
});
1822

1923
test("should list boards", async () => {
24+
const mockResponse = [
25+
{
26+
title: "Test Board",
27+
path: "test/path",
28+
username: "user",
29+
readonly: false,
30+
mine: true,
31+
tags: [],
32+
},
33+
];
34+
35+
(global.fetch as jest.Mock).mockResolvedValueOnce({
36+
ok: true,
37+
json: async () => mockResponse,
38+
});
39+
2040
const client = new BreadboardClient(config);
2141
const result = await client.listBoards();
22-
expect(Array.isArray(result)).toBeTruthy();
23-
expect(result[0]).toHaveProperty("title");
24-
expect(result[0]).toHaveProperty("path");
42+
43+
expect(global.fetch).toHaveBeenCalledWith(
44+
`${config.baseUrl}/boards?API_KEY=${config.apiKey}`,
45+
expect.objectContaining({
46+
method: "GET",
47+
headers: expect.objectContaining({
48+
"Content-Type": "application/json",
49+
}),
50+
})
51+
);
52+
53+
expect(result).toEqual(mockResponse);
54+
});
55+
56+
test("should handle errors when listing boards", async () => {
57+
(global.fetch as jest.Mock).mockResolvedValueOnce({
58+
ok: false,
59+
statusText: "Not Found",
60+
});
61+
62+
const client = new BreadboardClient(config);
63+
64+
await expect(client.listBoards()).rejects.toThrow(
65+
"Failed to list boards: Not Found"
66+
);
67+
});
68+
69+
test("should run a board", async () => {
70+
const mockReadableStream = new ReadableStream({
71+
start(controller) {
72+
controller.close();
73+
},
74+
});
75+
76+
(global.fetch as jest.Mock).mockResolvedValueOnce({
77+
ok: true,
78+
body: mockReadableStream,
79+
});
80+
81+
const client = new BreadboardClient(config);
82+
await client.runBoard({
83+
user: BREADBOARD_USER,
84+
board: BOARD_ID,
85+
data: { input: "test" },
86+
next: "token123",
87+
});
88+
89+
expect(global.fetch).toHaveBeenCalledWith(
90+
`${config.baseUrl}/boards/@${BREADBOARD_USER}/${BOARD_ID}.api/run`,
91+
expect.objectContaining({
92+
method: "POST",
93+
body: JSON.stringify({
94+
$key: config.apiKey,
95+
$next: "token123",
96+
input: "test",
97+
}),
98+
})
99+
);
100+
});
101+
102+
test("should handle errors when running a board", async () => {
103+
(global.fetch as jest.Mock).mockResolvedValueOnce({
104+
ok: false,
105+
statusText: "Bad Request",
106+
});
107+
108+
const client = new BreadboardClient(config);
109+
110+
await expect(
111+
client.runBoard({
112+
user: BREADBOARD_USER,
113+
board: BOARD_ID,
114+
})
115+
).rejects.toThrow("Failed to run board: Bad Request");
116+
});
117+
118+
test("should handle missing response body when running a board", async () => {
119+
(global.fetch as jest.Mock).mockResolvedValueOnce({
120+
ok: true,
121+
body: null,
122+
});
123+
124+
const client = new BreadboardClient(config);
125+
126+
await expect(
127+
client.runBoard({
128+
user: BREADBOARD_USER,
129+
board: BOARD_ID,
130+
})
131+
).rejects.toThrow("No response body received");
132+
});
133+
134+
test("should run a board and collect events", async () => {
135+
const events: RunEvent[] = [
136+
[
137+
"input",
138+
{ node: { id: "test" }, inputArguments: { schema: {} } },
139+
"next1",
140+
],
141+
["output", { node: { id: "test" }, outputs: {} }, "next2"],
142+
];
143+
144+
// Mock implementation of runBoard to return a stream with our test events
145+
const mockStream = new ReadableStream({
146+
start(controller) {
147+
events.forEach((event) => controller.enqueue(event));
148+
controller.close();
149+
},
150+
});
151+
152+
const client = new BreadboardClient(config);
153+
const spy = jest.spyOn(client, "runBoard").mockResolvedValue(mockStream);
154+
155+
const result = await client.runBoardAndCollect({
156+
user: BREADBOARD_USER,
157+
board: BOARD_ID,
158+
data: { input: "test" },
159+
});
160+
161+
expect(spy).toHaveBeenCalled();
162+
expect(result).toEqual(events);
163+
164+
spy.mockRestore();
165+
});
166+
167+
test("should invoke a board", async () => {
168+
const mockReadableStream = new ReadableStream({
169+
start(controller) {
170+
controller.close();
171+
},
172+
});
173+
174+
(global.fetch as jest.Mock).mockResolvedValueOnce({
175+
ok: true,
176+
body: mockReadableStream,
177+
});
178+
179+
const client = new BreadboardClient(config);
180+
await client.invokeBoard({
181+
user: BREADBOARD_USER,
182+
board: BOARD_ID,
183+
data: { input: "test" },
184+
});
185+
186+
expect(global.fetch).toHaveBeenCalledWith(
187+
`${config.baseUrl}/boards/@${BREADBOARD_USER}/${BOARD_ID}.api/invoke`,
188+
expect.objectContaining({
189+
method: "POST",
190+
body: JSON.stringify({
191+
$key: config.apiKey,
192+
input: "test",
193+
}),
194+
})
195+
);
196+
});
197+
198+
test("should handle errors when invoking a board", async () => {
199+
(global.fetch as jest.Mock).mockResolvedValueOnce({
200+
ok: false,
201+
statusText: "Bad Request",
202+
});
203+
204+
const client = new BreadboardClient(config);
205+
206+
await expect(
207+
client.invokeBoard({
208+
user: BREADBOARD_USER,
209+
board: BOARD_ID,
210+
})
211+
).rejects.toThrow("Failed to invoke board: Bad Request");
212+
});
213+
214+
test("should handle missing response body when invoking a board", async () => {
215+
(global.fetch as jest.Mock).mockResolvedValueOnce({
216+
ok: true,
217+
body: null,
218+
});
219+
220+
const client = new BreadboardClient(config);
221+
222+
await expect(
223+
client.invokeBoard({
224+
user: BREADBOARD_USER,
225+
board: BOARD_ID,
226+
})
227+
).rejects.toThrow("No response body received");
228+
});
229+
230+
test("should get a board", async () => {
231+
const mockResponse = {
232+
title: "Test Board",
233+
nodes: [],
234+
edges: [],
235+
};
236+
237+
(global.fetch as jest.Mock).mockResolvedValueOnce({
238+
ok: true,
239+
json: async () => mockResponse,
240+
});
241+
242+
const client = new BreadboardClient(config);
243+
const result = await client.getBoard(BREADBOARD_USER, BOARD_ID);
244+
245+
expect(global.fetch).toHaveBeenCalledWith(
246+
`${config.baseUrl}/boards/@${BREADBOARD_USER}/${BOARD_ID}.json`,
247+
expect.objectContaining({
248+
method: "GET",
249+
headers: expect.objectContaining({
250+
"Content-Type": "application/json",
251+
$key: config.apiKey,
252+
}),
253+
})
254+
);
255+
256+
expect(result).toEqual(mockResponse);
257+
});
258+
259+
test("should handle errors when getting a board", async () => {
260+
(global.fetch as jest.Mock).mockResolvedValueOnce({
261+
ok: false,
262+
statusText: "Not Found",
263+
});
264+
265+
const client = new BreadboardClient(config);
266+
267+
await expect(client.getBoard(BREADBOARD_USER, BOARD_ID)).rejects.toThrow(
268+
"Failed to get board: Not Found"
269+
);
270+
});
271+
272+
test("should describe a board", async () => {
273+
const mockResponse = {
274+
inputSchema: {},
275+
outputSchema: {},
276+
title: "Test Board",
277+
};
278+
279+
(global.fetch as jest.Mock).mockResolvedValueOnce({
280+
ok: true,
281+
json: async () => mockResponse,
282+
});
283+
284+
const client = new BreadboardClient(config);
285+
const result = await client.describeBoard({
286+
user: BREADBOARD_USER,
287+
board: BOARD_ID,
288+
});
289+
290+
expect(global.fetch).toHaveBeenCalledWith(
291+
`${config.baseUrl}/boards/@${BREADBOARD_USER}/${BOARD_ID}.api/describe`,
292+
expect.objectContaining({
293+
method: "POST",
294+
headers: expect.objectContaining({
295+
"Content-Type": "application/json",
296+
$key: config.apiKey,
297+
}),
298+
})
299+
);
300+
301+
expect(result).toEqual(mockResponse);
302+
});
303+
304+
test("should cleanup board ID and user when describing a board", async () => {
305+
const mockResponse = {
306+
inputSchema: {},
307+
outputSchema: {},
308+
};
309+
310+
(global.fetch as jest.Mock).mockResolvedValueOnce({
311+
ok: true,
312+
json: async () => mockResponse,
313+
});
314+
315+
const client = new BreadboardClient(config);
316+
317+
// Test with .json suffix and @ prefix
318+
await client.describeBoard({
319+
user: `@${BREADBOARD_USER}`,
320+
board: `${BOARD_ID}.json`,
321+
});
322+
323+
expect(global.fetch).toHaveBeenCalledWith(
324+
`${config.baseUrl}/boards/@${BREADBOARD_USER}/${BOARD_ID}.api/describe`,
325+
expect.any(Object)
326+
);
327+
});
328+
329+
test("should handle errors when describing a board", async () => {
330+
(global.fetch as jest.Mock).mockResolvedValueOnce({
331+
ok: false,
332+
statusText: "Not Found",
333+
});
334+
335+
const client = new BreadboardClient(config);
336+
337+
await expect(
338+
client.describeBoard({
339+
user: BREADBOARD_USER,
340+
board: BOARD_ID,
341+
})
342+
).rejects.toThrow("Failed to describe board: Not Found");
25343
});
26344

27345
test("should collect stream events", async () => {
@@ -58,4 +376,11 @@ describe("BreadboardClient", () => {
58376
const token = BreadboardClient.getNextToken(events);
59377
expect(token).toBe("next2");
60378
});
379+
380+
test("should return undefined when no next token is available", () => {
381+
const events: RunEvent[] = [["error", "Some error occurred"]];
382+
383+
const token = BreadboardClient.getNextToken(events);
384+
expect(token).toBeUndefined();
385+
});
61386
});

0 commit comments

Comments
 (0)