Skip to content

Commit 0d77e24

Browse files
committed
fix(tts): keep status details UTF-16 safe
1 parent 632efa2 commit 0d77e24

2 files changed

Lines changed: 52 additions & 1 deletion

File tree

src/tts/status-config.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ import type { OpenClawConfig } from "../config/types.js";
77
import { withEnvAsync } from "../test-utils/env.js";
88
import { resolveStatusTtsSnapshot } from "./status-config.js";
99

10+
function hasLoneSurrogate(value: string): boolean {
11+
for (let index = 0; index < value.length; index += 1) {
12+
const code = value.charCodeAt(index);
13+
if (code >= 0xd800 && code <= 0xdbff) {
14+
const next = value.charCodeAt(index + 1);
15+
if (next < 0xdc00 || next > 0xdfff) {
16+
return true;
17+
}
18+
index += 1;
19+
} else if (code >= 0xdc00 && code <= 0xdfff) {
20+
return true;
21+
}
22+
}
23+
return false;
24+
}
25+
1026
let fixtureRoot = "";
1127
let fixtureId = 0;
1228

@@ -198,6 +214,38 @@ describe("resolveStatusTtsSnapshot", () => {
198214
});
199215
});
200216

217+
it("keeps truncated status detail fields well-formed at UTF-16 boundaries", async () => {
218+
await withStatusTempHome(async () => {
219+
const displayName = `${"d".repeat(92)}😀tail`;
220+
const model = `${"m".repeat(92)}😀tail`;
221+
const voice = `${"v".repeat(92)}😀tail`;
222+
const snapshot = resolveStatusTtsSnapshot({
223+
cfg: {
224+
messages: {
225+
tts: {
226+
auto: "always",
227+
provider: "elevenlabs",
228+
providers: {
229+
elevenlabs: {
230+
displayName,
231+
model,
232+
voice,
233+
},
234+
},
235+
},
236+
},
237+
} as OpenClawConfig,
238+
});
239+
240+
expect(snapshot?.displayName).toBe(`${"d".repeat(92)}...`);
241+
expect(snapshot?.model).toBe(`${"m".repeat(92)}...`);
242+
expect(snapshot?.voice).toBe(`${"v".repeat(92)}...`);
243+
expect(hasLoneSurrogate(snapshot?.displayName ?? "")).toBe(false);
244+
expect(hasLoneSurrogate(snapshot?.model ?? "")).toBe(false);
245+
expect(hasLoneSurrogate(snapshot?.voice ?? "")).toBe(false);
246+
});
247+
});
248+
201249
it("omits default OpenAI endpoint details from status", async () => {
202250
await withStatusTempHome(async () => {
203251
expect(

src/tts/status-config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
normalizeOptionalLowercaseString,
66
normalizeOptionalString,
77
} from "@openclaw/normalization-core/string-coerce";
8+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
89
import type { OpenClawConfig } from "../config/types.js";
910
import type { TtsAutoMode, TtsConfig, TtsProvider } from "../config/types.tts.js";
1011
import { tryReadJsonSync } from "../infra/json-files.js";
@@ -114,7 +115,9 @@ function normalizeStatusDetail(
114115
if (!normalized) {
115116
return undefined;
116117
}
117-
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 3)}...` : normalized;
118+
return normalized.length > maxLength
119+
? `${truncateUtf16Safe(normalized, maxLength - 3)}...`
120+
: normalized;
118121
}
119122

120123
function sanitizeBaseUrlForStatus(value: unknown): string | undefined {

0 commit comments

Comments
 (0)