File size: 3,518 Bytes
5cb63c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { afterEach, expect, it, vi } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import { createProcessSupervisor } from "./supervisor.js";
import { createStubChildAdapter } from "./supervisor.test-support.js";

const { createChild } = vi.hoisted(() => ({
  createChild: vi.fn<typeof import("./adapters/child.js").createChildAdapter>(),
}));
vi.mock("./adapters/child.js", () => ({ createChildAdapter: createChild }));
afterEach(() => {
  createChild.mockReset();
  vi.useRealTimers();
});

it.each(["manual-cancel", "overall-timeout", "no-output-timeout"] as const)(
  "keeps %s construction cancellation active after subscribing to output",
  async (reason) => {
    vi.useFakeTimers();
    const ready = createDeferred();
    const cleanup = createDeferred();
    const adapter = createStubChildAdapter();
    const subscribed = createDeferred();
    const subscribe = adapter.onStdout;
    adapter.onStdout = (...args) => {
      subscribe(...args);
      subscribed.resolve();
    };
    const abort = vi.fn();
    createChild.mockImplementationOnce(async (input) => {
      input.onSpawnCleanup?.(cleanup.promise);
      input.abortSignal?.addEventListener("abort", abort, { once: true });
      return { adapter: { ...adapter, onExit: vi.fn(), onError: vi.fn() }, ready: ready.promise };
    });
    const supervisor = createProcessSupervisor();
    const starting = supervisor.spawn({
      mode: "child",
      argv: ["synthetic-child"],
      runId: "pending-private-input",
      ...(reason === "overall-timeout" ? { timeoutMs: 10 } : {}),
      ...(reason === "no-output-timeout" ? { noOutputTimeoutMs: 10 } : {}),
    });
    await subscribed.promise;
    adapter.emitStdout("early");
    if (reason === "manual-cancel") {
      supervisor.cancel("pending-private-input");
    } else {
      await vi.advanceTimersByTimeAsync(10);
    }
    const run = await starting;
    await expect(run.wait()).resolves.toMatchObject({ reason, stdout: "early" });
    expect(abort).toHaveBeenCalledOnce();
    cleanup.resolve();
    const closed = vi.fn();
    const shutdown = supervisor.shutdown().then(closed);
    await Promise.resolve();
    expect(closed).not.toHaveBeenCalled();
    expect(adapter.disposeMock).not.toHaveBeenCalled();
    ready.reject(new Error("private input aborted"));
    await shutdown;
    expect(closed).toHaveBeenCalledOnce();
    expect(adapter.disposeMock).toHaveBeenCalledOnce();
  },
);

it("preserves a readiness error while retaining a failed cleanup owner", async () => {
  const ready = createDeferred();
  const cleanup = createDeferred();
  const subscribed = createDeferred();
  const adapter = createStubChildAdapter();
  adapter.onStdout = () => subscribed.resolve();
  createChild.mockImplementationOnce(async (input) => {
    input.onSpawnCleanup?.(cleanup.promise);
    return { adapter: { ...adapter, onExit: vi.fn(), onError: vi.fn() }, ready: ready.promise };
  });
  const supervisor = createProcessSupervisor();
  const starting = supervisor.spawn({ mode: "child", argv: ["synthetic-child"] });
  const startupFailure = expect(starting).rejects.toThrow("private input failed");
  await subscribed.promise;
  ready.reject(new Error("private input failed"));
  await startupFailure;
  expect(adapter.disposeMock).not.toHaveBeenCalled();
  cleanup.reject(new Error("cleanup identity lost"));
  await expect(supervisor.shutdown()).rejects.toThrow("cleanup identity lost");
  expect(adapter.disposeMock).toHaveBeenCalledOnce();
});