




















@@ -13,9 +13,19 @@ import {
1313makeAgentAssistantMessage,
1414makeAgentUserMessage,
1515} from "openclaw/plugin-sdk/test-fixtures";
16-import { afterEach, describe, expect, it } from "vitest";
16+import { afterEach, describe, expect, it, vi } from "vitest";
1717import { attachCodexMirrorIdentity, mirrorCodexAppServerTranscript } from "./transcript-mirror.js";
181819+const emitSessionTranscriptUpdateMock = vi.hoisted(() => vi.fn());
20+21+vi.mock("openclaw/plugin-sdk/agent-harness-runtime", async (importOriginal) => {
22+const actual = await importOriginal<typeof import("openclaw/plugin-sdk/agent-harness-runtime")>();
23+return {
24+ ...actual,
25+emitSessionTranscriptUpdate: emitSessionTranscriptUpdateMock,
26+};
27+});
28+1929type MirroredAgentMessage = Extract<AgentMessage, { role: "user" | "assistant" | "toolResult" }>;
20302131// Mirrors transcript-mirror.ts's fallback fingerprint exactly so test
@@ -29,6 +39,7 @@ const tempDirs: string[] = [];
29393040afterEach(async () => {
3141resetGlobalHookRunner();
42+emitSessionTranscriptUpdateMock.mockReset();
3243for (const dir of tempDirs.splice(0)) {
3344await fs.rm(dir, { recursive: true, force: true });
3445}
@@ -105,6 +116,79 @@ describe("mirrorCodexAppServerTranscript", () => {
105116);
106117});
107118119+it("emits message-bearing updates for newly appended mirrored messages only", async () => {
120+const sessionFile = await createTempSessionFile();
121+const userMessage = attachCodexMirrorIdentity(
122+makeAgentUserMessage({
123+content: [{ type: "text", text: "show me live" }],
124+timestamp: Date.now(),
125+}),
126+"turn-1:prompt",
127+);
128+129+await mirrorCodexAppServerTranscript({
130+ sessionFile,
131+sessionKey: "agent:main:main",
132+messages: [userMessage],
133+idempotencyScope: "codex-app-server:thread-1",
134+});
135+await mirrorCodexAppServerTranscript({
136+ sessionFile,
137+sessionKey: "agent:main:main",
138+messages: [userMessage],
139+idempotencyScope: "codex-app-server:thread-1",
140+});
141+142+const updates = emitSessionTranscriptUpdateMock.mock.calls.map(
143+([update]) => update as Record<string, unknown>,
144+);
145+expect(updates).toHaveLength(1);
146+expect(updates[0]?.sessionFile).toBe(sessionFile);
147+expect(updates[0]?.sessionKey).toBe("agent:main:main");
148+expect(updates[0]?.messageId).toEqual(expect.any(String));
149+expect(updates[0]?.message).toMatchObject({
150+role: "user",
151+content: [{ type: "text", text: "show me live" }],
152+idempotencyKey: "codex-app-server:thread-1:turn-1:prompt",
153+});
154+expect(updates[0]?.messageSeq).toBe(1);
155+});
156+157+it("emits stable sequence numbers for multi-message mirror batches", async () => {
158+const sessionFile = await createTempSessionFile();
159+160+await mirrorCodexAppServerTranscript({
161+ sessionFile,
162+sessionKey: "agent:main:main",
163+messages: [
164+attachCodexMirrorIdentity(
165+makeAgentUserMessage({
166+content: [{ type: "text", text: "first" }],
167+timestamp: Date.now(),
168+}),
169+"turn-1:prompt",
170+),
171+attachCodexMirrorIdentity(
172+makeAgentAssistantMessage({
173+content: [{ type: "text", text: "second" }],
174+timestamp: Date.now() + 1,
175+}),
176+"turn-1:assistant",
177+),
178+],
179+idempotencyScope: "codex-app-server:thread-1",
180+});
181+182+const updates = emitSessionTranscriptUpdateMock.mock.calls.map(
183+([update]) => update as Record<string, unknown>,
184+);
185+expect(updates.map((update) => update.messageSeq)).toEqual([1, 2]);
186+expect(updates.map((update) => (update.message as { role?: string }).role)).toEqual([
187+"user",
188+"assistant",
189+]);
190+});
191+108192it("creates the transcript directory on first mirror", async () => {
109193const root = await makeRoot("openclaw-codex-transcript-missing-dir-");
110194const sessionFile = path.join(root, "nested", "sessions", "session.jsonl");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。