


























1-import { statSync, writeFileSync } from "node:fs";
1+import { randomUUID } from "node:crypto";
2+import { rmSync, statSync, writeFileSync } from "node:fs";
23import fs from "node:fs/promises";
34import { createServer, request as httpRequest } from "node:http";
45import { tmpdir } from "node:os";
@@ -24,6 +25,7 @@ import {
24252526afterEach(() => {
2627vi.useRealTimers();
28+vi.restoreAllMocks();
2729resetGlobalHookRunner();
2830setActivePluginRegistry(createEmptyPluginRegistry());
2931testing.clearNativeHookRelaysForTests();
@@ -80,6 +82,36 @@ async function waitForNativeHookRelayBridgeRecord(
8082return record as Record<string, unknown>;
8183}
828485+async function writeForeignNativeHookRelayBridgeRecordForTests(
86+relayId: string,
87+record: {
88+pid: number;
89+expiresAtMs: number;
90+},
91+): Promise<string> {
92+const bridgeDir = testing.getNativeHookRelayBridgeDirForTests();
93+await fs.mkdir(bridgeDir, { recursive: true, mode: 0o700 });
94+const registryPath = testing.getNativeHookRelayBridgeRegistryPathForTests(relayId);
95+writeFileSync(
96+registryPath,
97+`${JSON.stringify({
98+ version: 1,
99+ relayId,
100+ pid: record.pid,
101+ hostname: "127.0.0.1",
102+ port: 9,
103+ token: `token-${relayId}`,
104+ expiresAtMs: record.expiresAtMs,
105+ })}\n`,
106+{ mode: 0o600 },
107+);
108+return registryPath;
109+}
110+111+function uniqueNativeHookRelayIdForTests(prefix: string): string {
112+return `${prefix}-${randomUUID()}`;
113+}
114+83115function openDeferredNativeHookRelayBridgeRequest(
84116record: Record<string, unknown>,
85117payload: Record<string, unknown>,
@@ -852,6 +884,122 @@ describe("native hook relay registry", () => {
852884expect(response).toEqual({ stdout: "", stderr: "", exitCode: 0 });
853885});
854886887+it("prunes dead foreign direct bridge registry files during registration", async () => {
888+const stalePath = await writeForeignNativeHookRelayBridgeRecordForTests(
889+uniqueNativeHookRelayIdForTests("codex-dead-foreign-bridge"),
890+{
891+pid: 9_999_991,
892+expiresAtMs: Date.now() + 60_000,
893+},
894+);
895+const kill = vi.spyOn(process, "kill").mockImplementation((pid) => {
896+if (pid === 9_999_991) {
897+throw Object.assign(new Error("missing process"), { code: "ESRCH" });
898+}
899+return true;
900+});
901+902+registerNativeHookRelay({
903+provider: "codex",
904+relayId: "codex-prune-dead-foreign-bridge-session",
905+sessionId: "session-1",
906+runId: "run-1",
907+allowedEvents: ["pre_tool_use"],
908+});
909+910+expect(kill).toHaveBeenCalledWith(9_999_991, 0);
911+await expect(fs.stat(stalePath)).rejects.toMatchObject({ code: "ENOENT" });
912+});
913+914+it("prunes expired foreign direct bridge registry files even when their pid is alive", async () => {
915+const stalePath = await writeForeignNativeHookRelayBridgeRecordForTests(
916+uniqueNativeHookRelayIdForTests("codex-expired-foreign-bridge"),
917+{
918+pid: 9_999_992,
919+expiresAtMs: Date.now() - 1,
920+},
921+);
922+const kill = vi.spyOn(process, "kill").mockImplementation((pid) => {
923+if (pid !== 9_999_992) {
924+throw Object.assign(new Error("unexpected process"), { code: "ESRCH" });
925+}
926+return true;
927+});
928+929+registerNativeHookRelay({
930+provider: "codex",
931+relayId: "codex-prune-expired-foreign-bridge-session",
932+sessionId: "session-1",
933+runId: "run-1",
934+allowedEvents: ["pre_tool_use"],
935+});
936+937+expect(kill).not.toHaveBeenCalled();
938+await expect(fs.stat(stalePath)).rejects.toMatchObject({ code: "ENOENT" });
939+});
940+941+it("preserves live unexpired foreign direct bridge registry files during registration", async () => {
942+const livePath = await writeForeignNativeHookRelayBridgeRecordForTests(
943+uniqueNativeHookRelayIdForTests("codex-live-foreign-bridge"),
944+{
945+pid: 9_999_993,
946+expiresAtMs: Date.now() + 60_000,
947+},
948+);
949+const kill = vi.spyOn(process, "kill").mockImplementation((pid) => {
950+if (pid !== 9_999_993) {
951+throw Object.assign(new Error("unexpected process"), { code: "ESRCH" });
952+}
953+return true;
954+});
955+956+try {
957+registerNativeHookRelay({
958+provider: "codex",
959+relayId: "codex-preserve-live-foreign-bridge-session",
960+sessionId: "session-1",
961+runId: "run-1",
962+allowedEvents: ["pre_tool_use"],
963+});
964+965+expect(kill).toHaveBeenCalledWith(9_999_993, 0);
966+await expect(fs.stat(livePath)).resolves.toBeDefined();
967+} finally {
968+rmSync(livePath, { force: true });
969+}
970+});
971+972+it("preserves foreign direct bridge registry files when liveness is unknown", async () => {
973+const livePath = await writeForeignNativeHookRelayBridgeRecordForTests(
974+uniqueNativeHookRelayIdForTests("codex-unknown-liveness-foreign-bridge"),
975+{
976+pid: 9_999_994,
977+expiresAtMs: Date.now() + 60_000,
978+},
979+);
980+const kill = vi.spyOn(process, "kill").mockImplementation((pid) => {
981+if (pid === 9_999_994) {
982+throw Object.assign(new Error("permission denied"), { code: "EPERM" });
983+}
984+return true;
985+});
986+987+try {
988+registerNativeHookRelay({
989+provider: "codex",
990+relayId: "codex-preserve-unknown-liveness-foreign-bridge-session",
991+sessionId: "session-1",
992+runId: "run-1",
993+allowedEvents: ["pre_tool_use"],
994+});
995+996+expect(kill).toHaveBeenCalledWith(9_999_994, 0);
997+await expect(fs.stat(livePath)).resolves.toBeDefined();
998+} finally {
999+rmSync(livePath, { force: true });
1000+}
1001+});
1002+8551003it("keeps direct bridge registry files private and loopback-only", async () => {
8561004const relay = registerNativeHookRelay({
8571005provider: "codex",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。