























@@ -1,9 +1,14 @@
1+import fs from "node:fs";
2+import os from "node:os";
3+import path from "node:path";
4+import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
15import { afterEach, describe, expect, it, vi } from "vitest";
26import {
37__testing as replyRunTesting,
48createReplyOperation,
59replyRunRegistry,
610} from "../auto-reply/reply/reply-run-registry.js";
11+import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
712import { runPreparedCliAgent } from "./cli-runner.js";
813import {
914createManagedRun,
@@ -15,6 +20,43 @@ import { executePreparedCliRun } from "./cli-runner/execute.js";
1520import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.js";
1621import type { PreparedCliRunContext } from "./cli-runner/types.js";
172223+vi.mock("../plugins/hook-runner-global.js", async () => {
24+const actual = await vi.importActual<typeof import("../plugins/hook-runner-global.js")>(
25+"../plugins/hook-runner-global.js",
26+);
27+return {
28+ ...actual,
29+getGlobalHookRunner: vi.fn(() => null),
30+};
31+});
32+33+const mockGetGlobalHookRunner = vi.mocked(getGlobalHookRunner);
34+35+function createSessionFile(params?: { history?: Array<{ role: "user"; content: string }> }) {
36+const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-hooks-"));
37+const sessionFile = path.join(dir, "session.jsonl");
38+fs.writeFileSync(
39+sessionFile,
40+`${JSON.stringify({
41+ type: "session",
42+ version: CURRENT_SESSION_VERSION,
43+ id: "session-test",
44+ timestamp: new Date(0).toISOString(),
45+ cwd: dir,
46+ })}\n`,
47+"utf-8",
48+);
49+const sessionManager = SessionManager.open(sessionFile);
50+for (const entry of params?.history ?? []) {
51+sessionManager.appendMessage({
52+role: entry.role,
53+content: entry.content,
54+timestamp: Date.now(),
55+});
56+}
57+return { dir, sessionFile };
58+}
59+1860function buildPreparedContext(params?: {
1961sessionKey?: string;
2062cliSessionId?: string;
@@ -67,6 +109,7 @@ function buildPreparedContext(params?: {
67109describe("runCliAgent reliability", () => {
68110afterEach(() => {
69111replyRunTesting.resetReplyRunRegistry();
112+mockGetGlobalHookRunner.mockReset();
70113});
7111472115it("fails with timeout when no-output watchdog trips", async () => {
@@ -312,6 +355,150 @@ describe("runCliAgent reliability", () => {
312355expect(result.meta.finalAssistantVisibleText).toBe("goodbye from cli");
313356expect(result.meta.finalAssistantRawText).toBe("hello from cli");
314357});
358+359+it("emits llm_input, llm_output, and agent_end hooks for successful CLI runs", async () => {
360+const hookRunner = {
361+hasHooks: vi.fn((hookName: string) =>
362+["llm_input", "llm_output", "agent_end"].includes(hookName),
363+),
364+runLlmInput: vi.fn(async () => undefined),
365+runLlmOutput: vi.fn(async () => undefined),
366+runAgentEnd: vi.fn(async () => undefined),
367+};
368+mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);
369+const { dir, sessionFile } = createSessionFile();
370+371+supervisorSpawnMock.mockResolvedValueOnce(
372+createManagedRun({
373+reason: "exit",
374+exitCode: 0,
375+exitSignal: null,
376+durationMs: 50,
377+stdout: "hello from cli",
378+stderr: "",
379+timedOut: false,
380+noOutputTimedOut: false,
381+}),
382+);
383+384+try {
385+await runPreparedCliAgent({
386+ ...buildPreparedContext(),
387+params: {
388+ ...buildPreparedContext().params,
389+ sessionFile,
390+workspaceDir: dir,
391+sessionKey: "agent:main:main",
392+agentId: "main",
393+messageProvider: "acp",
394+messageChannel: "telegram",
395+trigger: "user",
396+},
397+});
398+399+await vi.waitFor(() => {
400+expect(hookRunner.runLlmInput).toHaveBeenCalledTimes(1);
401+expect(hookRunner.runLlmOutput).toHaveBeenCalledTimes(1);
402+expect(hookRunner.runAgentEnd).toHaveBeenCalledTimes(1);
403+});
404+405+expect(hookRunner.runLlmInput).toHaveBeenCalledWith(
406+expect.objectContaining({
407+runId: "run-2",
408+sessionId: "s1",
409+provider: "codex-cli",
410+model: "gpt-5.4",
411+prompt: "hi",
412+systemPrompt: "You are a helpful assistant.",
413+historyMessages: expect.any(Array),
414+imagesCount: 0,
415+}),
416+expect.objectContaining({
417+runId: "run-2",
418+agentId: "main",
419+sessionKey: "agent:main:main",
420+sessionId: "s1",
421+workspaceDir: dir,
422+messageProvider: "acp",
423+trigger: "user",
424+channelId: "telegram",
425+}),
426+);
427+expect(hookRunner.runLlmOutput).toHaveBeenCalledWith(
428+expect.objectContaining({
429+runId: "run-2",
430+sessionId: "s1",
431+provider: "codex-cli",
432+model: "gpt-5.4",
433+assistantTexts: ["hello from cli"],
434+lastAssistant: expect.objectContaining({
435+role: "assistant",
436+content: [{ type: "text", text: "hello from cli" }],
437+provider: "codex-cli",
438+model: "gpt-5.4",
439+}),
440+}),
441+expect.any(Object),
442+);
443+expect(hookRunner.runAgentEnd).toHaveBeenCalledWith(
444+expect.objectContaining({
445+success: true,
446+messages: [
447+{ role: "user", content: "hi", timestamp: expect.any(Number) },
448+expect.objectContaining({
449+role: "assistant",
450+content: [{ type: "text", text: "hello from cli" }],
451+}),
452+],
453+}),
454+expect.any(Object),
455+);
456+} finally {
457+fs.rmSync(dir, { recursive: true, force: true });
458+}
459+});
460+461+it("emits agent_end with failure details when the CLI run fails", async () => {
462+const hookRunner = {
463+hasHooks: vi.fn((hookName: string) => ["llm_input", "agent_end"].includes(hookName)),
464+runLlmInput: vi.fn(async () => undefined),
465+runLlmOutput: vi.fn(async () => undefined),
466+runAgentEnd: vi.fn(async () => undefined),
467+};
468+mockGetGlobalHookRunner.mockReturnValue(hookRunner as never);
469+470+supervisorSpawnMock.mockResolvedValueOnce(
471+createManagedRun({
472+reason: "exit",
473+exitCode: 1,
474+exitSignal: null,
475+durationMs: 50,
476+stdout: "",
477+stderr: "rate limit exceeded",
478+timedOut: false,
479+noOutputTimedOut: false,
480+}),
481+);
482+483+await expect(runPreparedCliAgent(buildPreparedContext())).rejects.toThrow(
484+"rate limit exceeded",
485+);
486+487+await vi.waitFor(() => {
488+expect(hookRunner.runLlmInput).toHaveBeenCalledTimes(1);
489+expect(hookRunner.runLlmOutput).not.toHaveBeenCalled();
490+expect(hookRunner.runAgentEnd).toHaveBeenCalledTimes(1);
491+});
492+493+expect(hookRunner.runAgentEnd).toHaveBeenCalledWith(
494+expect.objectContaining({
495+success: false,
496+error: "rate limit exceeded",
497+messages: [{ role: "user", content: "hi", timestamp: expect.any(Number) }],
498+}),
499+expect.any(Object),
500+);
501+});
315502});
316503317504describe("resolveCliNoOutputTimeoutMs", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。