Skip to content

Commit b6bc7b8

Browse files
committed
fix(prove-76729): narrow AgentMessage to Message-with-content before .content access
AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages] includes BashExecutionMessage which lacks .content, causing tsgo errors. Use a type guard (isMessageWithContent) that narrows AgentMessage to Message (= UserMessage | AssistantMessage | ToolResultMessage) before accessing .content or .text. This also lets the helper getMessageText() accept Message directly without casts. Fixes check-test-types and check-lint failures in CI.
1 parent f828f5f commit b6bc7b8

1 file changed

Lines changed: 36 additions & 19 deletions

File tree

src/agents/embedded-agent-runner/prove-76729.e2e.test.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,38 @@
33
import fs from "node:fs";
44
import os from "node:os";
55
import path from "node:path";
6+
import type { Message } from "openclaw/plugin-sdk/llm";
67
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
78
import { describe, expect, it } from "vitest";
89
import { makeAgentAssistantMessage } from "../test-helpers/agent-message-fixtures.js";
910
import {
1011
rotateTranscriptAfterCompaction,
1112
} from "./compaction-successor-transcript.js";
1213

14+
function isMessageWithContent(value: unknown): value is Message {
15+
return (
16+
typeof value === "object" &&
17+
value !== null &&
18+
"role" in value &&
19+
(value as { role: unknown }).role !== "bashExecution"
20+
);
21+
}
22+
1323
function makeAssistant(text: string, timestamp: number) {
1424
return makeAgentAssistantMessage({
1525
content: [{ type: "text", text }],
1626
timestamp,
1727
});
1828
}
1929

20-
function getMessageText(msg: { role: string; content?: unknown }): string {
30+
function getMessageText(msg: Message): string {
2131
if (typeof msg.content === "string") {
2232
return msg.content;
2333
}
2434
if (Array.isArray(msg.content)) {
25-
return msg.content.map((b: Record<string, unknown>) => String(b.text ?? "")).join(" ");
35+
return msg.content
36+
.map((b) => (b.type === "text" ? b.text : ""))
37+
.join(" ");
2638
}
2739
return "";
2840
}
@@ -65,12 +77,15 @@ describe("prove-76729 — real behavior proof", () => {
6577
proof.push("Session branch entries:");
6678
for (const entry of manager.getBranch()) {
6779
if (entry.type === "message") {
68-
const txt = typeof entry.message.content === "string"
69-
? entry.message.content
70-
: entry.message.content?.[0]?.text ?? "";
71-
proof.push(` ${entry.message.role}[t=${entry.message.timestamp}]: ${txt}`);
80+
if (isMessageWithContent(entry.message)) {
81+
const msg = entry.message;
82+
const txt = getMessageText(msg);
83+
proof.push(` [${msg.role}] "${txt}"`);
84+
} else {
85+
proof.push(` [${entry.message.role}] (no text content)`);
86+
}
7287
} else if (entry.type === "compaction") {
73-
proof.push(` compaction(firstKept=${entry.firstKeptEntryId?.slice(0, 8)}): ${entry.summary}`);
88+
proof.push(` compaction(firstKept=${entry.firstKeptEntryId.slice(0, 8)}): ${entry.summary}`);
7489
} else {
7590
proof.push(` ${entry.type}`);
7691
}
@@ -81,8 +96,10 @@ describe("prove-76729 — real behavior proof", () => {
8196
for (const msg of successor.buildSessionContext().messages) {
8297
if (msg.role === "compactionSummary") {
8398
proof.push(` [compactionSummary] summary="${msg.summary}"`);
84-
} else if ("content" in msg) {
85-
proof.push(` [${msg.role}] "${getMessageText(msg as { role: string; content: unknown })}"`);
99+
} else if (isMessageWithContent(msg)) {
100+
proof.push(` [${msg.role}] "${getMessageText(msg)}"`);
101+
} else {
102+
proof.push(` [${msg.role}] (no text content)`);
86103
}
87104
}
88105

@@ -92,27 +109,27 @@ describe("prove-76729 — real behavior proof", () => {
92109
if (m.role === "compactionSummary") {
93110
return { role: "compactionSummary", summary: m.summary, tokensBefore: m.tokensBefore };
94111
}
95-
if ("content" in m) {
96-
return { role: m.role, content: (m as { content: unknown }).content, timestamp: (m as { timestamp: number }).timestamp };
112+
if (isMessageWithContent(m)) {
113+
return { role: m.role, content: m.content, timestamp: m.timestamp };
97114
}
98115
return { role: m.role };
99116
});
100117
proof.push(JSON.stringify(json, null, 2));
101118

102119
const ctx = successor.buildSessionContext().messages;
103120
const hasAssistantPreserved = ctx.some(
104-
(m) => m.role === "assistant" && "content" in m &&
105-
getMessageText(m as { role: string; content: unknown }).includes("sunny"),
121+
(m) => m.role === "assistant" && isMessageWithContent(m) &&
122+
getMessageText(m).includes("sunny"),
106123
);
107124
const hasUserSurviving = ctx.some(
108-
(m) => m.role === "user" && "content" in m &&
109-
typeof (m as { content: unknown }).content === "string" &&
110-
((m as { content: string }).content).includes("tomorrow"),
125+
(m) => m.role === "user" && isMessageWithContent(m) &&
126+
typeof m.content === "string" &&
127+
m.content.includes("tomorrow"),
111128
);
112129
const summarizedRemoved = !ctx.some(
113-
(m) => m.role === "user" && "content" in m &&
114-
typeof (m as { content: unknown }).content === "string" &&
115-
((m as { content: string }).content).includes("weather today"),
130+
(m) => m.role === "user" && isMessageWithContent(m) &&
131+
typeof m.content === "string" &&
132+
m.content.includes("weather today"),
116133
);
117134

118135
proof.push("");

0 commit comments

Comments
 (0)