This commit is contained in:
notandrewblejde 2026-09-13 17:20:41 +08:00 committed by GitHub
commit 32a9ce05a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 0 deletions

View File

@ -442,6 +442,27 @@ describe("issue validators", () => {
expect(document.body).toBe("# Plan\n\nShip it");
});
it("preserves escaped newlines inside a JSON document body byte-for-byte", () => {
// Regression test for SPC-39026: a document body that is itself a JSON
// string (e.g. `json.dumps(brief, indent=2)`) must round-trip exactly —
// the `\n` inside the `narrative` value is a required JSON string escape,
// not literal text a client failed to turn into a line break, so it must
// not be rewritten into a raw control character.
const jsonBody = JSON.stringify(
{ narrative: "Line 1\n\nLine 2", note: "keep\\nliteral" },
null,
2,
);
const document = upsertIssueDocumentSchema.parse({
format: "markdown",
body: jsonBody,
});
expect(document.body).toBe(jsonBody);
expect(() => JSON.parse(document.body)).not.toThrow();
});
it("clamps oversized requestDepth values on create", () => {
const parsed = createIssueSchema.parse({
title: "Clamp request depth",

View File

@ -1,6 +1,19 @@
import { z } from "zod";
export function normalizeEscapedLineBreaks(value: string): string {
// Skip valid JSON payloads: their `\n`/`\r` sequences are structural string
// escapes, not literal text a client mistakenly failed to turn into a real
// line break. Rewriting those bytes turns escaped-newline JSON into raw
// control characters, breaking `JSON.parse`/`json.loads` for any consumer
// reading the stored value back as data (see SPC-39026).
try {
JSON.parse(value);
return value;
} catch {
// Not JSON — fall through to the legacy literal-escape normalization
// used for human/agent-authored markdown text.
}
return value
.replace(/\\r\\n/g, "\n")
.replace(/\\n/g, "\n")