
























@@ -1,5 +1,14 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { SessionManager } from "@mariozechner/pi-coding-agent";
15import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness";
2-import { describe, expect, it, vi } from "vitest";
6+import { afterEach, describe, expect, it, vi } from "vitest";
7+import {
8+initializeGlobalHookRunner,
9+resetGlobalHookRunner,
10+} from "../../../../src/plugins/hook-runner-global.js";
11+import { createMockPluginRegistry } from "../../../../src/plugins/hooks.test-helpers.js";
312import {
413CodexAppServerEventProjector,
514type CodexAppServerToolTelemetry,
@@ -8,36 +17,87 @@ import { createCodexTestModel } from "./test-support.js";
817918const THREAD_ID = "thread-1";
1019const TURN_ID = "turn-1";
20+const tempDirs = new Set<string>();
11211222type ProjectorNotification = Parameters<CodexAppServerEventProjector["handleNotification"]>[0];
132314-function createParams(): EmbeddedRunAttemptParams {
24+function assistantMessage(text: string, timestamp: number) {
25+return {
26+role: "assistant" as const,
27+content: [{ type: "text" as const, text }],
28+api: "openai-codex-responses",
29+provider: "openai-codex",
30+model: "gpt-5.4-codex",
31+usage: {
32+input: 0,
33+output: 0,
34+cacheRead: 0,
35+cacheWrite: 0,
36+totalTokens: 0,
37+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
38+},
39+stopReason: "stop" as const,
40+ timestamp,
41+};
42+}
43+44+async function createParams(): Promise<EmbeddedRunAttemptParams> {
45+const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-projector-"));
46+tempDirs.add(tempDir);
47+const sessionFile = path.join(tempDir, "session.jsonl");
48+SessionManager.open(sessionFile).appendMessage(assistantMessage("history", Date.now()));
1549return {
1650prompt: "hello",
1751sessionId: "session-1",
52+ sessionFile,
53+workspaceDir: tempDir,
54+runId: "run-1",
1855provider: "openai-codex",
1956modelId: "gpt-5.4-codex",
2057model: createCodexTestModel(),
2158thinkLevel: "medium",
22-} as unknown as EmbeddedRunAttemptParams;
59+} as EmbeddedRunAttemptParams;
2360}
246125-function createProjector(params = createParams()): CodexAppServerEventProjector {
26-return new CodexAppServerEventProjector(params, THREAD_ID, TURN_ID);
62+async function createProjector(
63+params?: EmbeddedRunAttemptParams,
64+): Promise<CodexAppServerEventProjector> {
65+const resolvedParams = params ?? (await createParams());
66+return new CodexAppServerEventProjector(resolvedParams, THREAD_ID, TURN_ID);
2767}
286829-function createProjectorWithAssistantHooks() {
69+async function createProjectorWithAssistantHooks() {
3070const onAssistantMessageStart = vi.fn();
3171const onPartialReply = vi.fn();
32-return {
72+const params = await createParams();
73+const projector = await createProjector({
74+ ...params,
3375 onAssistantMessageStart,
3476 onPartialReply,
35-projector: createProjector({
36- ...createParams(),
37- onAssistantMessageStart,
38- onPartialReply,
39-}),
40-};
77+});
78+return { onAssistantMessageStart, onPartialReply, projector };
79+}
80+81+afterEach(async () => {
82+resetGlobalHookRunner();
83+vi.restoreAllMocks();
84+for (const tempDir of tempDirs) {
85+await fs.rm(tempDir, { recursive: true, force: true });
86+}
87+tempDirs.clear();
88+});
89+90+async function createProjectorWithHooks() {
91+const beforeCompaction = vi.fn();
92+const afterCompaction = vi.fn();
93+initializeGlobalHookRunner(
94+createMockPluginRegistry([
95+{ hookName: "before_compaction", handler: beforeCompaction },
96+{ hookName: "after_compaction", handler: afterCompaction },
97+]),
98+);
99+const projector = await createProjector();
100+return { projector, beforeCompaction, afterCompaction };
41101}
4210243103function buildEmptyToolTelemetry(): CodexAppServerToolTelemetry {
@@ -72,7 +132,7 @@ function turnCompleted(items: unknown[] = []): ProjectorNotification {
72132describe("CodexAppServerEventProjector", () => {
73133it("projects assistant deltas and usage into embedded attempt results", async () => {
74134const { onAssistantMessageStart, onPartialReply, projector } =
75-createProjectorWithAssistantHooks();
135+await createProjectorWithAssistantHooks();
7613677137await projector.handleNotification(agentMessageDelta("hel"));
78138await projector.handleNotification(agentMessageDelta("lo"));
@@ -116,7 +176,7 @@ describe("CodexAppServerEventProjector", () => {
116176});
117177118178it("does not treat cumulative-only token usage as fresh context usage", async () => {
119-const projector = createProjector();
179+const projector = await createProjector();
120180121181await projector.handleNotification(agentMessageDelta("done"));
122182await projector.handleNotification(
@@ -145,7 +205,7 @@ describe("CodexAppServerEventProjector", () => {
145205});
146206147207it("normalizes snake_case current token usage fields", async () => {
148-const projector = createProjector();
208+const projector = await createProjector();
149209150210await projector.handleNotification(agentMessageDelta("done"));
151211await projector.handleNotification(
@@ -175,7 +235,7 @@ describe("CodexAppServerEventProjector", () => {
175235176236it("keeps intermediate agentMessage items out of the final visible reply", async () => {
177237const { onAssistantMessageStart, onPartialReply, projector } =
178-createProjectorWithAssistantHooks();
238+await createProjectorWithAssistantHooks();
179239180240await projector.handleNotification(
181241agentMessageDelta(
@@ -221,7 +281,7 @@ describe("CodexAppServerEventProjector", () => {
221281});
222282223283it("ignores notifications for other turns", async () => {
224-const projector = createProjector();
284+const projector = await createProjector();
225285226286await projector.handleNotification({
227287method: "item/agentMessage/delta",
@@ -233,7 +293,21 @@ describe("CodexAppServerEventProjector", () => {
233293});
234294235295it("preserves sessions_yield detection in attempt results", () => {
236-const projector = createProjector();
296+const projector = new CodexAppServerEventProjector(
297+{
298+prompt: "hello",
299+sessionId: "session-1",
300+sessionFile: "/tmp/session.jsonl",
301+workspaceDir: "/tmp",
302+runId: "run-1",
303+provider: "openai-codex",
304+modelId: "gpt-5.4-codex",
305+model: createCodexTestModel(),
306+thinkLevel: "medium",
307+} as EmbeddedRunAttemptParams,
308+THREAD_ID,
309+TURN_ID,
310+);
237311238312const result = projector.buildResult(buildEmptyToolTelemetry(), { yieldDetected: true });
239313@@ -245,12 +319,12 @@ describe("CodexAppServerEventProjector", () => {
245319const onReasoningEnd = vi.fn();
246320const onAgentEvent = vi.fn();
247321const params = {
248- ...createParams(),
322+ ...(await createParams()),
249323 onReasoningStream,
250324 onReasoningEnd,
251325 onAgentEvent,
252326};
253-const projector = createProjector(params);
327+const projector = await createProjector(params);
254328255329await projector.handleNotification(
256330forCurrentTurn("item/reasoning/textDelta", { itemId: "reason-1", delta: "thinking" }),
@@ -319,8 +393,8 @@ describe("CodexAppServerEventProjector", () => {
319393const onAgentEvent = vi.fn(() => {
320394throw new Error("consumer failed");
321395});
322-const projector = createProjector({
323- ...createParams(),
396+const projector = await createProjector({
397+ ...(await createParams()),
324398 onAgentEvent,
325399});
326400@@ -344,4 +418,42 @@ describe("CodexAppServerEventProjector", () => {
344418expect(result.assistantTexts).toEqual(["final answer"]);
345419expect(JSON.stringify(result.messagesSnapshot)).toContain("Codex plan");
346420});
421+422+it("fires before_compaction and after_compaction hooks for codex compaction items", async () => {
423+const { projector, beforeCompaction, afterCompaction } = await createProjectorWithHooks();
424+425+await projector.handleNotification(
426+forCurrentTurn("item/started", {
427+item: { type: "contextCompaction", id: "compact-1" },
428+}),
429+);
430+await projector.handleNotification(
431+forCurrentTurn("item/completed", {
432+item: { type: "contextCompaction", id: "compact-1" },
433+}),
434+);
435+436+expect(beforeCompaction).toHaveBeenCalledWith(
437+expect.objectContaining({
438+messageCount: 1,
439+sessionFile: expect.stringContaining("session.jsonl"),
440+messages: [expect.objectContaining({ role: "assistant" })],
441+}),
442+expect.objectContaining({
443+runId: "run-1",
444+sessionId: "session-1",
445+}),
446+);
447+expect(afterCompaction).toHaveBeenCalledWith(
448+expect.objectContaining({
449+messageCount: 1,
450+compactedCount: -1,
451+sessionFile: expect.stringContaining("session.jsonl"),
452+}),
453+expect.objectContaining({
454+runId: "run-1",
455+sessionId: "session-1",
456+}),
457+);
458+});
347459});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。