Skip to content

Commit 41287e7

Browse files
NERLOEclaude
andcommitted
test(core): exercise zodIpc acks over a real forked child
Replace the in-memory JSON channel with a forked child process so the ack goes through Node's actual IPC serialization. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d098714 commit 41287e7

3 files changed

Lines changed: 47 additions & 57 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { z } from "zod/v4";
2+
3+
export const ParentToChild = {
4+
FLUSH: {
5+
message: z.object({ timeoutInMs: z.number() }),
6+
callback: z.void(),
7+
},
8+
PING: {
9+
message: z.object({ value: z.string() }),
10+
callback: z.object({ echoed: z.string() }),
11+
},
12+
};
13+
14+
export const ChildToParent = {};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ZodIpcConnection } from "../../src/v3/zodIpc.js";
2+
import { ChildToParent, ParentToChild } from "./zodIpcCatalog.js";
3+
4+
// Forked by zodIpc.test.ts: answers the parent's messages over the real IPC channel.
5+
new ZodIpcConnection({
6+
listenSchema: ParentToChild,
7+
emitSchema: ChildToParent,
8+
process,
9+
handlers: {
10+
FLUSH: async () => {},
11+
PING: async ({ value }) => ({ echoed: value }),
12+
},
13+
});

‎packages/core/test/zodIpc.test.ts‎

Lines changed: 20 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,43 @@
1-
import { EventEmitter } from "node:events";
2-
import { describe, expect, it } from "vitest";
3-
import { z } from "zod/v4";
1+
import { fork, type ChildProcess } from "node:child_process";
2+
import { fileURLToPath } from "node:url";
3+
import { afterEach, describe, expect, it } from "vitest";
44
import { ZodIpcConnection } from "../src/v3/zodIpc.js";
5+
import { ChildToParent, ParentToChild } from "./fixtures/zodIpcCatalog.js";
56

6-
// A pair of in-memory endpoints that serialize packets the same way Node's default
7-
// ("json") IPC serialization does, so keys with `undefined` values are dropped in transit.
8-
function createIpcPair() {
9-
const parentEvents = new EventEmitter();
10-
const childEvents = new EventEmitter();
7+
const childPath = fileURLToPath(new URL("./fixtures/zodIpcChild.ts", import.meta.url));
118

12-
const endpoint = (inbox: EventEmitter, outbox: EventEmitter) => ({
13-
connected: true,
14-
send: (message: unknown) => {
15-
const serialized = JSON.stringify(message);
16-
setImmediate(() => outbox.emit("message", JSON.parse(serialized)));
17-
return true;
18-
},
19-
on: (event: "message", listener: (message: any) => void) => {
20-
inbox.on(event, listener);
21-
},
22-
});
23-
24-
return {
25-
parent: endpoint(parentEvents, childEvents),
26-
child: endpoint(childEvents, parentEvents),
27-
};
28-
}
29-
30-
const ParentToChild = {
31-
FLUSH: {
32-
message: z.object({ timeoutInMs: z.number() }),
33-
callback: z.void(),
34-
},
35-
PING: {
36-
message: z.object({ value: z.string() }),
37-
callback: z.object({ echoed: z.string() }),
38-
},
39-
};
9+
let child: ChildProcess | undefined;
4010

41-
const ChildToParent = {};
11+
afterEach(() => {
12+
child?.kill();
13+
child = undefined;
14+
});
4215

43-
function createConnections() {
44-
const { parent, child } = createIpcPair();
16+
// Node's default IPC serialization is JSON, so an ack with `message: undefined`
17+
// arrives without the `message` key, exactly as it does between a worker and its task run process.
18+
function forkChild() {
19+
child = fork(childPath, { execArgv: ["--import", "tsx"], stdio: "inherit" });
4520

46-
const parentConnection = new ZodIpcConnection({
21+
return new ZodIpcConnection({
4722
listenSchema: ChildToParent,
4823
emitSchema: ParentToChild,
49-
process: parent,
50-
});
51-
52-
new ZodIpcConnection({
53-
listenSchema: ParentToChild,
54-
emitSchema: ChildToParent,
5524
process: child,
56-
handlers: {
57-
FLUSH: async () => {},
58-
PING: async ({ value }) => ({ echoed: value }),
59-
},
6025
});
61-
62-
return parentConnection;
6326
}
6427

6528
describe("ZodIpcConnection", () => {
66-
it("resolves sendWithAck for a void callback after the ack crosses a JSON boundary", async () => {
67-
const connection = createConnections();
29+
it("resolves sendWithAck for a void callback", async () => {
30+
const connection = forkChild();
6831

69-
await expect(connection.sendWithAck("FLUSH", { timeoutInMs: 1000 }, 1000)).resolves.toBe(
32+
await expect(connection.sendWithAck("FLUSH", { timeoutInMs: 1000 }, 2000)).resolves.toBe(
7033
undefined
7134
);
7235
});
7336

7437
it("resolves sendWithAck with the callback payload", async () => {
75-
const connection = createConnections();
38+
const connection = forkChild();
7639

77-
await expect(connection.sendWithAck("PING", { value: "hello" }, 1000)).resolves.toEqual({
40+
await expect(connection.sendWithAck("PING", { value: "hello" }, 2000)).resolves.toEqual({
7841
echoed: "hello",
7942
});
8043
});

0 commit comments

Comments
 (0)