diff --git a/src/skills/workshop/service-evaluation.test.ts b/src/skills/workshop/service-evaluation.test.ts index 2c8c99fc9a13..c47d9fe4baad 100644 --- a/src/skills/workshop/service-evaluation.test.ts +++ b/src/skills/workshop/service-evaluation.test.ts @@ -759,6 +759,61 @@ describe("Skill Workshop proposal evaluation", () => { }); }); + it("does not split surrogate pairs when bounding evaluator text", async () => { + const workspaceDir = await tempDirs.make("openclaw-skill-evaluation-surrogate-"); + const proposal = await proposeCreateSkill({ + workspaceDir, + agentId: "main", + name: "Surrogate Bounds", + description: "Bound evaluator text without splitting surrogates", + content: "# Surrogate Bounds\n", + }); + hookMocks.evaluate.mockResolvedValue([ + { + evaluatorId: "emoji-bounds", + pluginId: "evaluation-tests", + status: "completed", + result: { + summary: `${"s".repeat(7_999)}🙂`, + decisionReason: `${"r".repeat(1_999)}🙂`, + metrics: { note: `${"m".repeat(3_999)}🙂` }, + }, + }, + { + evaluatorId: "emoji-error", + pluginId: "evaluation-tests", + status: "error", + error: `${"e".repeat(1_999)}🙂`, + }, + ]); + + const evaluated = await evaluateSkillProposal({ + workspaceDir, + agentId: "main", + proposalId: proposal.record.id, + expectedRevisionHash: proposal.revisionHash, + }); + + expect(evaluated.evaluation.outcomes).toEqual([ + { + evaluatorId: "emoji-bounds", + pluginId: "evaluation-tests", + status: "completed", + result: { + summary: "s".repeat(7_999), + decisionReason: "r".repeat(1_999), + metrics: { note: "m".repeat(3_999) }, + }, + }, + { + evaluatorId: "emoji-error", + pluginId: "evaluation-tests", + status: "error", + error: "e".repeat(1_999), + }, + ]); + }); + it("rejects evaluator results that exceed the aggregate persistence budget", async () => { const workspaceDir = await tempDirs.make("openclaw-skill-evaluation-size-budget-"); const proposal = await proposeCreateSkill({ diff --git a/src/skills/workshop/service-evaluation.ts b/src/skills/workshop/service-evaluation.ts index 6d677f5ac379..f34c2e3c19b1 100644 --- a/src/skills/workshop/service-evaluation.ts +++ b/src/skills/workshop/service-evaluation.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { PluginHookSkillEvaluationFinding, PluginHookSkillProposalEvaluateResult, @@ -346,18 +347,19 @@ function normalizeMetrics( ) { return undefined; } - normalized[key] = typeof value === "string" ? value.slice(0, 4_000) : value; + normalized[key] = typeof value === "string" ? truncateUtf16Safe(value, 4_000) : value; } return normalized; } function boundedRequired(value: string, maxLength: number, fallback: string): string { const normalized = normalizeOptionalString(value) ?? fallback; - return normalized.slice(0, maxLength); + return truncateUtf16Safe(normalized, maxLength); } function boundedOptional(value: string | undefined, maxLength: number): string | undefined { - return normalizeOptionalString(value)?.slice(0, maxLength); + const normalized = normalizeOptionalString(value); + return normalized === undefined ? undefined : truncateUtf16Safe(normalized, maxLength); } function storeOptions(env?: NodeJS.ProcessEnv) {