mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-29 13:41:10 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user