























@@ -0,0 +1,167 @@
1+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
3+4+// vi.mock factories are hoisted above imports, so any references inside them
5+// must come from vi.hoisted() so they exist at hoist time (otherwise they'd
6+// be TDZ-undefined and the mocks would silently misbehave). This test only
7+// exercises the hook-gate decision at the runCliAgent entry point — we mock
8+// the prepareCliRunContext + executePreparedCliRun seams so no broader CLI
9+// runtime needs to load.
10+type BeforeAgentReplyResult =
11+| undefined
12+| {
13+handled?: boolean;
14+reply?: { text?: string };
15+};
16+17+const {
18+ hasHooksMock,
19+ runBeforeAgentReplyMock,
20+ executePreparedCliRunMock,
21+ prepareCliRunContextMock,
22+} = vi.hoisted(() => ({
23+hasHooksMock: vi.fn<(hookName: string) => boolean>(() => false),
24+runBeforeAgentReplyMock: vi.fn<(event: unknown, ctx: unknown) => Promise<BeforeAgentReplyResult>>(
25+async () => undefined,
26+),
27+executePreparedCliRunMock: vi.fn(async (_context: unknown, _cliSessionIdToUse?: string) => ({
28+text: "",
29+})),
30+prepareCliRunContextMock: vi.fn(),
31+}));
32+33+vi.mock("../plugins/hook-runner-global.js", () => ({
34+getGlobalHookRunner: vi.fn(() => ({
35+hasHooks: hasHooksMock,
36+runBeforeAgentReply: runBeforeAgentReplyMock,
37+})),
38+}));
39+40+vi.mock("./cli-runner/prepare.runtime.js", () => ({
41+prepareCliRunContext: prepareCliRunContextMock,
42+}));
43+44+vi.mock("./cli-runner/execute.runtime.js", () => ({
45+executePreparedCliRun: executePreparedCliRunMock,
46+}));
47+48+const baseRunParams = {
49+sessionId: "test-session",
50+sessionKey: "test-session-key",
51+agentId: "main",
52+sessionFile: "/tmp/test-session.jsonl",
53+workspaceDir: "/tmp/test-workspace",
54+prompt: "__openclaw_memory_core_short_term_promotion_dream__",
55+provider: "codex-cli",
56+model: "gpt-5.5",
57+timeoutMs: 30_000,
58+runId: "test-run-id",
59+} as const;
60+61+function makeStubContext(params: typeof baseRunParams & { trigger?: string }) {
62+return {
63+ params,
64+started: Date.now(),
65+workspaceDir: params.workspaceDir,
66+modelId: params.model,
67+normalizedModel: params.model,
68+systemPrompt: "",
69+systemPromptReport: {},
70+bootstrapPromptWarningLines: [],
71+authEpochVersion: 0,
72+backendResolved: {},
73+preparedBackend: {},
74+reusableCliSession: {},
75+} as unknown;
76+}
77+78+beforeEach(() => {
79+hasHooksMock.mockReset();
80+hasHooksMock.mockReturnValue(false);
81+runBeforeAgentReplyMock.mockReset();
82+runBeforeAgentReplyMock.mockResolvedValue(undefined);
83+executePreparedCliRunMock.mockReset();
84+executePreparedCliRunMock.mockResolvedValue({ text: "" });
85+prepareCliRunContextMock.mockReset();
86+prepareCliRunContextMock.mockImplementation(async (params) =>
87+makeStubContext(params as typeof baseRunParams & { trigger?: string }),
88+);
89+});
90+91+afterEach(() => {
92+vi.clearAllMocks();
93+});
94+95+describe("runCliAgent cron before_agent_reply seam", () => {
96+it("lets before_agent_reply claim cron runs before the CLI subprocess is invoked", async () => {
97+const { runCliAgent } = await import("./cli-runner.js");
98+hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");
99+runBeforeAgentReplyMock.mockResolvedValue({
100+handled: true,
101+reply: { text: "dreaming claimed via cli runner" },
102+});
103+104+const result = await runCliAgent({ ...baseRunParams, trigger: "cron" });
105+106+expect(runBeforeAgentReplyMock).toHaveBeenCalledTimes(1);
107+expect(runBeforeAgentReplyMock).toHaveBeenCalledWith(
108+{ cleanedBody: baseRunParams.prompt },
109+expect.objectContaining({
110+agentId: baseRunParams.agentId,
111+sessionId: baseRunParams.sessionId,
112+sessionKey: baseRunParams.sessionKey,
113+workspaceDir: baseRunParams.workspaceDir,
114+trigger: "cron",
115+}),
116+);
117+expect(executePreparedCliRunMock).not.toHaveBeenCalled();
118+expect(result.payloads?.[0]?.text).toBe("dreaming claimed via cli runner");
119+});
120+121+it("does not run prepareCliRunContext when the cron hook claims (no resource allocation, no leak)", async () => {
122+// Regression for PR #70950 review (greptile-apps, P1): the gate must fire
123+// before any backend resources are allocated, otherwise preparedBackend.cleanup
124+// is silently skipped on every claimed cron turn.
125+const { runCliAgent } = await import("./cli-runner.js");
126+hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");
127+runBeforeAgentReplyMock.mockResolvedValue({ handled: true });
128+129+await runCliAgent({ ...baseRunParams, trigger: "cron" });
130+131+expect(prepareCliRunContextMock).not.toHaveBeenCalled();
132+expect(executePreparedCliRunMock).not.toHaveBeenCalled();
133+});
134+135+it("returns a silent payload when a cron hook claims without a reply body", async () => {
136+const { runCliAgent } = await import("./cli-runner.js");
137+hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");
138+runBeforeAgentReplyMock.mockResolvedValue({ handled: true });
139+140+const result = await runCliAgent({ ...baseRunParams, trigger: "cron" });
141+142+expect(executePreparedCliRunMock).not.toHaveBeenCalled();
143+expect(result.payloads?.[0]?.text).toBe(SILENT_REPLY_TOKEN);
144+});
145+146+it("does not invoke before_agent_reply for non-cron triggers", async () => {
147+const { runCliAgent } = await import("./cli-runner.js");
148+hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");
149+executePreparedCliRunMock.mockResolvedValue({ text: "real reply" });
150+151+await runCliAgent({ ...baseRunParams, trigger: "user" });
152+153+expect(runBeforeAgentReplyMock).not.toHaveBeenCalled();
154+expect(executePreparedCliRunMock).toHaveBeenCalled();
155+});
156+157+it("falls through to the CLI subprocess when no before_agent_reply hook is registered", async () => {
158+const { runCliAgent } = await import("./cli-runner.js");
159+hasHooksMock.mockReturnValue(false);
160+executePreparedCliRunMock.mockResolvedValue({ text: "real reply" });
161+162+await runCliAgent({ ...baseRunParams, trigger: "cron" });
163+164+expect(runBeforeAgentReplyMock).not.toHaveBeenCalled();
165+expect(executePreparedCliRunMock).toHaveBeenCalled();
166+});
167+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。