Skip to content
File

Blob: tests/worker/dispatch/shared/process-tree.test.ts

typescript114 lines
1import { afterEach, describe, expect, it, vi } from "vitest";
2 
3import {
4 destroySandbox,
5 getLiveCurrentProcess,
6 softCancelProcessTree,
7 waitForProcessTreeToStop,
8} from "@/worker/dispatch/shared/run-execution-context/process-tree";
9import {
10 CONTAINER_NOT_RUNNING_ERROR_SUBSTRING,
11 NO_CONTAINER_INSTANCE_ERROR_SUBSTRING,
12} from "@/worker/sandbox/container-errors";
13import type { RunExecutionContextState } from "@/worker/dispatch/shared/run-execution-context/types";
14 
15import { createQueueScope, createQueueState } from "../../../helpers/dispatch/shared";
16 
17const makeScope = () => createQueueScope();
18 
19const makeState = () => createQueueState();
20 
21afterEach(() => {
22 vi.restoreAllMocks();
23});
24 
25describe("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});