
























@@ -114,6 +114,9 @@ function createAppServerHarness(
114114},
115115});
116116},
117+async notify(notification: CodexServerNotification) {
118+await notify(notification);
119+},
117120};
118121}
119122@@ -238,6 +241,209 @@ describe("runCodexAppServerAttempt", () => {
238241);
239242});
240243244+it("fires llm_input, llm_output, and agent_end hooks for codex turns", async () => {
245+const llmInput = vi.fn();
246+const llmOutput = vi.fn();
247+const agentEnd = vi.fn();
248+initializeGlobalHookRunner(
249+createMockPluginRegistry([
250+{ hookName: "llm_input", handler: llmInput },
251+{ hookName: "llm_output", handler: llmOutput },
252+{ hookName: "agent_end", handler: agentEnd },
253+]),
254+);
255+const sessionFile = path.join(tempDir, "session.jsonl");
256+const workspaceDir = path.join(tempDir, "workspace");
257+const sessionManager = SessionManager.open(sessionFile);
258+sessionManager.appendMessage(assistantMessage("existing context", Date.now()));
259+const harness = createStartedThreadHarness();
260+261+const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
262+await harness.waitForMethod("turn/start");
263+await vi.waitFor(() => expect(llmInput).toHaveBeenCalledTimes(1), { interval: 1 });
264+265+expect(llmInput).toHaveBeenCalledWith(
266+expect.objectContaining({
267+runId: "run-1",
268+sessionId: "session-1",
269+provider: "codex",
270+model: "gpt-5.4-codex",
271+prompt: "hello",
272+imagesCount: 0,
273+historyMessages: [expect.objectContaining({ role: "assistant" })],
274+systemPrompt: expect.stringContaining(CODEX_GPT5_BEHAVIOR_CONTRACT),
275+}),
276+expect.objectContaining({
277+runId: "run-1",
278+sessionId: "session-1",
279+sessionKey: "agent:main:session-1",
280+}),
281+);
282+283+await harness.notify({
284+method: "item/agentMessage/delta",
285+params: {
286+threadId: "thread-1",
287+turnId: "turn-1",
288+itemId: "msg-1",
289+delta: "hello back",
290+},
291+});
292+await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
293+const result = await run;
294+295+expect(result.assistantTexts).toEqual(["hello back"]);
296+await vi.waitFor(() => expect(llmOutput).toHaveBeenCalledTimes(1), { interval: 1 });
297+await vi.waitFor(() => expect(agentEnd).toHaveBeenCalledTimes(1), { interval: 1 });
298+299+expect(llmOutput).toHaveBeenCalledWith(
300+expect.objectContaining({
301+runId: "run-1",
302+sessionId: "session-1",
303+provider: "codex",
304+model: "gpt-5.4-codex",
305+assistantTexts: ["hello back"],
306+lastAssistant: expect.objectContaining({
307+role: "assistant",
308+}),
309+}),
310+expect.objectContaining({
311+runId: "run-1",
312+sessionId: "session-1",
313+}),
314+);
315+expect(agentEnd).toHaveBeenCalledWith(
316+expect.objectContaining({
317+success: true,
318+messages: expect.arrayContaining([
319+expect.objectContaining({ role: "user" }),
320+expect.objectContaining({ role: "assistant" }),
321+]),
322+}),
323+expect.objectContaining({
324+runId: "run-1",
325+sessionId: "session-1",
326+}),
327+);
328+});
329+330+it("fires agent_end with failure metadata when the codex turn fails", async () => {
331+const agentEnd = vi.fn();
332+initializeGlobalHookRunner(
333+createMockPluginRegistry([{ hookName: "agent_end", handler: agentEnd }]),
334+);
335+const sessionFile = path.join(tempDir, "session.jsonl");
336+const workspaceDir = path.join(tempDir, "workspace");
337+const harness = createStartedThreadHarness();
338+339+const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
340+await harness.waitForMethod("turn/start");
341+await harness.notify({
342+method: "turn/completed",
343+params: {
344+threadId: "thread-1",
345+turnId: "turn-1",
346+turn: {
347+id: "turn-1",
348+status: "failed",
349+error: { message: "codex exploded" },
350+},
351+},
352+});
353+354+const result = await run;
355+356+expect(result.promptError).toBe("codex exploded");
357+await vi.waitFor(() => expect(agentEnd).toHaveBeenCalledTimes(1), { interval: 1 });
358+expect(agentEnd).toHaveBeenCalledWith(
359+expect.objectContaining({
360+success: false,
361+error: "codex exploded",
362+}),
363+expect.objectContaining({
364+runId: "run-1",
365+sessionId: "session-1",
366+}),
367+);
368+});
369+370+it("fires llm_output and agent_end when turn/start fails", async () => {
371+const llmInput = vi.fn();
372+const llmOutput = vi.fn();
373+const agentEnd = vi.fn();
374+initializeGlobalHookRunner(
375+createMockPluginRegistry([
376+{ hookName: "llm_input", handler: llmInput },
377+{ hookName: "llm_output", handler: llmOutput },
378+{ hookName: "agent_end", handler: agentEnd },
379+]),
380+);
381+const sessionFile = path.join(tempDir, "session.jsonl");
382+const workspaceDir = path.join(tempDir, "workspace");
383+SessionManager.open(sessionFile).appendMessage(
384+assistantMessage("existing context", Date.now()),
385+);
386+createStartedThreadHarness(async (method) => {
387+if (method === "turn/start") {
388+throw new Error("turn start exploded");
389+}
390+return undefined;
391+});
392+393+await expect(runCodexAppServerAttempt(createParams(sessionFile, workspaceDir))).rejects.toThrow(
394+"turn start exploded",
395+);
396+397+await vi.waitFor(() => expect(llmInput).toHaveBeenCalledTimes(1), { interval: 1 });
398+await vi.waitFor(() => expect(llmOutput).toHaveBeenCalledTimes(1), { interval: 1 });
399+await vi.waitFor(() => expect(agentEnd).toHaveBeenCalledTimes(1), { interval: 1 });
400+expect(llmOutput).toHaveBeenCalledWith(
401+expect.objectContaining({
402+assistantTexts: [],
403+model: "gpt-5.4-codex",
404+provider: "codex",
405+runId: "run-1",
406+sessionId: "session-1",
407+}),
408+expect.any(Object),
409+);
410+expect(agentEnd).toHaveBeenCalledWith(
411+expect.objectContaining({
412+success: false,
413+error: "turn start exploded",
414+messages: expect.arrayContaining([
415+expect.objectContaining({ role: "assistant" }),
416+expect.objectContaining({ role: "user" }),
417+]),
418+}),
419+expect.any(Object),
420+);
421+});
422+423+it("fires agent_end with success false when the codex turn is aborted", async () => {
424+const agentEnd = vi.fn();
425+initializeGlobalHookRunner(
426+createMockPluginRegistry([{ hookName: "agent_end", handler: agentEnd }]),
427+);
428+const { waitForMethod } = createStartedThreadHarness();
429+const run = runCodexAppServerAttempt(
430+createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
431+);
432+433+await waitForMethod("turn/start");
434+expect(abortAgentHarnessRun("session-1")).toBe(true);
435+436+const result = await run;
437+expect(result.aborted).toBe(true);
438+await vi.waitFor(() => expect(agentEnd).toHaveBeenCalledTimes(1), { interval: 1 });
439+expect(agentEnd).toHaveBeenCalledWith(
440+expect.objectContaining({
441+success: false,
442+}),
443+expect.any(Object),
444+);
445+});
446+241447it("forwards queued user input and aborts the active app-server turn", async () => {
242448const { requests, waitForMethod } = createStartedThreadHarness();
243449此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。