返回 CodeWhale
fleet-client.test.mjs
根目录 / npm / runtime-sdk / test / fleet-client.test.mjs
1 import assert from "node:assert/strict";
2 import test from "node:test";
3 import {
4 CodeWhaleRuntimeClient,
5 RuntimeApiError,
6 RuntimeCapabilityError,
7 createRuntimeClient,
8 } from "../index.js";
9
10 function jsonResponse(body, init = {}) {
11 return new Response(JSON.stringify(body), {
12 status: init.status ?? 200,
13 headers: { "content-type": "application/json", ...(init.headers ?? {}) },
14 });
15 }
16
17 function fakeFetch(responseFactory) {
18 const calls = [];
19 const fetch = async (url, init) => {
20 calls.push({ url: url.toString(), init });
21 return responseFactory(url, init, calls.length);
22 };
23 fetch.calls = calls;
24 return fetch;
25 }
26
27 test("createRuntimeClient returns CodeWhaleRuntimeClient instance", () => {
28 const fetch = fakeFetch(() => jsonResponse({}));
29 const client = createRuntimeClient({ fetch });
30
31 assert.ok(client instanceof CodeWhaleRuntimeClient);
32 assert.equal(client.baseUrl, "http://127.0.0.1:7878/");
33 });
34
35 test("listFleetRuns calls the Runtime API with bearer auth", async () => {
36 const fetch = fakeFetch(() =>
37 jsonResponse({
38 status: { runs: 1, workers: {} },
39 runs: [{ id: "run-1", name: "smoke", tasks: [], labels: {} }],
40 }),
41 );
42 const client = createRuntimeClient({
43 baseUrl: "http://127.0.0.1:7878",
44 token: "token-1",
45 fetch,
46 });
47
48 const response = await client.listFleetRuns();
49
50 assert.equal(response.runs[0].id, "run-1");
51 assert.equal(fetch.calls[0].url, "http://127.0.0.1:7878/v1/fleet/runs");
52 assert.equal(fetch.calls[0].init.method, "GET");
53 assert.equal(fetch.calls[0].init.headers.get("authorization"), "Bearer token-1");
54 });
55
56 test("worker and run actions use POST endpoints", async () => {
57 const fetch = fakeFetch((url) =>
58 jsonResponse(
59 url.pathname.endsWith("/stop")
60 ? {
61 action: "stop",
62 run_id: "run-1",
63 stopped: 1,
64 status: { runs: 1, workers: {} },
65 }
66 : {
67 action: url.pathname.endsWith("/restart")
68 ? "restart"
69 : url.pathname.endsWith("/stop")
70 ? "stop"
71 : "interrupt",
72 worker: { worker_id: "w1", artifacts: [] },
73 },
74 ),
75 );
76 const client = new CodeWhaleRuntimeClient({ fetch });
77
78 await client.interruptWorker("w1");
79 await client.stopWorker("w1");
80 await client.restartWorker("w1");
81 await client.startFleetRun("run-1");
82 await client.stopFleetRun("run-1");
83
84 assert.deepEqual(
85 fetch.calls.map((call) => [new URL(call.url).pathname, call.init.method]),
86 [
87 ["/v1/fleet/workers/w1/interrupt", "POST"],
88 ["/v1/fleet/workers/w1/stop", "POST"],
89 ["/v1/fleet/workers/w1/restart", "POST"],
90 ["/v1/fleet/runs/run-1/start", "POST"],
91 ["/v1/fleet/runs/run-1/stop", "POST"],
92 ],
93 );
94 });
95
96 test("managed Fleet helpers send explicit launch metadata and reconnect cursors", async () => {
97 const fetch = fakeFetch((url) =>
98 jsonResponse(
99 url.pathname.endsWith("/events/replay")
100 ? { run_id: "run-1", events: [], has_more: false, history_truncated: false }
101 : { execution: "awaiting_start", run: { id: "run-1" }, warnings: [] },
102 ),
103 );
104 const client = new CodeWhaleRuntimeClient({ fetch });
105 const spec = {
106 target: "this_computer",
107 roles: [{ name: "reviewer" }],
108 workflow: {
109 id: "review",
110 kind: "parallel",
111 tasks: [{ id: "review", name: "Review", instructions: "Review.", worker: { role: "reviewer" } }],
112 },
113 };
114
115 await client.createFleetRun(spec);
116 await client.replayFleetEvents("run-1", { after: "fev1_cursor_worker", limit: 25 });
117
118 assert.deepEqual(JSON.parse(fetch.calls[0].init.body), spec);
119 const replayUrl = new URL(fetch.calls[1].url);
120 assert.equal(replayUrl.pathname, "/v1/fleet/runs/run-1/events/replay");
121 assert.equal(replayUrl.searchParams.get("after"), "fev1_cursor_worker");
122 assert.equal(replayUrl.searchParams.get("limit"), "25");
123 });
124
125 test("unsupported fleet capabilities raise typed errors", async () => {
126 const fetch = fakeFetch(() => jsonResponse({ error: "not found" }, { status: 404 }));
127 const client = new CodeWhaleRuntimeClient({ fetch });
128
129 await assert.rejects(
130 () => client.createFleetRun({ name: "future" }),
131 (error) =>
132 error instanceof RuntimeCapabilityError &&
133 error.capability === "fleet_run_create" &&
134 error.status === 404,
135 );
136
137 await assert.rejects(
138 async () => {
139 for await (const _event of client.fleetEvents("run-1")) {
140 throw new Error("unexpected event");
141 }
142 },
143 (error) =>
144 error instanceof RuntimeCapabilityError &&
145 error.capability === "fleet_event_stream" &&
146 error.status === 404,
147 );
148 });
149
150 test("fleetEvents can replay JSON event fixtures when the API exposes them", async () => {
151 const fetch = fakeFetch(() =>
152 jsonResponse({
153 events: [
154 {
155 seq: 1,
156 run_id: "run-1",
157 worker_id: "w1",
158 task_id: "task-1",
159 timestamp: "2026-06-13T00:00:00Z",
160 label: "running",
161 payload: { state: "running" },
162 },
163 ],
164 }),
165 );
166 const client = new CodeWhaleRuntimeClient({ fetch });
167
168 const events = [];
169 for await (const event of client.fleetEvents("run-1", { path: "/v1/fleet/runs/run-1/events" })) {
170 events.push(event);
171 }
172
173 assert.equal(events.length, 1);
174 assert.equal(events[0].payload.state, "running");
175 });
176
177 test("fleetEvents parses text/event-stream frames", async () => {
178 const encoder = new TextEncoder();
179 const body = new ReadableStream({
180 start(controller) {
181 controller.enqueue(
182 encoder.encode(
183 'id: fev1_heartbeat_worker\nevent: fleet.worker.heartbeat\ndata: {"cursor":"fev1_heartbeat_worker","event":"fleet.worker.heartbeat","run_id":"run-1","worker_id":"w1","task_id":"task-1","timestamp":"2026-06-13T00:00:01Z","worker_seq":2,"payload":{"state":"heartbeat","memory_mb":128}}\n\n',
184 ),
185 );
186 controller.close();
187 },
188 });
189 const fetch = fakeFetch(
190 () =>
191 new Response(body, {
192 status: 200,
193 headers: { "content-type": "text/event-stream" },
194 }),
195 );
196 const client = new CodeWhaleRuntimeClient({ fetch });
197
198 const events = [];
199 for await (const event of client.fleetEvents("run-1", { after: "fev1_previous", limit: 10 })) {
200 events.push(event);
201 }
202
203 assert.equal(events.length, 1);
204 assert.equal(events[0].payload.state, "heartbeat");
205 assert.equal(events[0].payload.memory_mb, 128);
206 const eventUrl = new URL(fetch.calls[0].url);
207 assert.equal(eventUrl.searchParams.get("after"), "fev1_previous");
208 assert.equal(eventUrl.searchParams.get("limit"), "10");
209 assert.equal(fetch.calls[0].init.headers.get("accept"), "text/event-stream");
210 });
211
212 test("fleetEvents preserves SSE control event names", async () => {
213 const encoder = new TextEncoder();
214 const body = new ReadableStream({
215 start(controller) {
216 controller.enqueue(
217 encoder.encode(
218 'event: fleet.replay.cursor_unavailable\r\ndata: {"run_id":"run-1","reload_projection":true}\r\n\r\n',
219 ),
220 );
221 controller.close();
222 },
223 });
224 const client = new CodeWhaleRuntimeClient({
225 fetch: fakeFetch(
226 () =>
227 new Response(body, {
228 status: 200,
229 headers: { "content-type": "text/event-stream" },
230 }),
231 ),
232 });
233
234 const events = [];
235 for await (const event of client.fleetEvents("run-1")) {
236 events.push(event);
237 }
238
239 assert.deepEqual(events, [
240 {
241 event: "fleet.replay.cursor_unavailable",
242 run_id: "run-1",
243 reload_projection: true,
244 },
245 ]);
246 });
247
248 test("ordinary HTTP errors remain RuntimeApiError", async () => {
249 const fetch = fakeFetch(() => jsonResponse({ error: "bad" }, { status: 500 }));
250 const client = new CodeWhaleRuntimeClient({ fetch });
251
252 await assert.rejects(
253 () => client.getFleetRun("run-1"),
254 (error) =>
255 error instanceof RuntimeApiError &&
256 !(error instanceof RuntimeCapabilityError) &&
257 error.status === 500,
258 );
259 });
260
260 lines Plain Text