


























@@ -1810,6 +1810,162 @@ describe("runCodexAppServerAttempt", () => {
18101810expect(request.mock.calls.map(([method]) => method)).not.toContain("app/list");
18111811});
181218121813+it("retires the shared Codex app-server client after one-shot cleanup turns", async () => {
1814+const retireSpy = vi.spyOn(sharedClientModule, "retireSharedCodexAppServerClientIfCurrent");
1815+retireSpy.mockReturnValue({ activeLeases: 0, closed: true });
1816+const events: string[] = [];
1817+const closeAndWait = vi.fn(async () => {
1818+events.push("closeAndWait");
1819+return true;
1820+});
1821+let startedClient: unknown;
1822+let notify: ((notification: CodexServerNotification) => Promise<void>) | undefined;
1823+setCodexAppServerClientFactoryForTest(async () => {
1824+const client = {
1825+request: vi.fn(async (method: string) => {
1826+events.push(`request:${method}`);
1827+if (method === "thread/start") {
1828+return threadStartResult();
1829+}
1830+if (method === "turn/start") {
1831+return turnStartResult();
1832+}
1833+return {};
1834+}),
1835+addNotificationHandler: vi.fn((handler) => {
1836+notify = handler;
1837+return () => undefined;
1838+}),
1839+addRequestHandler: vi.fn(() => () => undefined),
1840+addCloseHandler: vi.fn(() => () => undefined),
1841+ closeAndWait,
1842+};
1843+startedClient = client;
1844+return client as never;
1845+});
1846+const params = createParams(
1847+path.join(tempDir, "session.jsonl"),
1848+path.join(tempDir, "workspace"),
1849+);
1850+params.cleanupBundleMcpOnRunEnd = true;
1851+1852+const run = runCodexAppServerAttempt(params);
1853+await vi.waitFor(() => expect(notify).toBeDefined(), fastWait);
1854+if (!notify) {
1855+throw new Error("expected turn notification handler");
1856+}
1857+await notify({
1858+method: "turn/completed",
1859+params: {
1860+threadId: "thread-1",
1861+turnId: "turn-1",
1862+turn: { id: "turn-1", status: "completed" },
1863+},
1864+});
1865+await run;
1866+1867+expect(retireSpy).toHaveBeenCalledWith(startedClient);
1868+expect(closeAndWait).toHaveBeenCalledWith({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 });
1869+expect(events.indexOf("request:thread/unsubscribe")).toBeGreaterThan(-1);
1870+expect(events.indexOf("closeAndWait")).toBeGreaterThan(
1871+events.indexOf("request:thread/unsubscribe"),
1872+);
1873+});
1874+1875+it("retires the shared Codex app-server client after one-shot turn start failures", async () => {
1876+const retireSpy = vi.spyOn(sharedClientModule, "retireSharedCodexAppServerClientIfCurrent");
1877+retireSpy.mockReturnValue({ activeLeases: 0, closed: true });
1878+const events: string[] = [];
1879+const closeAndWait = vi.fn(async () => {
1880+events.push("closeAndWait");
1881+return true;
1882+});
1883+let startedClient: unknown;
1884+setCodexAppServerClientFactoryForTest(async () => {
1885+const client = {
1886+request: vi.fn(async (method: string) => {
1887+events.push(`request:${method}`);
1888+if (method === "thread/start") {
1889+return threadStartResult();
1890+}
1891+if (method === "turn/start") {
1892+throw new Error("turn start failed");
1893+}
1894+return {};
1895+}),
1896+addNotificationHandler: vi.fn(() => () => undefined),
1897+addRequestHandler: vi.fn(() => () => undefined),
1898+addCloseHandler: vi.fn(() => () => undefined),
1899+ closeAndWait,
1900+};
1901+startedClient = client;
1902+return client as never;
1903+});
1904+const params = createParams(
1905+path.join(tempDir, "session.jsonl"),
1906+path.join(tempDir, "workspace"),
1907+);
1908+params.cleanupBundleMcpOnRunEnd = true;
1909+1910+await expect(runCodexAppServerAttempt(params)).rejects.toThrow("turn start failed");
1911+1912+expect(retireSpy).toHaveBeenCalledWith(startedClient);
1913+expect(closeAndWait).toHaveBeenCalledWith({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 });
1914+expect(events.indexOf("request:thread/unsubscribe")).toBeGreaterThan(-1);
1915+expect(events.indexOf("closeAndWait")).toBeGreaterThan(
1916+events.indexOf("request:thread/unsubscribe"),
1917+);
1918+});
1919+1920+it("keeps the shared Codex app-server client warm without one-shot cleanup", async () => {
1921+const retireSpy = vi.spyOn(sharedClientModule, "retireSharedCodexAppServerClientIfCurrent");
1922+const closeAndWait = vi.fn(async () => true);
1923+let notify: ((notification: CodexServerNotification) => Promise<void>) | undefined;
1924+setCodexAppServerClientFactoryForTest(
1925+async () =>
1926+({
1927+request: vi.fn(async (method: string) => {
1928+if (method === "thread/start") {
1929+return threadStartResult();
1930+}
1931+if (method === "turn/start") {
1932+return turnStartResult();
1933+}
1934+return {};
1935+}),
1936+addNotificationHandler: vi.fn((handler) => {
1937+notify = handler;
1938+return () => undefined;
1939+}),
1940+addRequestHandler: vi.fn(() => () => undefined),
1941+addCloseHandler: vi.fn(() => () => undefined),
1942+ closeAndWait,
1943+}) as never,
1944+);
1945+const params = createParams(
1946+path.join(tempDir, "session.jsonl"),
1947+path.join(tempDir, "workspace"),
1948+);
1949+1950+const run = runCodexAppServerAttempt(params);
1951+await vi.waitFor(() => expect(notify).toBeDefined(), fastWait);
1952+if (!notify) {
1953+throw new Error("expected turn notification handler");
1954+}
1955+await notify({
1956+method: "turn/completed",
1957+params: {
1958+threadId: "thread-1",
1959+turnId: "turn-1",
1960+turn: { id: "turn-1", status: "completed" },
1961+},
1962+});
1963+await run;
1964+1965+expect(retireSpy).not.toHaveBeenCalled();
1966+expect(closeAndWait).not.toHaveBeenCalled();
1967+});
1968+18131969it("keeps searchable Codex dynamic tools canonical in mirrored transcript snapshots", async () => {
18141970const params = createParams(
18151971path.join(tempDir, "session.jsonl"),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。