

























@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
22import * as os from "node:os";
33import * as path from "node:path";
44import { describe, expect, it } from "vitest";
5+import { WebSocketServer } from "ws";
56import { loadCodexSupervisorEndpoints, resolveCodexSupervisorPluginConfig } from "./config.js";
67import { connectCodexAppServerEndpoint, resolveSafeApprovalResult } from "./json-rpc-client.js";
78import { CodexSupervisor } from "./supervisor.js";
@@ -350,6 +351,7 @@ describe("CodexSupervisor", () => {
350351]);
351352expect(fake.calls.find((call) => call.method === "thread/list")?.params).toMatchObject({
352353sourceKinds: ["cli", "vscode", "exec", "appServer", "unknown"],
354+useStateDbOnly: true,
353355});
354356});
355357@@ -394,6 +396,51 @@ describe("CodexSupervisor", () => {
394396]);
395397});
396398399+it("bounds stored session pagination for large real Codex homes", async () => {
400+const fake = new FakeCodexConnection({
401+id: "thread-1",
402+status: { type: "idle" },
403+turns: [],
404+});
405+fake.request = async (method, params) => {
406+fake.calls.push({ method, params });
407+if (method === "thread/loaded/list") {
408+return { data: [], nextCursor: null };
409+}
410+if (method === "thread/list") {
411+return {
412+data: [
413+{ id: "thread-1", status: { type: "notLoaded" }, turns: [] },
414+{ id: "thread-2", status: { type: "notLoaded" }, turns: [] },
415+],
416+nextCursor: "page-2",
417+};
418+}
419+throw new Error(`unexpected method: ${method}`);
420+};
421+const supervisor = new CodexSupervisor([endpoint], async () => fake);
422+423+await expect(
424+supervisor.listSessions({ includeStored: true, maxStoredSessions: 1 }),
425+).resolves.toEqual([
426+{
427+endpointId: "local",
428+threadId: "thread-1",
429+status: "notLoaded",
430+},
431+]);
432+expect(fake.calls.filter((call) => call.method === "thread/list")).toEqual([
433+{
434+method: "thread/list",
435+params: {
436+limit: 1,
437+sourceKinds: ["cli", "vscode", "exec", "appServer", "unknown"],
438+useStateDbOnly: true,
439+},
440+},
441+]);
442+});
443+397444it("closes settled connections when evicting them", async () => {
398445const fake = new FakeCodexConnection({
399446id: "thread-1",
@@ -508,6 +555,88 @@ describe("CodexSupervisor", () => {
508555});
509556});
510557558+it("uses a unique loaded endpoint match even when another endpoint is down", async () => {
559+const upEndpoint: CodexSupervisorEndpoint = { id: "up", transport: "stdio-proxy" };
560+const downEndpoint: CodexSupervisorEndpoint = { id: "down", transport: "stdio-proxy" };
561+const fake = new FakeCodexConnection({
562+id: "thread-1",
563+status: { type: "idle" },
564+turns: [],
565+});
566+const supervisor = new CodexSupervisor([upEndpoint, downEndpoint], async (target) => {
567+if (target.id === "down") {
568+throw new Error("host offline");
569+}
570+return fake;
571+});
572+573+await expect(
574+supervisor.sendToSession({ threadId: "thread-1", text: "continue" }),
575+).resolves.toMatchObject({
576+endpointId: "up",
577+threadId: "thread-1",
578+mode: "start",
579+});
580+});
581+582+it("resolves omitted endpoint ids by exact thread read without scanning stored pages", async () => {
583+const fake = new FakeCodexConnection({
584+id: "thread-old",
585+status: { type: "notLoaded" },
586+turns: [],
587+});
588+fake.request = async (method, params) => {
589+fake.calls.push({ method, params });
590+if (method === "thread/loaded/list") {
591+return { data: [], nextCursor: null };
592+}
593+if (method === "thread/read" && params?.threadId === "thread-old") {
594+return { thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] } };
595+}
596+throw new Error(`unexpected method: ${method}`);
597+};
598+const supervisor = new CodexSupervisor([endpoint], async () => fake);
599+600+await expect(supervisor.readSession({ threadId: "thread-old" })).resolves.toEqual({
601+thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] },
602+});
603+expect(fake.calls.map((call) => call.method)).toEqual([
604+"thread/loaded/list",
605+"thread/read",
606+"thread/read",
607+]);
608+});
609+610+it("resolves stored threads on healthy endpoints when another endpoint is down", async () => {
611+const downEndpoint: CodexSupervisorEndpoint = { id: "down", transport: "stdio-proxy" };
612+const upEndpoint: CodexSupervisorEndpoint = { id: "up", transport: "stdio-proxy" };
613+const fake = new FakeCodexConnection({
614+id: "thread-old",
615+status: { type: "notLoaded" },
616+turns: [],
617+});
618+fake.request = async (method, params) => {
619+fake.calls.push({ method, params });
620+if (method === "thread/loaded/list") {
621+return { data: [], nextCursor: null };
622+}
623+if (method === "thread/read" && params?.threadId === "thread-old") {
624+return { thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] } };
625+}
626+throw new Error(`unexpected method: ${method}`);
627+};
628+const supervisor = new CodexSupervisor([downEndpoint, upEndpoint], async (target) => {
629+if (target.id === "down") {
630+throw new Error("host offline");
631+}
632+return fake;
633+});
634+635+await expect(supervisor.readSession({ threadId: "thread-old" })).resolves.toEqual({
636+thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] },
637+});
638+});
639+511640it("steers active sessions when the in-progress turn is readable", async () => {
512641const fake = new FakeCodexConnection({
513642id: "thread-1",
@@ -668,6 +797,45 @@ async function waitForFile(filePath: string): Promise<string> {
668797}
669798670799describe("connectCodexAppServerEndpoint", () => {
800+it("rejects pending websocket requests when the supervisor closes intentionally", async () => {
801+const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
802+const port = await new Promise<number>((resolve) => {
803+server.once("listening", () => {
804+const address = server.address();
805+resolve(typeof address === "object" && address ? address.port : 0);
806+});
807+});
808+const sawProbeRequest = new Promise<void>((resolve) => {
809+server.once("connection", (socket) => {
810+socket.on("message", (data) => {
811+const request = JSON.parse(data.toString()) as Record<string, unknown>;
812+if (request.method === "initialize") {
813+socket.send(JSON.stringify({ id: request.id, result: {} }));
814+}
815+if (request.method === "thread/loaded/list") {
816+resolve();
817+}
818+});
819+});
820+});
821+const supervisor = new CodexSupervisor(
822+[{ id: "ws", transport: "websocket", url: `ws://127.0.0.1:${port}` }],
823+connectCodexAppServerEndpoint,
824+);
825+826+const probe = supervisor.probeEndpoints();
827+await sawProbeRequest;
828+await supervisor.close();
829+830+await expect(
831+Promise.race([
832+probe,
833+new Promise((_, reject) => setTimeout(() => reject(new Error("probe timed out")), 500)),
834+]),
835+).resolves.toMatchObject([{ endpointId: "ws", ok: false }]);
836+await new Promise<void>((resolve) => server.close(() => resolve()));
837+});
838+671839it("rejects malformed stdio frames instead of throwing out of band", async () => {
672840const markerDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-supervisor-malformed-"));
673841const marker = path.join(markerDir, "closed");
@@ -735,7 +903,7 @@ describe("connectCodexAppServerEndpoint", () => {
735903 process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n");
736904 return;
737905 }
738- if (request.method === "thread/list") {
906+ if (request.method === "thread/loaded/list") {
739907 process.stdout.write(JSON.stringify({ id: request.id, result: { threads: [] } }) + "\\n");
740908 setTimeout(() => process.exit(0), 0);
741909 }
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。