fix(doctor): preserve Unicode in migration issue reports (#109592)

* fix(doctor): preserve Unicode in migration issue reports

* fix(doctor): preserve migration report Unicode boundaries

* test(doctor): stabilize migration Unicode boundary fixture

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Wynne668
2026-07-17 12:03:52 +08:00
committed by GitHub
parent 1fc1b58ec8
commit 0bb2ed321c
2 changed files with 71 additions and 6 deletions

View File

@@ -2,6 +2,7 @@
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { z } from "zod";
import { resolveStateDir } from "../config/paths.js";
import * as replaceFile from "../infra/replace-file.js";
@@ -459,14 +460,13 @@ export function createSessionSqliteMigrationFailureIssue(
"OpenClaw doctor generated this sanitized report from a local session SQLite migration recovery.",
"",
reportBody,
]
.join("\n")
.slice(0, 20_000);
].join("\n");
const boundedBody = truncateUtf16Safe(body, 20_000);
return {
body,
body: boundedBody,
...(bodyPath ? { bodyPath } : {}),
title,
url: createPrefilledGithubIssueUrl(title, body),
url: createPrefilledGithubIssueUrl(title, boundedBody),
};
}
@@ -862,7 +862,7 @@ function manifestSortTime(manifest: SessionSqliteMigrationManifest): number {
function createPrefilledGithubIssueUrl(title: string, body: string): string {
const urlBody =
body.length > 6_000
? `${body.slice(0, 6_000)}\n\n...(truncated for URL; see local failure report for the full sanitized body)`
? `${truncateUtf16Safe(body, 6_000)}\n\n...(truncated for URL; see local failure report for the full sanitized body)`
: body;
const params = new URLSearchParams({
body: urlBody,

View File

@@ -24,6 +24,7 @@ import {
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import {
assertSafeSessionSqliteMigrationMove,
createSessionSqliteMigrationFailureIssue,
restoreSessionSqliteMigrationRun,
type ActiveSessionSqliteMigrationRun,
} from "./doctor-session-sqlite-migration-run.js";
@@ -1562,6 +1563,70 @@ describe("runDoctorSessionSqlite", () => {
expect(recover.supportIssue?.url).toContain("github.com/openclaw/openclaw/issues/new");
});
it("keeps truncated GitHub issue bodies on a valid UTF-16 boundary", () => {
const store = createLegacyStore();
const manifestPath = path.join(store.tempDir, "failed-migration.json");
const writeManifest = (messages: string[], targetCount = 1) => {
const manifest: SessionSqliteMigrationManifest = {
failedAt: "2030-01-01T00:00:00.000Z",
manifestVersion: 2,
openClawVersion: "test",
runId: "utf16-boundary",
startedAt: "2030-01-01T00:00:00.000Z",
targets: Array.from({ length: targetCount }, (_, index) => {
const targetMessages = index === targetCount - 1 ? messages : ["x".repeat(500)];
return {
agentId:
targetCount === 1
? "agent-with-long-name-".repeat(10)
: `agent-${index}-${"long-name-".repeat(10)}`,
completedMoves: [],
issues: targetMessages.map((message) => ({ code: "startup_failure", message })),
plannedMoves: [],
sqlitePath: path.join(store.tempDir, "openclaw-agent.sqlite"),
storePath: store.storePath,
validationBeforeArchive: "failed",
};
}),
};
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
};
const baseMessages = Array.from({ length: 9 }, () => "x".repeat(500));
writeManifest([...baseMessages, "MESSAGE_START"]);
const probe = createSessionSqliteMigrationFailureIssue(manifestPath);
const messageOffset = probe?.body.indexOf("MESSAGE_START") ?? -1;
expect(5_999 - messageOffset).toBeLessThan(500);
writeManifest([...baseMessages, `${"x".repeat(5_999 - messageOffset)}🎉tail`]);
const issue = createSessionSqliteMigrationFailureIssue(manifestPath);
expect(issue?.body).toContain("🎉tail");
const urlBody = new URL(issue?.url ?? "").searchParams.get("body");
expect(urlBody).not.toContain("<22>");
expect(urlBody).toContain("truncated for URL");
let bodyTargetCount = 0;
let bodyMessageOffset = -1;
for (let count = 1; count < 50; count += 1) {
writeManifest(["BODY_START"], count);
const candidateIssue = createSessionSqliteMigrationFailureIssue(manifestPath);
const candidateOffset = candidateIssue?.body.indexOf("BODY_START") ?? -1;
if (candidateOffset < 0) {
break;
}
bodyTargetCount = count;
bodyMessageOffset = candidateOffset;
}
expect(bodyMessageOffset).toBeGreaterThanOrEqual(0);
expect(19_999 - bodyMessageOffset).toBeLessThan(600);
writeManifest([`${"x".repeat(19_999 - bodyMessageOffset)}🎉tail`], bodyTargetCount);
const bodyIssue = createSessionSqliteMigrationFailureIssue(manifestPath);
expect(bodyIssue?.body).not.toMatch(
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u,
);
});
it("recovers only manifests matching an explicit store selector", async () => {
const store = createLegacyStore();
const importReport = await runDoctorSessionSqlite({