libretto 0.6.10 → 0.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +4 -0
  2. package/README.template.md +4 -0
  3. package/dist/cli/cli.js +4 -3
  4. package/dist/cli/commands/ai.js +3 -2
  5. package/dist/cli/commands/browser.js +17 -17
  6. package/dist/cli/commands/execution.js +254 -234
  7. package/dist/cli/commands/experiments.js +100 -0
  8. package/dist/cli/commands/setup.js +20 -34
  9. package/dist/cli/commands/shared.js +10 -0
  10. package/dist/cli/commands/snapshot.js +81 -9
  11. package/dist/cli/commands/status.js +5 -4
  12. package/dist/cli/core/ai-model.js +6 -3
  13. package/dist/cli/core/browser.js +300 -121
  14. package/dist/cli/core/config.js +4 -2
  15. package/dist/cli/core/context.js +4 -0
  16. package/dist/cli/core/daemon/config.js +0 -6
  17. package/dist/cli/core/daemon/daemon.js +535 -89
  18. package/dist/cli/core/daemon/ipc.js +170 -129
  19. package/dist/cli/core/daemon/snapshot.js +72 -6
  20. package/dist/cli/core/experiments.js +66 -0
  21. package/dist/cli/core/session.js +5 -4
  22. package/dist/cli/core/skill-version.js +2 -1
  23. package/dist/cli/core/snapshot-analyzer.js +4 -3
  24. package/dist/cli/core/workflow-runner/runner.js +147 -0
  25. package/dist/cli/core/workflow-runtime.js +60 -0
  26. package/dist/cli/router.js +4 -1
  27. package/dist/shared/debug/pause-handler.d.ts +9 -0
  28. package/dist/shared/debug/pause-handler.js +15 -0
  29. package/dist/shared/debug/pause.d.ts +1 -2
  30. package/dist/shared/debug/pause.js +13 -36
  31. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  32. package/dist/shared/ipc/child-process-transport.js +60 -0
  33. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  34. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  35. package/dist/shared/ipc/ipc.d.ts +46 -0
  36. package/dist/shared/ipc/ipc.js +165 -0
  37. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  38. package/dist/shared/ipc/ipc.spec.js +114 -0
  39. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  40. package/dist/shared/ipc/socket-transport.js +143 -0
  41. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  42. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  43. package/dist/shared/package-manager.d.ts +7 -0
  44. package/dist/shared/package-manager.js +60 -0
  45. package/dist/shared/paths/paths.d.ts +1 -8
  46. package/dist/shared/paths/paths.js +1 -49
  47. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  48. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  49. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  50. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  51. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  52. package/dist/shared/snapshot/render-snapshot.js +651 -0
  53. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  54. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  55. package/dist/shared/snapshot/types.d.ts +40 -0
  56. package/dist/shared/snapshot/types.js +0 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  58. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  59. package/dist/shared/state/session-state.d.ts +1 -0
  60. package/dist/shared/state/session-state.js +1 -0
  61. package/docs/experiments.md +67 -0
  62. package/package.json +4 -2
  63. package/skills/libretto/SKILL.md +3 -1
  64. package/skills/libretto-readonly/SKILL.md +1 -1
  65. package/src/cli/AGENTS.md +7 -0
  66. package/src/cli/cli.ts +4 -3
  67. package/src/cli/commands/ai.ts +3 -2
  68. package/src/cli/commands/browser.ts +13 -11
  69. package/src/cli/commands/execution.ts +303 -271
  70. package/src/cli/commands/experiments.ts +120 -0
  71. package/src/cli/commands/setup.ts +18 -36
  72. package/src/cli/commands/shared.ts +20 -0
  73. package/src/cli/commands/snapshot.ts +99 -11
  74. package/src/cli/commands/status.ts +5 -4
  75. package/src/cli/core/ai-model.ts +6 -3
  76. package/src/cli/core/browser.ts +369 -147
  77. package/src/cli/core/config.ts +3 -1
  78. package/src/cli/core/context.ts +4 -0
  79. package/src/cli/core/daemon/config.ts +35 -19
  80. package/src/cli/core/daemon/daemon.ts +686 -106
  81. package/src/cli/core/daemon/ipc.ts +330 -214
  82. package/src/cli/core/daemon/snapshot.ts +106 -8
  83. package/src/cli/core/experiments.ts +85 -0
  84. package/src/cli/core/session.ts +5 -4
  85. package/src/cli/core/skill-version.ts +2 -1
  86. package/src/cli/core/snapshot-analyzer.ts +4 -3
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +85 -0
  89. package/src/cli/router.ts +4 -1
  90. package/src/shared/debug/pause-handler.ts +20 -0
  91. package/src/shared/debug/pause.ts +14 -48
  92. package/src/shared/ipc/AGENTS.md +24 -0
  93. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  94. package/src/shared/ipc/child-process-transport.ts +96 -0
  95. package/src/shared/ipc/ipc.spec.ts +161 -0
  96. package/src/shared/ipc/ipc.ts +288 -0
  97. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  98. package/src/shared/ipc/socket-transport.ts +189 -0
  99. package/src/shared/package-manager.ts +76 -0
  100. package/src/shared/paths/paths.ts +0 -72
  101. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  102. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  103. package/src/shared/snapshot/render-snapshot.ts +962 -0
  104. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  105. package/src/shared/snapshot/types.ts +43 -0
  106. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  107. package/src/shared/state/session-state.ts +1 -0
  108. package/dist/cli/core/daemon/index.js +0 -16
  109. package/dist/cli/core/daemon/spawn.js +0 -90
  110. package/dist/cli/core/pause-signals.js +0 -29
  111. package/dist/cli/workers/run-integration-runtime.js +0 -235
  112. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  113. package/dist/cli/workers/run-integration-worker.js +0 -64
  114. package/src/cli/core/daemon/index.ts +0 -24
  115. package/src/cli/core/daemon/spawn.ts +0 -171
  116. package/src/cli/core/pause-signals.ts +0 -35
  117. package/src/cli/workers/run-integration-runtime.ts +0 -326
  118. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  119. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -0,0 +1,20 @@
