-
Notifications
You must be signed in to change notification settings - Fork 2
Add Runs resource and runs.test.ts #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import APIResource, { ResourceObject } from "./base"; | ||
| import { RunData } from "../types/run"; | ||
| import beamClient, { beamOpts } from "../index"; | ||
| import { EStubType } from "../types/stub"; | ||
|
|
||
| class Runs extends APIResource<Run, RunData> { | ||
| public object: string = "task"; | ||
|
|
||
| protected _constructResource(data: RunData): Run { | ||
| return new Run(this, data); | ||
| } | ||
|
|
||
| public async list(opts?: any): Promise<Run[]> { | ||
| return super.list({ | ||
| stubType: EStubType.PodRun, | ||
| ...opts, | ||
| }); | ||
| } | ||
|
|
||
| public async cancel(runs: string[] | Run[]): Promise<void> { | ||
| const ids = runs.map((r) => (r instanceof Run ? r.data.id : r)); | ||
| return await beamClient.request({ | ||
| method: "DELETE", | ||
| url: `/api/v1/task/${beamOpts.workspaceId}`, | ||
| data: { | ||
| ids, | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| class Run implements ResourceObject<RunData> { | ||
| public data: RunData; | ||
| public manager: Runs; | ||
|
|
||
| constructor(resource: Runs, data: RunData) { | ||
| this.manager = resource; | ||
| this.data = data; | ||
| } | ||
|
|
||
| public async refresh(): Promise<Run> { | ||
| const data = await this.manager.get({ id: this.data.id }); | ||
| this.data = data.data; | ||
| return this; | ||
| } | ||
|
|
||
| public async cancel(): Promise<void> { | ||
| return await this.manager.cancel([this]); | ||
| } | ||
| } | ||
|
|
||
| export default new Runs(); | ||
| export { Run }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { TaskData, ETaskStatus } from "./task"; | ||
|
|
||
| // RunData represents a pod/run task | ||
| export type RunData = TaskData; | ||
|
|
||
| export { ETaskStatus }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| import Runs, { Run } from "../lib/resources/run"; | ||
| import { ETaskStatus } from "../lib/types/task"; | ||
| import { EStubType } from "../lib/types/stub"; | ||
|
|
||
| // Mock the beamClient and beamOpts used by the resource | ||
| jest.mock("../lib/index", () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| request: jest.fn(), | ||
| _parseOptsToURLParams: jest.fn((opts) => new URLSearchParams(opts)), | ||
| }, | ||
| beamOpts: { | ||
| token: "test-token", | ||
| workspaceId: "test-workspace", | ||
| gatewayUrl: "https://app.beam.cloud", | ||
| timeout: 30000, | ||
| }, | ||
| })); | ||
|
|
||
| import beamClient, { beamOpts } from "../lib/index"; | ||
|
|
||
| const mockBeamClient = beamClient as jest.Mocked<typeof beamClient>; | ||
|
|
||
| function makeRunData(overrides = {}) { | ||
| return { | ||
| id: "run-abc123", | ||
| status: ETaskStatus.RUNNING, | ||
| containerId: "container-xyz", | ||
| startedAt: "2024-01-01T00:00:00Z", | ||
| endedAt: "", | ||
| stubId: "stub-123", | ||
| stubName: "my-pod", | ||
| workspaceId: "test-workspace", | ||
| workspaceName: "my-workspace", | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| updatedAt: "2024-01-01T00:00:00Z", | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| describe("Run", () => { | ||
| describe("constructor", () => { | ||
| test("stores data and manager references", () => { | ||
| const data = makeRunData(); | ||
| const run = new Run(Runs, data); | ||
|
|
||
| expect(run.data).toBe(data); | ||
| expect(run.manager).toBe(Runs); | ||
| }); | ||
|
|
||
| test("exposes run fields via data", () => { | ||
| const data = makeRunData({ status: ETaskStatus.COMPLETE, containerId: "c-99" }); | ||
| const run = new Run(Runs, data); | ||
|
|
||
| expect(run.data.status).toBe(ETaskStatus.COMPLETE); | ||
| expect(run.data.containerId).toBe("c-99"); | ||
| expect(run.data.id).toBe("run-abc123"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("cancel", () => { | ||
| test("delegates to manager.cancel with self", async () => { | ||
| const data = makeRunData(); | ||
| const run = new Run(Runs, data); | ||
| const cancelSpy = jest.spyOn(Runs, "cancel").mockResolvedValue(undefined); | ||
|
|
||
| await run.cancel(); | ||
|
|
||
| expect(cancelSpy).toHaveBeenCalledWith([run]); | ||
| cancelSpy.mockRestore(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("refresh", () => { | ||
| test("calls manager.get with run id and updates data", async () => { | ||
| const originalData = makeRunData({ status: ETaskStatus.RUNNING }); | ||
| const updatedData = makeRunData({ status: ETaskStatus.COMPLETE }); | ||
| const run = new Run(Runs, originalData); | ||
|
|
||
| const getSpy = jest.spyOn(Runs, "get").mockResolvedValue(new Run(Runs, updatedData)); | ||
|
|
||
| const result = await run.refresh(); | ||
|
|
||
| expect(getSpy).toHaveBeenCalledWith({ id: "run-abc123" }); | ||
| expect(run.data.status).toBe(ETaskStatus.COMPLETE); | ||
| expect(result).toBe(run); | ||
|
|
||
| getSpy.mockRestore(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Runs", () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe("_constructResource", () => { | ||
| test("creates a Run instance from data", () => { | ||
| const data = makeRunData(); | ||
| const run = (Runs as any)._constructResource(data); | ||
|
|
||
| expect(run).toBeInstanceOf(Run); | ||
| expect(run.data).toBe(data); | ||
| }); | ||
| }); | ||
|
|
||
| describe("object", () => { | ||
| test("uses task as the API object name", () => { | ||
| expect((Runs as any).object).toBe("task"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("list", () => { | ||
| test("passes pod/run stub type filter to the API", async () => { | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue({ | ||
| status: 200, | ||
| data: { data: [makeRunData()] }, | ||
| }); | ||
| (mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue( | ||
| new URLSearchParams({ stub_type: EStubType.PodRun }) | ||
| ); | ||
|
|
||
| await Runs.list(); | ||
|
|
||
| expect(mockBeamClient._parseOptsToURLParams).toHaveBeenCalledWith( | ||
| expect.objectContaining({ stubType: EStubType.PodRun }) | ||
| ); | ||
| }); | ||
|
|
||
| test("merges caller opts with pod/run stub type", async () => { | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue({ | ||
| status: 200, | ||
| data: { data: [] }, | ||
| }); | ||
| (mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue( | ||
| new URLSearchParams() | ||
| ); | ||
|
|
||
| await Runs.list({ limit: 10 }); | ||
|
|
||
| expect(mockBeamClient._parseOptsToURLParams).toHaveBeenCalledWith( | ||
| expect.objectContaining({ stubType: EStubType.PodRun, limit: 10 }) | ||
| ); | ||
| }); | ||
|
|
||
| test("returns Run instances", async () => { | ||
| const runData = makeRunData(); | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue({ | ||
| status: 200, | ||
| data: { data: [runData] }, | ||
| }); | ||
| (mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue( | ||
| new URLSearchParams() | ||
| ); | ||
|
|
||
| const runs = await Runs.list(); | ||
|
|
||
| expect(runs).toHaveLength(1); | ||
| expect(runs[0]).toBeInstanceOf(Run); | ||
| expect(runs[0].data.id).toBe("run-abc123"); | ||
| }); | ||
|
|
||
| test("returns empty array when API returns no data", async () => { | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue({ | ||
| status: 200, | ||
| data: {}, | ||
| }); | ||
| (mockBeamClient._parseOptsToURLParams as jest.Mock).mockReturnValue( | ||
| new URLSearchParams() | ||
| ); | ||
|
|
||
| const runs = await Runs.list(); | ||
|
|
||
| expect(runs).toEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| describe("cancel", () => { | ||
| test("sends DELETE request with run ids from Run instances", async () => { | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue(undefined); | ||
|
|
||
| const run1 = new Run(Runs, makeRunData({ id: "run-1" })); | ||
| const run2 = new Run(Runs, makeRunData({ id: "run-2" })); | ||
|
|
||
| await Runs.cancel([run1, run2]); | ||
|
|
||
| expect(mockBeamClient.request).toHaveBeenCalledWith({ | ||
| method: "DELETE", | ||
| url: `/api/v1/task/${beamOpts.workspaceId}`, | ||
| data: { ids: ["run-1", "run-2"] }, | ||
| }); | ||
| }); | ||
|
|
||
| test("sends DELETE request with raw id strings", async () => { | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue(undefined); | ||
|
|
||
| await Runs.cancel(["run-abc", "run-def"]); | ||
|
|
||
| expect(mockBeamClient.request).toHaveBeenCalledWith({ | ||
| method: "DELETE", | ||
| url: `/api/v1/task/${beamOpts.workspaceId}`, | ||
| data: { ids: ["run-abc", "run-def"] }, | ||
| }); | ||
| }); | ||
|
|
||
| test("handles mixed Run instances and id strings", async () => { | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue(undefined); | ||
|
|
||
| const run = new Run(Runs, makeRunData({ id: "run-obj" })); | ||
|
|
||
| await Runs.cancel([run, "run-str"]); | ||
|
|
||
| expect(mockBeamClient.request).toHaveBeenCalledWith({ | ||
| method: "DELETE", | ||
| url: `/api/v1/task/${beamOpts.workspaceId}`, | ||
| data: { ids: ["run-obj", "run-str"] }, | ||
| }); | ||
| }); | ||
|
|
||
| test("handles empty array", async () => { | ||
| (mockBeamClient.request as jest.Mock).mockResolvedValue(undefined); | ||
|
|
||
| await Runs.cancel([]); | ||
|
|
||
| expect(mockBeamClient.request).toHaveBeenCalledWith({ | ||
| method: "DELETE", | ||
| url: `/api/v1/task/${beamOpts.workspaceId}`, | ||
| data: { ids: [] }, | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Runs.list allows opts.stubType to override the intended PodRun filter because the spread comes after stubType. This can return non-run tasks. Spread opts first, then set stubType to enforce the filter.
Prompt for AI agents