mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(ui): stop chat normalizers throwing on null/undefined message entries (#111494)
normalizeMessage / isToolResultMessage / isStandaloneToolMessageForDisplay
cast their unknown input to a record and then read `typeof m.role`. That
still evaluates `m.role`, so an undefined entry (e.g. groupMessages calling
normalizeMessage(item.message) with an absent message on a pending or
malformed transcript row) threw "Cannot read properties of undefined
(reading 'role')" inside the gateway event handler — a caught but recurring
error storm on the live team instance. Coerce non-object input to {} at the
source so every downstream `typeof m.<field>` check works and role degrades
to "unknown". The existing safeNormalizeMessage try/catch wrapper existed
precisely for this hazard; the guard now lives in normalizeMessage itself.
This commit is contained in:
@@ -1,12 +1,41 @@
|
||||
// @vitest-environment node
|
||||
// Control UI tests cover message normalizer behavior.
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { normalizeMessage } from "./message-normalizer.ts";
|
||||
import {
|
||||
isStandaloneToolMessageForDisplay,
|
||||
isToolResultMessage,
|
||||
normalizeMessage,
|
||||
} from "./message-normalizer.ts";
|
||||
|
||||
const SENDER_METADATA_BLOCK =
|
||||
'Sender (untrusted metadata):\n```json\n{"label":"openclaw-control-ui","id":"openclaw-control-ui"}\n```';
|
||||
|
||||
describe("message-normalizer", () => {
|
||||
// Regression: gateway/transcript events can carry a null/undefined or
|
||||
// non-object entry (e.g. a transcript row without a `message`). `typeof
|
||||
// m.role` still reads `.role` off the object, so an undefined entry threw
|
||||
// "Cannot read properties of undefined (reading 'role')" inside the gateway
|
||||
// event handler. These entry points must degrade to a safe default instead.
|
||||
describe("malformed input never throws", () => {
|
||||
it.each([undefined, null, "raw string", 42, true])(
|
||||
"normalizeMessage(%o) yields role 'unknown' without throwing",
|
||||
(input) => {
|
||||
expect(() => normalizeMessage(input)).not.toThrow();
|
||||
expect(normalizeMessage(input).role).toBe("unknown");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([undefined, null, "raw string", 42, true])(
|
||||
"tool-message predicates return false for %o without throwing",
|
||||
(input) => {
|
||||
expect(() => isToolResultMessage(input)).not.toThrow();
|
||||
expect(() => isStandaloneToolMessageForDisplay(input)).not.toThrow();
|
||||
expect(isToolResultMessage(input)).toBe(false);
|
||||
expect(isStandaloneToolMessageForDisplay(input)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("normalizeMessage", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
@@ -16,6 +16,16 @@ import { getMediaFileExtension } from "../media-file-extension.ts";
|
||||
import type { NormalizedMessage, MessageContentItem } from "./chat-types.ts";
|
||||
import { formatSenderLabel, normalizeSenderIdentity } from "./sender-label.ts";
|
||||
|
||||
// These normalizers take `unknown` gateway/transcript data. A malformed or
|
||||
// absent entry can arrive as null/undefined (e.g. a transcript row without a
|
||||
// `message`), and `typeof m.role` still throws "reading 'role'" when `m` itself
|
||||
// is undefined — the typeof only guards the property, not the object. Coercing
|
||||
// a non-object to `{}` keeps every downstream `typeof m.<field>` check working
|
||||
// and yields role "unknown" instead of crashing the gateway event handler.
|
||||
function asMessageRecord(message: unknown): Record<string, unknown> {
|
||||
return message && typeof message === "object" ? (message as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
export function normalizeRoleForGrouping(role: string): string {
|
||||
const lower = role.toLowerCase();
|
||||
if (lower === "user") {
|
||||
@@ -39,13 +49,13 @@ export function normalizeRoleForGrouping(role: string): string {
|
||||
}
|
||||
|
||||
export function isToolResultMessage(message: unknown): boolean {
|
||||
const m = message as Record<string, unknown>;
|
||||
const m = asMessageRecord(message);
|
||||
const role = typeof m.role === "string" ? m.role.toLowerCase() : "";
|
||||
return role === "toolresult" || role === "tool_result";
|
||||
}
|
||||
|
||||
export function isStandaloneToolMessageForDisplay(message: unknown): boolean {
|
||||
const m = message as Record<string, unknown>;
|
||||
const m = asMessageRecord(message);
|
||||
const role = typeof m.role === "string" ? normalizeRoleForGrouping(m.role) : "unknown";
|
||||
return (
|
||||
role === "tool" ||
|
||||
@@ -363,7 +373,7 @@ function expandTextContent(text: string): {
|
||||
* Normalize a raw message object into a consistent structure.
|
||||
*/
|
||||
export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
const m = message as Record<string, unknown>;
|
||||
const m = asMessageRecord(message);
|
||||
let role = typeof m.role === "string" ? m.role : "unknown";
|
||||
|
||||
// Detect tool messages by common gateway shapes.
|
||||
|
||||
Reference in New Issue
Block a user