













@@ -555,6 +555,31 @@ async function buildDynamicToolsForTest(
555555});
556556}
557557558+async function createCodexToolBridgeForTest(
559+params: EmbeddedRunAttemptParams,
560+workspaceDir: string,
561+options: Partial<
562+Pick<
563+Parameters<typeof testing.buildDynamicTools>[0],
564+"forceHeartbeatTool" | "ignoreRuntimePlan"
565+>
566+> = {},
567+) {
568+const signal = new AbortController().signal;
569+const tools = await buildDynamicToolsForTest(params, workspaceDir, options);
570+const registeredTools = await buildDynamicToolsForTest(params, workspaceDir, {
571+ ...options,
572+forceHeartbeatTool: true,
573+ignoreRuntimePlan: true,
574+});
575+return createCodexDynamicToolBridge({
576+ tools,
577+ registeredTools,
578+ signal,
579+directToolNames: testing.shouldForceMessageTool(params) ? ["message"] : [],
580+});
581+}
582+558583function filterAllowedRuntimeToolNamesForTest(
559584params: EmbeddedRunAttemptParams,
560585tools: RuntimeDynamicToolForTest[],
@@ -2358,14 +2383,18 @@ describe("runCodexAppServerAttempt", () => {
23582383 ? [createRuntimeDynamicTool("heartbeat_respond")]
23592384 : []),
23602385]);
2361-const harness = createStartedThreadHarness(async (method) => {
2386+const request = vi.fn(async (method: string) => {
2387+if (method === "thread/start") {
2388+return threadStartResult("thread-1");
2389+}
23622390if (method === "thread/resume") {
23632391return threadStartResult("thread-1");
23642392}
2365-return undefined;
2393+throw new Error(`unexpected method: ${method}`);
23662394});
23672395const sessionFile = path.join(tempDir, "session.jsonl");
23682396const workspaceDir = path.join(tempDir, "workspace");
2397+const appServer = createThreadLifecycleAppServerOptions();
23692398const createRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => {
23702399const params = createParams(sessionFile, workspaceDir);
23712400params.disableTools = false;
@@ -2378,16 +2407,26 @@ describe("runCodexAppServerAttempt", () => {
23782407}
23792408return params;
23802409};
2410+const startOrResume = async (params: EmbeddedRunAttemptParams) => {
2411+const toolBridge = await createCodexToolBridgeForTest(params, workspaceDir);
2412+return startOrResumeThread({
2413+client: { request } as never,
2414+ params,
2415+cwd: workspaceDir,
2416+dynamicTools: toolBridge.specs,
2417+ appServer,
2418+developerInstructions: testing.buildDeveloperInstructions(params, {
2419+dynamicTools: toolBridge.availableSpecs,
2420+}),
2421+nativeCodeModeEnabled: true,
2422+userMcpServersEnabled: false,
2423+});
2424+};
238124252382-const normalRun = runCodexAppServerAttempt(createRunParams(), {
2383-pluginConfig: { appServer: { mode: "yolo" } },
2384-});
2385-await harness.waitForMethod("turn/start", 120_000);
2386-await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
2387-await normalRun;
2426+await startOrResume(createRunParams());
238824272389-const startRequest = harness.requests.find((entry) => entry.method === "thread/start");
2390-const startParams = startRequest?.params as
2428+const startRequest = request.mock.calls.find(([method]) => method === "thread/start");
2429+const startParams = startRequest?.[1] as
23912430| { dynamicTools?: Array<{ name?: string }>; developerInstructions?: string }
23922431| undefined;
23932432const registeredToolNames = startParams?.dynamicTools?.map((tool) => tool.name) ?? [];
@@ -2401,32 +2440,14 @@ describe("runCodexAppServerAttempt", () => {
24012440"Deferred searchable OpenClaw dynamic tools available: heartbeat_respond",
24022441);
240324422404-const heartbeatRun = runCodexAppServerAttempt(createRunParams("heartbeat"), {
2405-pluginConfig: { appServer: { mode: "yolo" } },
2406-});
2407-await vi.waitFor(
2408-() => {
2409-expect(harness.requests.filter((entry) => entry.method === "turn/start")).toHaveLength(2);
2410-},
2411-{ interval: 1, timeout: 120_000 },
2412-);
2413-await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
2414-await heartbeatRun;
2443+await startOrResume(createRunParams("heartbeat"));
2444+await startOrResume(createRunParams());
241524452416-const nextNormalRun = runCodexAppServerAttempt(createRunParams(), {
2417-pluginConfig: { appServer: { mode: "yolo" } },
2418-});
2419-await vi.waitFor(
2420-() => {
2421-expect(harness.requests.filter((entry) => entry.method === "turn/start")).toHaveLength(3);
2422-},
2423-{ interval: 1, timeout: 120_000 },
2424-);
2425-await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
2426-await nextNormalRun;
2427-2428-expect(harness.requests.filter((entry) => entry.method === "thread/start")).toHaveLength(1);
2429-expect(harness.requests.filter((entry) => entry.method === "thread/resume")).toHaveLength(2);
2446+expect(request.mock.calls.map(([method]) => method)).toEqual([
2447+"thread/start",
2448+"thread/resume",
2449+"thread/resume",
2450+]);
24302451});
2431245224322453it("keeps the persistent dynamic schema stable across heartbeat-only turns", async () => {
@@ -2437,14 +2458,18 @@ describe("runCodexAppServerAttempt", () => {
24372458 ? [createRuntimeDynamicTool("heartbeat_respond")]
24382459 : []),
24392460]);
2440-const harness = createStartedThreadHarness(async (method) => {
2461+const request = vi.fn(async (method: string) => {
2462+if (method === "thread/start") {
2463+return threadStartResult("thread-1");
2464+}
24412465if (method === "thread/resume") {
24422466return threadStartResult("thread-1");
24432467}
2444-return undefined;
2468+throw new Error(`unexpected method: ${method}`);
24452469});
24462470const sessionFile = path.join(tempDir, "session.jsonl");
24472471const workspaceDir = path.join(tempDir, "workspace");
2472+const appServer = createThreadLifecycleAppServerOptions();
24482473const createRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => {
24492474const params = createParams(sessionFile, workspaceDir);
24502475params.disableTools = false;
@@ -2464,41 +2489,29 @@ describe("runCodexAppServerAttempt", () => {
24642489}
24652490return params;
24662491};
2492+const startOrResume = async (params: EmbeddedRunAttemptParams) => {
2493+const toolBridge = await createCodexToolBridgeForTest(params, workspaceDir);
2494+return startOrResumeThread({
2495+client: { request } as never,
2496+ params,
2497+cwd: workspaceDir,
2498+dynamicTools: toolBridge.specs,
2499+ appServer,
2500+developerInstructions: testing.buildDeveloperInstructions(params, {
2501+dynamicTools: toolBridge.availableSpecs,
2502+}),
2503+nativeCodeModeEnabled: true,
2504+userMcpServersEnabled: false,
2505+});
2506+};
246725072468-const normalRun = runCodexAppServerAttempt(createRunParams(), {
2469-pluginConfig: { appServer: { mode: "yolo" } },
2470-});
2471-await harness.waitForMethod("turn/start", 120_000);
2472-await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
2473-await normalRun;
2474-2475-const heartbeatRun = runCodexAppServerAttempt(createRunParams("heartbeat"), {
2476-pluginConfig: { appServer: { mode: "yolo" } },
2477-});
2478-await vi.waitFor(
2479-() => {
2480-expect(harness.requests.filter((entry) => entry.method === "turn/start")).toHaveLength(2);
2481-},
2482-{ interval: 1, timeout: 120_000 },
2483-);
2484-await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
2485-await heartbeatRun;
2486-2487-const nextNormalRun = runCodexAppServerAttempt(createRunParams(), {
2488-pluginConfig: { appServer: { mode: "yolo" } },
2489-});
2490-await vi.waitFor(
2491-() => {
2492-expect(harness.requests.filter((entry) => entry.method === "turn/start")).toHaveLength(3);
2493-},
2494-{ interval: 1, timeout: 120_000 },
2495-);
2496-await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
2497-await nextNormalRun;
2508+await startOrResume(createRunParams());
2509+await startOrResume(createRunParams("heartbeat"));
2510+await startOrResume(createRunParams());
2498251124992512expect(
2500-harness.requests
2501-.map((entry) => entry.method)
2513+request.mock.calls
2514+.map(([method]) => method)
25022515.filter((method) => method === "thread/start" || method === "thread/resume"),
25032516).toEqual(["thread/start", "thread/resume", "thread/resume"]);
25042517});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。