1
+ export type ActivePauseHandler = (args: {
2
+ session: string;
3
+ pausedAt: string;
4
+ url?: string;
5
+ }) => Promise<void>;
6
+
7
+ let activePauseHandler: ActivePauseHandler | undefined;
8
+
9
+ export function installPauseHandler(handler: ActivePauseHandler): () => void {
10
+ const previousHandler = activePauseHandler;
11
+ activePauseHandler = handler;
12
+
13
+ return () => {
14
+ activePauseHandler = previousHandler;
15
+ };
16
+ }
17
+
18
+ export function getActivePauseHandler(): ActivePauseHandler | undefined {
19
+ return activePauseHandler;
20
+ }
@@ -1,32 +1,17 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import { getSessionDir } from "../../cli/core/context.js";
4
- import {
5
- getPauseSignalPaths,
6
- removeSignalIfExists,
7
- } from "../../cli/core/pause-signals.js";
8
- import { listRunningSessions } from "../../cli/core/session.js";
1
+ import { getActivePauseHandler } from "./pause-handler.js";
2
+ import { librettoCommand } from "../package-manager.js";
9
3
 
10
4
  function throwMissingSessionError(): never {
11
- const runningSessions = listRunningSessions();
12
- const lines = ["pause(session) requires a non-empty session ID."];
13
-
14
- if (runningSessions.length > 0) {
15
- lines.push("", "Running sessions:");
16
- for (const s of runningSessions) {
17
- lines.push(` ${s.session}`);
18
- }
19
- }
20
-
21
- throw new Error(lines.join("\n"));
5
+ throw new Error(
6
+ `pause(session) requires a non-empty session ID. Pass ctx.session from inside your workflow: await pause(ctx.session). To list running sessions, run: ${librettoCommand("status")}.`,
7
+ );
22
8
  }
23
9
 
