openclaw/src/tui/components/chat-log.test.ts
lisitan f3c00fce15
fix: prevent duplicate assistant messages in TUI (fixes #35278) (#35364)
* fix: prevent duplicate assistant messages in TUI (fixes #35278)

When startAssistant() is called multiple times with the same runId,
it was creating duplicate AssistantMessageComponent instances instead
of reusing the existing one. This caused messages to appear twice in
the terminal UI.

The fix checks if a component already exists for the runId before
creating a new one. If it exists, we update its text instead of
appending a duplicate component.

Test coverage includes verification that:
- Only one component is created when startAssistant is called twice
- The second text replaces the first
- Component count remains 1 (prevents regression)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* Changelog: add TUI duplicate-render fix entry

---------

Co-authored-by: 沐沐 <mumu@example.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-03-12 02:59:42 -04:00

56 lines
1.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { ChatLog } from "./chat-log.js";
describe("ChatLog", () => {
it("caps component growth to avoid unbounded render trees", () => {
const chatLog = new ChatLog(20);
for (let i = 1; i <= 40; i++) {
chatLog.addSystem(`system-${i}`);
}
expect(chatLog.children.length).toBe(20);
const rendered = chatLog.render(120).join("\n");
expect(rendered).toContain("system-40");
expect(rendered).not.toContain("system-1");
});
it("drops stale streaming references when old components are pruned", () => {
const chatLog = new ChatLog(20);
chatLog.startAssistant("first", "run-1");
for (let i = 0; i < 25; i++) {
chatLog.addSystem(`overflow-${i}`);
}
// Should not throw if the original streaming component was pruned.
chatLog.updateAssistant("recreated", "run-1");
const rendered = chatLog.render(120).join("\n");
expect(chatLog.children.length).toBe(20);
expect(rendered).toContain("recreated");
});
it("does not append duplicate assistant components when a run is started twice", () => {
const chatLog = new ChatLog(40);
chatLog.startAssistant("first", "run-dup");
chatLog.startAssistant("second", "run-dup");
const rendered = chatLog.render(120).join("\n");
expect(rendered).toContain("second");
expect(rendered).not.toContain("first");
expect(chatLog.children.length).toBe(1);
});
it("drops stale tool references when old components are pruned", () => {
const chatLog = new ChatLog(20);
chatLog.startTool("tool-1", "read_file", { path: "a.txt" });
for (let i = 0; i < 25; i++) {
chatLog.addSystem(`overflow-${i}`);
}
// Should no-op safely after the tool component is pruned.
chatLog.updateToolResult("tool-1", { content: [{ type: "text", text: "done" }] });
expect(chatLog.children.length).toBe(20);
});
});