


















@@ -0,0 +1,115 @@
1+import type { AgentMessage } from "@mariozechner/pi-agent-core";
2+import { describe, expect, it } from "vitest";
3+import { normalizeAssistantReplayContent } from "./replay-history.js";
4+5+const FALLBACK_TEXT = "[assistant turn failed before producing content]";
6+7+function bedrockAssistant(
8+content: unknown,
9+stopReason: "error" | "stop" | "toolUse" | "length" = "error",
10+): AgentMessage {
11+return {
12+role: "assistant",
13+ content,
14+api: "bedrock-converse-stream",
15+provider: "amazon-bedrock",
16+model: "anthropic.claude-3-haiku-20240307-v1:0",
17+usage: {
18+input: 0,
19+output: 0,
20+cacheRead: 0,
21+cacheWrite: 0,
22+totalTokens: 0,
23+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
24+},
25+ stopReason,
26+timestamp: 0,
27+} as unknown as AgentMessage;
28+}
29+30+function userMessage(text: string): AgentMessage {
31+return { role: "user", content: text, timestamp: 0 } as unknown as AgentMessage;
32+}
33+34+function openclawTranscriptAssistant(model: "delivery-mirror" | "gateway-injected"): AgentMessage {
35+return {
36+role: "assistant",
37+content: [{ type: "text", text: "channel mirror" }],
38+api: "openai-responses",
39+provider: "openclaw",
40+ model,
41+usage: {
42+input: 0,
43+output: 0,
44+cacheRead: 0,
45+cacheWrite: 0,
46+totalTokens: 0,
47+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
48+},
49+stopReason: "stop",
50+timestamp: 0,
51+} as unknown as AgentMessage;
52+}
53+54+describe("normalizeAssistantReplayContent", () => {
55+it("converts assistant content: [] to a non-empty sentinel text block when stopReason is error", () => {
56+const messages = [userMessage("hello"), bedrockAssistant([], "error")];
57+const out = normalizeAssistantReplayContent(messages);
58+expect(out).not.toBe(messages);
59+const repaired = out[1] as AgentMessage & { content: { type: string; text: string }[] };
60+expect(repaired.content).toEqual([{ type: "text", text: FALLBACK_TEXT }]);
61+});
62+63+it("preserves silent-reply turns (stopReason=stop, content=[]) untouched", () => {
64+// run.empty-error-retry.test.ts treats `stopReason:"stop"` + `content:[]`
65+// as a legitimate NO_REPLY / silent-reply, NOT a crash. Substituting the
66+// failure sentinel here would inject a fabricated "[assistant turn failed
67+// before producing content]" into the next provider request and change
68+// model behavior even though no failure occurred.
69+const silentStop = bedrockAssistant([], "stop");
70+const messages = [userMessage("hello"), silentStop];
71+const out = normalizeAssistantReplayContent(messages);
72+expect(out).toBe(messages);
73+expect(out[1]).toBe(silentStop);
74+});
75+76+it("preserves empty content with non-error stopReasons (toolUse, length) untouched", () => {
77+// Boundary lock: only `stopReason:"error"` should trip the sentinel
78+// substitution. `toolUse` and `length` are reachable in practice when a
79+// provider terminates a turn before a content block is emitted, and
80+// rewriting them as a failure would lie about what happened.
81+const toolUse = bedrockAssistant([], "toolUse");
82+const length = bedrockAssistant([], "length");
83+const messages = [userMessage("hello"), toolUse, length];
84+const out = normalizeAssistantReplayContent(messages);
85+expect(out).toBe(messages);
86+expect(out[1]).toBe(toolUse);
87+expect(out[2]).toBe(length);
88+});
89+90+it("wraps legacy string assistant content as a single text block (regression)", () => {
91+const messages = [userMessage("hi"), bedrockAssistant("plain string content")];
92+const out = normalizeAssistantReplayContent(messages);
93+const wrapped = out[1] as AgentMessage & { content: { type: string; text: string }[] };
94+expect(wrapped.content).toEqual([{ type: "text", text: "plain string content" }]);
95+});
96+97+it("filters openclaw delivery-mirror and gateway-injected assistant messages from replay", () => {
98+const messages = [
99+userMessage("hello"),
100+openclawTranscriptAssistant("delivery-mirror"),
101+bedrockAssistant([{ type: "text", text: "real reply" }]),
102+openclawTranscriptAssistant("gateway-injected"),
103+];
104+const out = normalizeAssistantReplayContent(messages);
105+expect(out).toHaveLength(2);
106+expect((out[0] as { role: string }).role).toBe("user");
107+expect((out[1] as { provider: string }).provider).toBe("amazon-bedrock");
108+});
109+110+it("returns the original array reference when nothing needs to change", () => {
111+const messages = [userMessage("hello"), bedrockAssistant([{ type: "text", text: "fine" }])];
112+const out = normalizeAssistantReplayContent(messages);
113+expect(out).toBe(messages);
114+});
115+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。