24
10
  /**
25
11
  * Standalone pause function.
26
12
  *
27
13
  * - In production (`NODE_ENV === "production"`), returns immediately (no-op).
28
- * - Otherwise, writes a `.paused` signal file and polls for a `.resume` signal,
29
- * using the same file-based mechanism as the CLI runner.
14
+ * - Otherwise, delegates to the active Libretto workflow runtime pause handler.
30
15
  *
31
16
  * Import directly: `import { pause } from "libretto";`
32
17
  */
@@ -39,34 +24,15 @@ export async function pause(session: string): Promise<void> {
39
24
  throwMissingSessionError();
40
25
  }
41
26
 
42
- const signalPaths = getPauseSignalPaths(session);
43
- const { pausedSignalPath, resumeSignalPath } = signalPaths;
44
-
45
- await mkdir(getSessionDir(session), { recursive: true });
46
- await removeSignalIfExists(resumeSignalPath);
47
-
48
- const details = {
49
- sessionName: session,
50
- pausedAt: new Date().toISOString(),
51
- url: "unknown",
52
- };
53
-
54
- // Try to read the current page URL from the process (best-effort).
55
- // The standalone pause doesn't have access to the page object,
56
- // so we just record what we can.
57
- await writeFile(pausedSignalPath, JSON.stringify(details, null, 2), "utf8");
58
-
59
- console.log(`[pause] Paused (session: ${session})`);
60
- console.log("[pause] Waiting for resume signal...");
61
-
62
- const RESUME_POLL_INTERVAL_MS = 250;
63
- while (!existsSync(resumeSignalPath)) {
64
- await new Promise((resolve) =>
65
- setTimeout(resolve, RESUME_POLL_INTERVAL_MS),
27
+ const handler = getActivePauseHandler();
28
+ if (!handler) {
29
+ throw new Error(
30
+ `pause(session) can only suspend an active Libretto workflow. Run the workflow with ${librettoCommand("run <integrationFile>")} and call pause(ctx.session) from inside the workflow.`,
66
31
  );
67
32
  }
68
33
 
69
- await removeSignalIfExists(resumeSignalPath);
70
- await removeSignalIfExists(pausedSignalPath);
71
- console.log("[pause] Resume signal received. Continuing workflow...");
34
+ await handler({
35
+ session,
36
+ pausedAt: new Date().toISOString(),
37
+ });
72
38
  }
@@ -0,0 +1,24 @@
1
+ # Shared IPC
2
+
3
+ This directory contains the generic typed IPC peer used across socket and child-process boundaries. Keep it transport-agnostic and avoid daemon-, workflow-, or CLI-specific behavior here.
4
+
5
+ ## IpcPeer model
6
+
7
+ `createIpcPeer<Remote, Local>(transport, handlers)` creates one bidirectional peer:
8
+
9
+ - `Remote` is the API this side can call through `peer.call.*`.
10
+ - `Local` is the API this side exposes through `handlers`.
11
+ - Both peers on a connection use opposite generic ordering.
12
+ - Calls are request/response RPCs over `IpcProtocolMessage`; concurrent calls are supported.
13
+
14
+ ## Transports
15
+
16
+ Transports implement `IpcTransport<IpcProtocolMessage>` and are responsible only for delivery, framing, close notification, and cleanup. Use `socket-transport.ts` for Unix socket connections and `child-process-transport.ts` for Node child-process IPC.
17
+
18
+ When adding a transport, filter incoming messages to valid IPC protocol messages before passing them to `createIpcPeer`, and wire close/error events through `onClose` so pending calls reject when the connection dies.
19
+
20
+ ## Error behavior
21
+
22
+ `createIpcPeer` serializes handler throws into rejected call promises. Callers should usually let those rejections propagate instead of wrapping them in subsystem-specific error adapters.
23
+
24
+ Use explicit result shapes only when failure is part of the method contract. For example, daemon `exec` and `readonlyExec` return user-code failures as `{ ok: false, message, output }` so CLI callers can preserve captured stdout and stderr.
@@ -0,0 +1,86 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { fileURLToPath } from "node:url";
4
+ import { expect, test } from "vitest";
5
+ import { createIpcPeer } from "./ipc.js";
6
+ import { createChildProcessIpcTransport } from "./child-process-transport.js";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ type ParentApi = {
11
+ greet(name: string): string;
12
+ };
13
+
14
+ type ChildApi = {
15
+ double(value: number): number;
16
+ askParent(name: string): Promise<string>;
17
+ };
18
+
19
+ test("throws a clear error without a child-process IPC channel", async () => {
20
+ const modulePath = fileURLToPath(
21
+ new URL("./child-process-transport.ts", import.meta.url),
22
+ );
23
+
24
+ await expect(
25
+ execFileAsync(
26
+ process.execPath,
27
+ [
28
+ "--import",
29
+ "tsx",
30
+ "-e",
31
+ `import { createParentProcessIpcTransport } from ${JSON.stringify(modulePath)}; createParentProcessIpcTransport();`,
32
+ ],
33
+ { cwd: process.cwd() },
34
+ ),
35
+ ).rejects.toMatchObject({
36
+ stderr: expect.stringContaining("process.send is not available"),
37
+ });
38
+ });
39
+
40
+ test("lets parent and child process call each other", async () => {
41
+ const child = spawn(
42
+ process.execPath,
43
+ ["--import", "tsx", "--input-type=module", "-e", getChildFixtureSource()],
44
+ { stdio: ["ignore", "ignore", "ignore", "ipc"] },
45
+ );
46
+ child.send({ type: "not-an-ipc-protocol-message" });
47
+
48
+ const peer = createIpcPeer<ChildApi, ParentApi>(
49
+ createChildProcessIpcTransport(child),
50
+ {
51
+ greet(name) {
52
+ return `hello ${name}`;
53
+ },
54
+ },
55
+ );
56
+
57
+ await expect(peer.call.double(21)).resolves.toBe(42);
58
+ await expect(peer.call.askParent("Ada")).resolves.toBe("hello Ada");
59
+
60
+ peer.destroy();
61
+ await new Promise<void>((resolve) => child.once("exit", () => resolve()));
62
+ });
63
+
64
+ function getChildFixtureSource(): string {
65
+ const ipcModule = new URL("./ipc.ts", import.meta.url).href;
66
+ const transportModule = new URL(
67
+ "./child-process-transport.ts",
68
+ import.meta.url,
69
+ ).href;
70
+
71
+ return `
72
+ import { createIpcPeer } from ${JSON.stringify(ipcModule)};
73
+ import { createParentProcessIpcTransport } from ${JSON.stringify(transportModule)};
74
+
75
+ const peer = createIpcPeer(createParentProcessIpcTransport(), {
76
+ double(value) {
77
+ return value * 2;
78
+ },
79
+ async askParent(name) {
80
+ return peer.call.greet(name);
81
+ },
82
+ });
83
+
84
+ process.on("disconnect", () => peer.destroy());
85
+ `;
86
+ }
@@ -0,0 +1,96 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import type { IpcProtocolMessage, IpcTransport } from "./ipc.js";
3
+
4
+ type ProcessIpcTarget = {
5
+ send?: (message: IpcProtocolMessage) => boolean;
6
+ on(event: "message", listener: (message: unknown) => void): ProcessIpcTarget;
7
+ off(event: "message", listener: (message: unknown) => void): ProcessIpcTarget;
8
+ on(event: "disconnect", listener: () => void): ProcessIpcTarget;
9
+ off(event: "disconnect", listener: () => void): ProcessIpcTarget;
10
+ };
11
+
12
+ export function createChildProcessIpcTransport(
13
+ child: ChildProcess,
14
+ ): IpcTransport<IpcProtocolMessage> {
15
+ return createProcessIpcTransport(child, () => {
16
+ if (child.connected) child.disconnect();
17
+ });
18
+ }
19
+
20
+ export function createParentProcessIpcTransport(): IpcTransport<IpcProtocolMessage> {
21
+ if (!process.send) {
22
+ throw new Error(
23
+ "Cannot create child-process IPC transport: process.send is not available. Start the process with an IPC channel.",
24
+ );
25
+ }
26
+
27
+ return createProcessIpcTransport(process, () => {
28
+ if (process.connected) process.disconnect?.();
29
+ });
30
+ }
31
+
32
+ function createProcessIpcTransport(
33
+ target: ProcessIpcTarget,
34
+ close: () => void,
35
+ ): IpcTransport<IpcProtocolMessage> {
36
+ return {
37
+ send(message) {
38
+ if (!target.send) {
39
+ throw new Error(
40
+ "Cannot send IPC message: process IPC channel is closed.",
41
+ );
42
+ }
43
+
44
+ target.send(message);
45
+ },
46
+ listen(callback) {
47
+ const onMessage = (message: unknown) => {
48
+ if (isIpcProtocolMessage(message)) callback(message);
49
+ };
50
+
51
+ target.on("message", onMessage);
52
+ return () => target.off("message", onMessage);
53
+ },
54
+ onClose(callback) {
55
+ const onDisconnect = () => callback();
56
+ target.on("disconnect", onDisconnect);
57
+ return () => target.off("disconnect", onDisconnect);
58
+ },
59
+ close,
60
+ };
61
+ }
62
+
63
+ function isIpcProtocolMessage(message: unknown): message is IpcProtocolMessage {
64
+ if (!isRecord(message)) return false;
65
+
66
+ if (message.type === "ipc-request") {
67
+ return (
68
+ typeof message.id === "string" &&
69
+ typeof message.method === "string" &&
70
+ Array.isArray(message.args)
71
+ );
72
+ }
73
+
74
+ if (message.type === "ipc-response") {
75
+ return (
76
+ typeof message.id === "string" &&
77
+ typeof message.method === "string" &&
78
+ (message.error === undefined || isSerializedError(message.error))
79
+ );
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ function isSerializedError(value: unknown): boolean {
86
+ return (
87
+ isRecord(value) &&
88
+ typeof value.name === "string" &&
89
+ typeof value.message === "string" &&
90
+ (value.stack === undefined || typeof value.stack === "string")
91
+ );
92
+ }
93
+
94
+ function isRecord(value: unknown): value is Record<string, unknown> {
95
+ return typeof value === "object" && value !== null;
96
+ }
@@ -0,0 +1,161 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { expect, test as base } from "vitest";
3
+ import {
4
+ createIpcPeer,
5
+ type IpcPeer,
6
+ type IpcProtocolMessage,
7
+ } from "./ipc.js";
8
+
9
+ type ApiA = {
10
+ greet(name: string): string;
11
+ };
12
+
13
+ type ApiB = {
14
+ add(left: number, right: number): Promise<number>;
15
+ fail(): Promise<void>;
16
+ failWithCause(): Promise<void>;
17
+ failWithCode(): Promise<void>;
18
+ failWithNonError(): Promise<void>;
19
+ wait(): Promise<string>;
20
+ };
21
+
22
+ type Peers = {
23
+ a: IpcPeer<ApiB>;
24
+ b: IpcPeer<ApiA>;
25
+ };
26
+
27
+ type Fixtures = Peers & {
28
+ peers: Peers;
29
+ };
30
+
31
+ const test = base.extend<Fixtures>({
32
+ peers: async ({}, use) => {
33
+ const channel = new EventEmitter<{
34
+ a: [IpcProtocolMessage];
35
+ b: [IpcProtocolMessage];
36
+ }>();
37
+ const a = createIpcPeer<ApiB, ApiA>({
38
+ send(message) {
39
+ channel.emit("b", message);
40
+ },
41
+ listen(callback) {
42
+ channel.on("a", callback);
43
+ return () => channel.off("a", callback);
44
+ },
45
+ }, {
46
+ greet(name) {
47
+ return `hello ${name}`;
48
+ },
49
+ });
50
+ const b = createIpcPeer<ApiA, ApiB>({
51
+ send(message) {
52
+ channel.emit("a", message);
53
+ },
54
+ listen(callback) {
55
+ channel.on("b", callback);
56
+ return () => channel.off("b", callback);
57
+ },
58
+ }, {
59
+ async add(left, right) {
60
+ return left + right;
61
+ },
62
+ async fail() {
63
+ throw new Error("expected failure");
64
+ },
65
+ async failWithCause() {
66
+ throw new Error("outer failure", {
67
+ cause: new Error("inner failure", {
68
+ cause: new TypeError("root failure"),
69
+ }),
70
+ });
71
+ },
72
+ async failWithCode() {
73
+ const error = new Error("coded failure") as Error & { code: string };
74
+ error.code = "ERR_EXPECTED";
75
+ throw error;
76
+ },
77
+ async failWithNonError() {
78
+ throw "plain failure";
79
+ },
80
+ async wait() {
81
+ return new Promise(() => {});
82
+ },
83
+ });
84
+
85
+ await use({ a, b });
86
+
87
+ a.destroy();
88
+ b.destroy();
89
+ },
90
+ a: async ({ peers }, use) => {
91
+ await use(peers.a);
92
+ },
93
+ b: async ({ peers }, use) => {
94
+ await use(peers.b);
95
+ },
96
+ });
97
+
98
+ test("calls handlers on the remote peer", async ({ a, b }) => {
99
+ await expect(a.call.add(2, 3)).resolves.toBe(5);
100
+ await expect(b.call.greet("Ada")).resolves.toBe("hello Ada");
101
+ });
102
+
103
+ test("rejects with the remote handler error", async ({ a }) => {
104
+ await expect(a.call.fail()).rejects.toThrow("fail > expected failure");
105
+ });
106
+
107
+ test("rejects with the remote handler error cause chain", async ({ a }) => {
108
+ const error = await getRejectedError(a.call.failWithCause());
109
+
110
+ expect(error.message).toBe("failWithCause > outer failure");
111
+ expect(error.stack).toContain("outer failure");
112
+
113
+ const cause = getErrorCause(error);
114
+ expect(cause.message).toBe("inner failure");
115
+
116
+ const rootCause = getErrorCause(cause);
117
+ expect(rootCause.name).toBe("TypeError");
118
+ expect(rootCause.message).toBe("root failure");
119
+ });
120
+
121
+ test("rejects with the remote handler error code", async ({ a }) => {
122
+ const error = await getRejectedError(a.call.failWithCode());
123
+
124
+ expect(error.message).toBe("failWithCode > coded failure");
125
+ expect(error.code).toBe("ERR_EXPECTED");
126
+ expect(error.stack).toContain("coded failure");
127
+ });
128
+
129
+ test("rejects with a serialized non-error thrown value", async ({ a }) => {
130
+ const error = await getRejectedError(a.call.failWithNonError());
131
+
132
+ expect(error.name).toBe("NonError");
133
+ expect(error.message).toBe("failWithNonError > plain failure");
134
+ expect(error.stack).toContain("failWithNonError");
135
+ });
136
+
137
+ test("rejects pending calls when destroyed", async ({ a }) => {
138
+ const result = a.call.wait();
139
+
140
+ a.destroy();
141
+
142
+ await expect(result).rejects.toThrow("IPC peer destroyed");
143
+ });
144
+
145
+ async function getRejectedError(
146
+ promise: Promise<unknown>,
147
+ ): Promise<Error & { code?: unknown }> {
148
+ try {
149
+ await promise;
150
+ } catch (error) {
151
+ expect(error).toBeInstanceOf(Error);
152
+ return error as Error & { code?: unknown };
153
+ }
154
+
155
+ throw new Error("Expected promise to reject");
156
+ }
157
+
158
+ function getErrorCause(error: Error): Error {
159
+ expect(error.cause).toBeInstanceOf(Error);
160
+ return error.cause as Error;
161
+ }