Skip to content

Commit a63b4cc

Browse files
luke-lombardiclaude
andcommitted
Add Runs resource and runs.test.ts
- Add lib/types/run.ts: RunData type (alias for TaskData) for pod/run tasks - Add lib/resources/run.ts: Run and Runs classes mirroring Task/Tasks pattern; Runs.list() auto-filters by pod/run stub type - Add tests/runs.test.ts: 14 unit tests covering Run construction, cancel, refresh, and Runs._constructResource, object, list, and cancel - Update lib/index.ts to export Run, Runs, and run types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8b90eca commit a63b4cc

4 files changed

Lines changed: 296 additions & 0 deletions

File tree

lib/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export * from "./types/deployment";
7070
export * from "./resources/task";
7171
export * from "./types/task";
7272

73+
// Export Run classes and types
74+
export { default as Runs, Run } from "./resources/run";
75+
export * from "./types/run";
76+
7377
// Export Pod classes and types
7478
export { Pod, PodInstance } from "./resources/abstraction/pod";
7579
export * from "./types/pod";

lib/resources/run.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import APIResource, { ResourceObject } from "./base";
2+
import { RunData } from "../types/run";
3+
import beamClient, { beamOpts } from "../index";
4+
import { EStubType } from "../types/stub";
5+
6+
class Runs extends APIResource<Run, RunData> {
7+
public object: string = "task";
8+
9+
protected _constructResource(data: RunData): Run {
10+
return new Run(this, data);
11+
}
12+
13+
public async list(opts?: any): Promise<Run[]> {
14+
return super.list({
15+
stubType: EStubType.PodRun,
16+
...opts,
17+
});
18+
}
19+
20+
public async cancel(runs: string[] | Run[]): Promise<void> {
21+
const ids = runs.map((r) => (r instanceof Run ? r.data.id : r));
22+
return await beamClient.request({
23+
method: "DELETE",
24+
url: `/api/v1/task/${beamOpts.workspaceId}`,
25+
data: {
26+
ids,
27+
},
28+
});
29+
}
30+
}
31+
32+
class Run implements ResourceObject<RunData> {
33+
public data: RunData;
34+
public manager: Runs;
35+
36+
constructor(resource: Runs, data: RunData) {
37+
this.manager = resource;
38+
this.data = data;
39+
}
40+
41+
public async refresh(): Promise<Run> {
42+
const data = await this.manager.get({ id: this.data.id });
43+
this.data = data.data;
44+
return this;
45+
}
46+
47+
public async cancel(): Promise<void> {
48+
return await this.manager.cancel([this]);
49+
}
50+
}
51+
52+
export default new Runs();
53+
export { Run };

lib/types/run.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { TaskData, ETaskStatus } from "./task";
2+
3+
// RunData represents a pod/run task
4+
export type RunData = TaskData;
5+
6+
export { ETaskStatus };

