Files
openclaw/src/agents/cli-output-error.test.ts
Monkey-wusky e6ac6c8506 fix(agents,skills): keep session-identity and experience-review transcript truncation UTF-16 safe (#106370)
* fix(agents,skills): keep session-identity and experience-review transcript truncation UTF-16 safe

normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.

Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.

Follows the pattern established by #98644, #102470, #101728 and the broader
UTF-16 safety sweep already applied across the codebase.

* chore: re-trigger CI after PR body update

* test(agents,skills): construct transcripts exceeding 60k chars, verify old slices dangle

Both experience-review fixtures now exceed EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS
so the truncation branch actually fires. Pre-condition assertions prove the old raw
.slice(0,6000) / .slice(tailStart) produce isolated surrogates; the production
functions (formatSkillExperienceReviewTranscript, formatCliOutputError) do not.

cli-output test also adds pre-condition check showing normalizeCliContextValue
with raw .slice(0,200) would split a surrogate pair.

* test: tighten UTF-16 truncation coverage

Co-authored-by: 毛宇豪0668001457 <mao.yuhao@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-16 09:53:58 -07:00

23 lines
746 B
TypeScript

import { describe, expect, it } from "vitest";
import { formatCliOutputError } from "./cli-output.js";
function hasDanglingSurrogate(value: string): boolean {
return /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
}
describe("formatCliOutputError", () => {
it("keeps truncated session identity UTF-16 safe", () => {
const sessionId = `${"s".repeat(199)}😀tail`;
expect(hasDanglingSurrogate(sessionId.slice(0, 200))).toBe(true);
const error = formatCliOutputError({
text: "",
sessionId,
terminalFailure: { reason: "max_turns" },
});
expect(hasDanglingSurrogate(error)).toBe(false);
expect(error).toContain(`Claude session: ${"s".repeat(199)}.`);
});
});