



























@@ -1684,6 +1684,173 @@ describe("runCodexAppServerAttempt", () => {
16841684expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(true);
16851685});
168616861687+it("counts handled nullable-turn elicitations as turn attempt progress", async () => {
1688+const harness = createStartedThreadHarness();
1689+vi.spyOn(elicitationBridge, "handleCodexAppServerElicitationRequest").mockResolvedValue({
1690+action: "accept",
1691+content: null,
1692+_meta: null,
1693+});
1694+const params = createParams(
1695+path.join(tempDir, "session.jsonl"),
1696+path.join(tempDir, "workspace"),
1697+);
1698+params.timeoutMs = 100;
1699+1700+const run = runCodexAppServerAttempt(params, {
1701+turnCompletionIdleTimeoutMs: 300,
1702+turnAssistantCompletionIdleTimeoutMs: 300,
1703+turnTerminalIdleTimeoutMs: 300,
1704+});
1705+await harness.waitForMethod("turn/start");
1706+1707+await new Promise((resolve) => setTimeout(resolve, 60));
1708+await harness.handleServerRequest({
1709+id: "request-null-turn-elicitation",
1710+method: "mcpServer/elicitation/request",
1711+params: {
1712+threadId: "thread-1",
1713+turnId: null,
1714+mode: "form",
1715+message: "Approve?",
1716+requestedSchema: { type: "object", properties: {} },
1717+serverName: "server-1",
1718+_meta: null,
1719+},
1720+});
1721+await new Promise((resolve) => setTimeout(resolve, 60));
1722+1723+expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
1724+await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
1725+1726+const result = await run;
1727+expect(result.aborted).toBe(false);
1728+expect(result.timedOut).toBe(false);
1729+expect(result.promptError).toBeNull();
1730+});
1731+1732+it("counts pending user input requests as turn attempt progress", async () => {
1733+const harness = createStartedThreadHarness();
1734+const params = createParams(
1735+path.join(tempDir, "session.jsonl"),
1736+path.join(tempDir, "workspace"),
1737+);
1738+params.timeoutMs = 100;
1739+params.onBlockReply = vi.fn();
1740+1741+const run = runCodexAppServerAttempt(params, {
1742+turnCompletionIdleTimeoutMs: 300,
1743+turnAssistantCompletionIdleTimeoutMs: 300,
1744+turnTerminalIdleTimeoutMs: 300,
1745+});
1746+await harness.waitForMethod("turn/start");
1747+1748+await new Promise((resolve) => setTimeout(resolve, 60));
1749+const response = harness.handleServerRequest({
1750+id: "request-user-input",
1751+method: "item/tool/requestUserInput",
1752+params: {
1753+threadId: "thread-1",
1754+turnId: "turn-1",
1755+itemId: "input-1",
1756+questions: [
1757+{
1758+id: "mode",
1759+header: "Mode",
1760+question: "Pick a mode",
1761+isOther: false,
1762+isSecret: false,
1763+options: [
1764+{ label: "Fast", description: "Use less reasoning" },
1765+{ label: "Deep", description: "Use more reasoning" },
1766+],
1767+},
1768+],
1769+},
1770+});
1771+await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1), { interval: 1 });
1772+await new Promise((resolve) => setTimeout(resolve, 60));
1773+1774+expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
1775+expect(queueActiveRunMessageForTest("session-1", "2")).toBe(true);
1776+await expect(response).resolves.toEqual({
1777+answers: { mode: { answers: ["Deep"] } },
1778+});
1779+await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
1780+1781+const result = await run;
1782+expect(result.aborted).toBe(false);
1783+expect(result.timedOut).toBe(false);
1784+expect(result.promptError).toBeNull();
1785+});
1786+1787+it("does not count mismatched turn-scoped requests as turn attempt progress", async () => {
1788+const harness = createStartedThreadHarness();
1789+const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
1790+const params = createParams(
1791+path.join(tempDir, "session.jsonl"),
1792+path.join(tempDir, "workspace"),
1793+);
1794+params.timeoutMs = 100;
1795+1796+const run = runCodexAppServerAttempt(params, {
1797+turnCompletionIdleTimeoutMs: 500,
1798+turnAssistantCompletionIdleTimeoutMs: 500,
1799+turnTerminalIdleTimeoutMs: 500,
1800+});
1801+await harness.waitForMethod("turn/start");
1802+1803+await new Promise((resolve) => setTimeout(resolve, 60));
1804+await harness.handleServerRequest({
1805+id: "request-foreign-elicitation",
1806+method: "mcpServer/elicitation/request",
1807+params: {
1808+threadId: "thread-1",
1809+turnId: "turn-other",
1810+mode: "form",
1811+message: "Approve?",
1812+requestedSchema: { type: "object", properties: {} },
1813+serverName: "server-1",
1814+_meta: null,
1815+},
1816+});
1817+await harness.handleServerRequest({
1818+id: "request-foreign-user-input",
1819+method: "item/tool/requestUserInput",
1820+params: {
1821+threadId: "thread-1",
1822+turnId: "turn-other",
1823+itemId: "input-1",
1824+questions: [],
1825+},
1826+});
1827+await harness.handleServerRequest({
1828+id: "request-foreign-approval",
1829+method: "item/commandExecution/requestApproval",
1830+params: {
1831+threadId: "thread-1",
1832+turnId: "turn-other",
1833+itemId: "command-1",
1834+},
1835+});
1836+1837+const result = await run;
1838+expect(result.aborted).toBe(true);
1839+expect(result.timedOut).toBe(true);
1840+expect(result.promptError).toBe(
1841+"codex app-server turn idle timed out waiting for turn/completed",
1842+);
1843+const warnCall = warn.mock.calls.find(
1844+([message]) => message === "codex app-server turn idle timed out waiting for progress",
1845+);
1846+const warnData = warnCall?.[1] as
1847+| { lastActivityReason?: string; timeoutMs?: number }
1848+| undefined;
1849+expect(warnData?.timeoutMs).toBe(100);
1850+expect(warnData?.lastActivityReason).toBe("turn:start");
1851+expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(true);
1852+});
1853+16871854it("does not count account rate-limit updates as turn completion activity", async () => {
16881855let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
16891856let handleRequest:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。