tests/runs.test.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import Runs, { Run } from "../lib/resources/run";
2+
import { ETaskStatus } from "../lib/types/task";
3+
import { EStubType } from "../lib/types/stub";
4+
5+
// Mock the beamClient and beamOpts used by the resource
6+
jest.mock("../lib/index", () => ({
7+
__esModule: true,
8+
default: {
9+
request: jest.fn(),
10+
_parseOptsToURLParams: jest.fn((opts) => new URLSearchParams(opts)),
11+
},
12+
beamOpts: {
13+
token: "test-token",
14+
workspaceId: "test-workspace",
15+
gatewayUrl: "https://app.beam.cloud",
16+
timeout: 30000,
17+
},
18+
}));
19+
20+
import beamClient, { beamOpts } from "../lib/index";
21+
22+
const mockBeamClient = beamClient as jest.Mocked<typeof beamClient>;
23+
24+
function makeRunData(overrides = {}) {
25+
return {
26+
id: "run-abc123",
27+
status: ETaskStatus.RUNNING,
28+
containerId: "container-xyz",
29+
startedAt: "2024-01-01T00:00:00Z",
30+
endedAt: "",
31+
stubId: "stub-123",
32+
stubName: "my-pod",
33+
workspaceId: "test-workspace",
34+
workspaceName: "my-workspace",
35+
createdAt: "2024-01-01T00:00:00Z",
36+
updatedAt: "2024-01-01T00:00:00Z",
37+
...overrides,
38+
};
39+
}
40+
41+
describe("Run", () => {
42+
describe("constructor", () => {
43+
test("stores data and manager references", () => {
44+
const data = makeRunData();
45+
const run = new Run(Runs, data);
46+
47+
expect(run.data).toBe(data);
48+
expect(run.manager).toBe(Runs);
49+
});
50+
51+
test("exposes run fields via data", () => {
52+
const data = makeRunData({ status: ETaskStatus.COMPLETE, containerId: "c-99" });
53+
const run = new Run(Runs, data);
54+
55+
expect(run.data.status).toBe(ETaskStatus.COMPLETE);
56+
expect(run.data.containerId).toBe("c-99");
57+
expect(run.data.id).toBe("run-abc123");
58+
});
59+
});
60+
61+
describe("cancel", () => {
62+
test("delegates to manager.cancel with self", async () => {
63+
const data = makeRunData();
64+
const run = new Run(Runs, data);
65+
const cancelSpy = jest.spyOn(Runs, "cancel").mockResolvedValue(undefined);
66+
67+
await run.cancel();
68+
69+
expect(cancelSpy).toHaveBeenCalledWith([run]);
70+
cancelSpy.mockRestore();
71+
});
72+
});
73+
74+
describe("refresh", () => {
75+
test("calls manager.get with run id and updates data", async () => {
76+
const originalData = makeRunData({ status: ETaskStatus.RUNNING });
77+
const updatedData = makeRunData({ status: ETaskStatus.COMPLETE });
78+
const run = new Run(Runs, originalData);
79+
80+
const getSpy = jest.spyOn(Runs, "get").mockResolvedValue(new Run(Runs, updatedData));
81+
82+
const result = await run.refresh();
83+
84+
expect(getSpy).toHaveBeenCalledWith({ id: "run-abc123" });
85+
expect(run.data.status).toBe(ETaskStatus.COMPLETE);
86+
expect(result).toBe(run);
87+
88+
getSpy.mockRestore();
89+
});
90+
});
91+
});
92+
93+
describe("Runs", () => {
94+
beforeEach(() => {
95+
jest.clearAllMocks();
96+
});
97+
98+
describe("_constructResource", () => {
99+
test("creates a Run instance from data", () => {
100+
const data = makeRunData();
101+
const run = (Runs as any)._constructResource(data);
102+
103+
expect(run).toBeInstanceOf(Run);
104+
expect(run.data).toBe(data);
105+
});
106+
});
107+
108+
describe("object", () => {
109+
test("uses task as the API object name", () => {
110+
expect((Runs as any).object).toBe("task");
111+
});
112+
});
113+
114+
describe("list", () => {
115+
test("passes pod/run stub type filter to the API", async () => {
116+
(mockBeamClient.request as jest.Mock).mockResolvedValue({
117+
status: 200,
118+
data: { data: [makeRunData()] },
119+
});
120+
(mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue(
121+
new URLSearchParams({ stub_type: EStubType.PodRun })
122+
);
123+
124+
await Runs.list();
125+
126+
expect(mockBeamClient._parseOptsToURLParams).toHaveBeenCalledWith(
127+
expect.objectContaining({ stubType: EStubType.PodRun })
128+
);
129+
});
130+
131+
test("merges caller opts with pod/run stub type", async () => {
132+
(mockBeamClient.request as jest.Mock).mockResolvedValue({
133+
status: 200,
134+
data: { data: [] },
135+
});
136+
(mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue(
137+
new URLSearchParams()
138+
);
139+
140+
await Runs.list({ limit: 10 });
141+
142+
expect(mockBeamClient._parseOptsToURLParams).toHaveBeenCalledWith(
143+
expect.objectContaining({ stubType: EStubType.PodRun, limit: 10 })
144+
);
145+
});
146+
147+
test("returns Run instances", async () => {
148+
const runData = makeRunData();
149+
(mockBeamClient.request as jest.Mock).mockResolvedValue({
150+
status: 200,
151+
data: { data: [runData] },
152+
});
153+
(mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue(
154+
new URLSearchParams()
155+
);
156+
157+
const runs = await Runs.list();
158+
159+
expect(runs).toHaveLength(1);
160+
expect(runs[0]).toBeInstanceOf(Run);
161+
expect(runs[0].data.id).toBe("run-abc123");
162+
});
163+
164+
test("returns empty array when API returns no data", async () => {
165+
(mockBeamClient.request as jest.Mock).mockResolvedValue({
166+
status: 200,
167+
data: {},
168+
});
169+
(mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue(
170+
new URLSearchParams()
171+
);
172+
173+
const runs = await Runs.list();
174+
175+
expect(runs).toEqual([]);
176+
});
177+
});
178+
179+
describe("cancel", () => {
180+
test("sends DELETE request with run ids from Run instances", async () => {
181+
(mockBeamClient.request as jest.Mock).mockResolvedValue(undefined);
182+
183+
const run1 = new Run(Runs, makeRunData({ id: "run-1" }));
184+
const run2 = new Run(Runs, makeRunData({ id: "run-2" }));
185+
186+
await Runs.cancel([run1, run2]);
187+
188+
expect(mockBeamClient.request).toHaveBeenCalledWith({
189+
method: "DELETE",
190+
url: `/api/v1/task/${beamOpts.workspaceId}`,
191+
data: { ids: ["run-1", "run-2"] },
192+
});
193+
});
194+
195+
test("sends DELETE request with raw id strings", async () => {
196+
(mockBeamClient.request as jest.Mock).mockResolvedValue(undefined);
197+
198+
await Runs.cancel(["run-abc", "run-def"]);
199+
200+
expect(mockBeamClient.request).toHaveBeenCalledWith({
201+
method: "DELETE",
202+
url: `/api/v1/task/${beamOpts.workspaceId}`,
203+
data: { ids: ["run-abc", "run-def"] },
204+
});
205+
});
206+
207+
test("handles mixed Run instances and id strings", async () => {
208+
(mockBeamClient.request as jest.Mock).mockResolvedValue(undefined);
209+
210+
const run = new Run(Runs, makeRunData({ id: "run-obj" }));
211+
212+
await Runs.cancel([run, "run-str"]);
213+
214+
expect(mockBeamClient.request).toHaveBeenCalledWith({
215+
method: "DELETE",
216+
url: `/api/v1/task/${beamOpts.workspaceId}`,
217+
data: { ids: ["run-obj", "run-str"] },
218+
});
219+
});
220+
221+
test("handles empty array", async () => {
222+
(mockBeamClient.request as jest.Mock).mockResolvedValue(undefined);
223+
224+
await Runs.cancel([]);
225+
226+
expect(mockBeamClient.request).toHaveBeenCalledWith({
227+
method: "DELETE",
228+
url: `/api/v1/task/${beamOpts.workspaceId}`,
229+
data: { ids: [] },
230+
});
231+
});
232+
});
233+
});

0 commit comments

Comments
 (0)