File
Blob: tests/worker/dispatch/shared/process-tree.test.ts
| 1 | import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | |
| 3 | import { |
| 4 | destroySandbox, |
| 5 | getLiveCurrentProcess, |
| 6 | softCancelProcessTree, |
| 7 | waitForProcessTreeToStop, |
| 8 | } from "@/worker/dispatch/shared/run-execution-context/process-tree"; |
| 9 | import { |
| 10 | CONTAINER_NOT_RUNNING_ERROR_SUBSTRING, |
| 11 | NO_CONTAINER_INSTANCE_ERROR_SUBSTRING, |
| 12 | } from "@/worker/sandbox/container-errors"; |
| 13 | import type { RunExecutionContextState } from "@/worker/dispatch/shared/run-execution-context/types"; |
| 14 | |
| 15 | import { createQueueScope, createQueueState } from "../../../helpers/dispatch/shared"; |
| 16 | |
| 17 | const makeScope = () => createQueueScope(); |
| 18 | |
| 19 | const makeState = () => createQueueState(); |
| 20 | |
| 21 | afterEach(() => { |
| 22 | vi.restoreAllMocks(); |
| 23 | }); |
| 24 | |
| 25 | describe("queue process helpers", () => { |
| 26 | it("returns only live current processes", () => { |
| 27 | const state = makeState(); |
| 28 | state.currentProcess = { |
| 29 | id: "proc-live", |
| 30 | status: "running", |
| 31 | } as RunExecutionContextState["currentProcess"]; |
| 32 | |
| 33 | expect(getLiveCurrentProcess(state)?.id).toBe("proc-live"); |
| 34 | |
| 35 | state.currentProcess = { |
| 36 | id: "proc-dead", |
| 37 | status: "failed", |
| 38 | } as unknown as RunExecutionContextState["currentProcess"]; |
| 39 | |
| 40 | expect(getLiveCurrentProcess(state)).toBeNull(); |
| 41 | }); |
| 42 | |
| 43 | it("soft-cancels the current process and the full tree", async () => { |
| 44 | vi.spyOn(console, "log").mockImplementation(() => {}); |
| 45 | const scope = makeScope(); |
| 46 | const state = makeState(); |
| 47 | const session = { |
| 48 | killProcess: vi.fn(async () => {}), |
| 49 | killAllProcesses: vi.fn(async () => {}), |
| 50 | }; |
| 51 | |
| 52 | await softCancelProcessTree(scope, state, session as never, { id: "proc-1", status: "running" } as never); |
| 53 | |
| 54 | expect(session.killProcess).toHaveBeenCalledWith("proc-1", "SIGTERM"); |
| 55 | expect(session.killAllProcesses).toHaveBeenCalledTimes(1); |
| 56 | }); |
| 57 | |
| 58 | it("waits for the process tree to stop and cleans completed processes", async () => { |
| 59 | const state = makeState(); |
| 60 | const session = { |
| 61 | listProcesses: vi.fn(async () => []), |
| 62 | cleanupCompletedProcesses: vi.fn(async () => {}), |
| 63 | }; |
| 64 | |
| 65 | await expect(waitForProcessTreeToStop(session as never, 50, 0)).resolves.toBe(true); |
| 66 | expect(session.cleanupCompletedProcesses).toHaveBeenCalledTimes(1); |
| 67 | }); |
| 68 | |
| 69 | it("treats a stopped container as an already-stopped process tree", async () => { |
| 70 | const session = { |
| 71 | listProcesses: vi.fn(async () => { |
| 72 | throw new Error(`Error: The ${CONTAINER_NOT_RUNNING_ERROR_SUBSTRING}, consider calling start()`); |
| 73 | }), |
| 74 | cleanupCompletedProcesses: vi.fn(async () => {}), |
| 75 | }; |
| 76 | |
| 77 | await expect(waitForProcessTreeToStop(session as never, 50, 0)).resolves.toBe(true); |
| 78 | }); |
| 79 | |
| 80 | it("returns false when sandbox destroy fails", async () => { |
| 81 | vi.spyOn(console, "log").mockImplementation(() => {}); |
| 82 | const scope = makeScope(); |
| 83 | const state = makeState(); |
| 84 | const order: string[] = []; |
| 85 | const sandbox = { |
| 86 | setKeepAlive: vi.fn(async () => { |
| 87 | order.push("setKeepAlive"); |
| 88 | }), |
| 89 | destroy: vi.fn(async () => { |
| 90 | order.push("destroy"); |
| 91 | throw new Error("boom"); |
| 92 | }), |
| 93 | }; |
| 94 | |
| 95 | await expect(destroySandbox(scope, state, sandbox)).resolves.toBe(false); |
| 96 | expect(sandbox.setKeepAlive).toHaveBeenCalledWith(false); |
| 97 | expect(order).toEqual(["setKeepAlive", "destroy"]); |
| 98 | }); |
| 99 | |
| 100 | it("treats a missing container instance as already destroyed", async () => { |
| 101 | vi.spyOn(console, "log").mockImplementation(() => {}); |
| 102 | const scope = makeScope(); |
| 103 | const state = makeState(); |
| 104 | const sandbox = { |
| 105 | setKeepAlive: vi.fn(async () => {}), |
| 106 | destroy: vi.fn(async () => { |
| 107 | throw new Error(`${NO_CONTAINER_INSTANCE_ERROR_SUBSTRING} that can be provided to this durable object`); |
| 108 | }), |
| 109 | }; |
| 110 | |
| 111 | await expect(destroySandbox(scope, state, sandbox)).resolves.toBe(true); |
| 112 | }); |
| 113 | }); |