From 1d542d82b5e08af98a953122818a22b451aeb087 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 3 Jun 2026 09:58:20 +0000 Subject: [PATCH] fix: accept minimax apply-patch add delete quirks --- scripts/src/apply-patch-v2.ts | 84 ++++++++++------------ scripts/ssh-argv-guidance-contract-test.ts | 45 ++++++++---- 2 files changed, 68 insertions(+), 61 deletions(-) diff --git a/scripts/src/apply-patch-v2.ts b/scripts/src/apply-patch-v2.ts index 204650f2..bbe45995 100644 --- a/scripts/src/apply-patch-v2.ts +++ b/scripts/src/apply-patch-v2.ts @@ -16,6 +16,7 @@ export interface UpdateChunk { export interface PatchParseResult { patch: string; hunks: PatchHunk[]; + hints: string[]; } export interface PatchUpdateResult { @@ -119,7 +120,8 @@ export function applyPatchV2HelpPayload() { "Add File has no @@ hunk marker: put content immediately after `*** Add File: ` and prefix every content line with +.", "A blank line in Add File is a line containing only +.", "Update File uses @@ or @@ context markers, followed by context lines starting with space and changed lines starting with - or +.", - "Prefer `trans apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content." + "Prefer `trans apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content.", + "MiniMax compatibility: stray @@ or unprefixed content inside Add File, and extra hunk/body lines after Delete File, are accepted with stderr hints." ], examples: { addFile: [ @@ -159,6 +161,7 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult { if (last !== endMarker) throw new ApplyPatchV2Error(`invalid patch: last line must be '${endMarker}'`); const hunks: PatchHunk[] = []; + const hints: string[] = []; let index = 1; const environmentLine = lines[index]?.trimStart() ?? ""; if (environmentLine.startsWith(environmentIdMarker)) { @@ -178,10 +181,30 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult { const added: string[] = []; while (index < lines.length - 1 && !isFileHeader(lines[index] ?? "")) { const addLine = lines[index] ?? ""; - if (!addLine.startsWith("+")) { - throw invalidAddFileLineError(addLine, index + 1, filePath); + if (addLine.startsWith("+")) { + if (addLine.length > 1 && addLine.slice(1).trim().length === 0) { + hints.push(`apply-patch hint: accepted MiniMax-style whitespace-only Add File line in ${filePath} on line ${index + 1}; prefer a line containing only + for blank lines.`); + added.push(""); + index += 1; + continue; + } + added.push(addLine.slice(1)); + index += 1; + continue; } - added.push(addLine.slice(1)); + if (addLine.trimStart().startsWith("@@")) { + hints.push(`apply-patch hint: accepted MiniMax-style @@ inside Add File ${filePath} on line ${index + 1}; Add File does not need @@.`); + index += 1; + continue; + } + if (addLine.trim().length === 0) { + hints.push(`apply-patch hint: accepted a bare blank line inside Add File ${filePath} on line ${index + 1}; prefer a line containing only +.`); + added.push(""); + index += 1; + continue; + } + hints.push(`apply-patch hint: accepted unprefixed Add File content in ${filePath} on line ${index + 1}; prefix new-file content lines with +.`); + added.push(addLine); index += 1; } hunks.push({ kind: "add", path: filePath, content: joinLinesWithFinalNewline(added) }); @@ -190,9 +213,13 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult { if (line.startsWith(deleteFileMarker)) { const filePath = validatePatchPath(line.slice(deleteFileMarker.length), index + 1); index += 1; - const nextContentIndex = nextNonBlankLineIndex(lines, index, lines.length - 1); - if (nextContentIndex < lines.length - 1 && !isFileHeader(lines[nextContentIndex] ?? "")) { - throw invalidDeleteFileLineError(lines[nextContentIndex] ?? "", nextContentIndex + 1, filePath); + const extraLines: number[] = []; + while (index < lines.length - 1 && !isFileHeader(lines[index] ?? "")) { + if ((lines[index] ?? "").trim().length > 0) extraLines.push(index + 1); + index += 1; + } + if (extraLines.length > 0) { + hints.push(`apply-patch hint: ignored extra MiniMax-style hunk/body lines after Delete File ${filePath} on line ${extraLines[0]}; Delete File only needs the header.`); } hunks.push({ kind: "delete", path: filePath }); continue; @@ -222,47 +249,7 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult { throw new ApplyPatchV2Error("invalid hunk header", { line: index + 1, text: line }); } - return { patch, hunks }; -} - -function invalidAddFileLineError(lineText: string, line: number, filePath: string): ApplyPatchV2Error { - const details = { line, path: filePath, text: lineText }; - if (lineText.trimStart().startsWith("@@")) { - return new ApplyPatchV2Error( - "invalid add file line: remove @@ for Add File. Add File sections do not use hunk markers; put content directly after the header and prefix every content line with +.", - details, - ); - } - if (lineText.trim().length === 0) { - return new ApplyPatchV2Error( - "invalid add file line: blank lines in Add File must be written as a line containing only +.", - details, - ); - } - return new ApplyPatchV2Error( - "invalid add file line: every Add File content line must start with +. Add File has no @@ marker; use @@ only under Update File.", - details, - ); -} - -function invalidDeleteFileLineError(lineText: string, line: number, filePath: string): ApplyPatchV2Error { - const details = { line, path: filePath, text: lineText }; - if (lineText.trimStart().startsWith("@@")) { - return new ApplyPatchV2Error( - "invalid delete file line: remove @@ for Delete File. Delete File sections contain only the `*** Delete File: ` header; do not include hunk markers or file content.", - details, - ); - } - return new ApplyPatchV2Error( - "invalid delete file line: Delete File sections contain only the `*** Delete File: ` header. Remove old file content lines such as -old line.", - details, - ); -} - -function nextNonBlankLineIndex(lines: string[], start: number, endExclusive: number): number { - let index = start; - while (index < endExclusive && (lines[index] ?? "").trim().length === 0) index += 1; - return index; + return { patch, hunks, hints }; } export function deriveUpdatedContent(filePath: string, originalContent: string, chunks: UpdateChunk[]): PatchUpdateResult { @@ -291,6 +278,7 @@ export async function runApplyPatchV2(options: ApplyPatchV2Options): Promise { "*** End Patch", "", ].join("\n"), {}, { stderrOutput: true }); - assertCondition(addFileWithHunkMarkerV2.exitCode === 1 && addFileWithHunkMarkerV2.error === null, "v2 CLI path should return a visible parse error for Add File @@ misuse", addFileWithHunkMarkerV2); - assertCondition(addFileWithHunkMarkerV2.stdout === "", "v2 Add File parse failures should keep Codex-style empty stdout", addFileWithHunkMarkerV2); + assertCondition(addFileWithHunkMarkerV2.exitCode === 0 && addFileWithHunkMarkerV2.error === null, "v2 should accept MiniMax-style Add File @@ misuse with a hint", addFileWithHunkMarkerV2); + assertCondition(addFileWithHunkMarkerV2.stdout.includes("A bad-add.txt"), "v2 MiniMax-style Add File should still apply the new file", addFileWithHunkMarkerV2); assertCondition( - addFileWithHunkMarkerV2.stderr.includes("remove @@ for Add File") - && addFileWithHunkMarkerV2.stderr.includes("prefix every content line with +"), - "v2 Add File @@ misuse should tell agents exactly how to repair the patch", + addFileWithHunkMarkerV2.stderr.includes("accepted MiniMax-style @@ inside Add File bad-add.txt") + && addFileWithHunkMarkerV2.stderr.includes("Add File does not need @@"), + "v2 MiniMax-style Add File compatibility should still hint the canonical syntax", addFileWithHunkMarkerV2.stderr, ); - assertCondition(!addFileWithHunkMarkerV2.commands.some((command) => command.startsWith("write-b64") || command.startsWith("delete")), "v2 Add File parse failures must not touch remote files", addFileWithHunkMarkerV2.commands); + assertCondition(addFileWithHunkMarkerV2.files["bad-add.txt"] === "content\n", "v2 MiniMax-style Add File compatibility should preserve content", addFileWithHunkMarkerV2); + + const addFileBareLinesV2 = await applyPatchV2FixtureAttempt([ + "*** Begin Patch", + "*** Add File: loose-add.txt", + "first", + "", + "+ ", + "+third", + "*** End Patch", + "", + ].join("\n"), {}, { stderrOutput: true }); + assertCondition(addFileBareLinesV2.exitCode === 0 && addFileBareLinesV2.error === null, "v2 should accept MiniMax-style Add File bare content and blank lines with hints", addFileBareLinesV2); + assertCondition(addFileBareLinesV2.files["loose-add.txt"] === "first\n\n\nthird\n", "v2 MiniMax-style Add File loose lines should preserve intended content", addFileBareLinesV2); + assertCondition( + addFileBareLinesV2.stderr.includes("accepted unprefixed Add File content in loose-add.txt") + && addFileBareLinesV2.stderr.includes("accepted a bare blank line inside Add File loose-add.txt") + && addFileBareLinesV2.stderr.includes("accepted MiniMax-style whitespace-only Add File line in loose-add.txt"), + "v2 MiniMax-style Add File loose lines should emit canonical syntax hints", + addFileBareLinesV2.stderr, + ); const deleteFileWithHunkMarkerV2 = await applyPatchV2FixtureAttempt([ "*** Begin Patch", @@ -931,16 +951,15 @@ export async function runSshArgvGuidanceContract(): Promise { "*** End Patch", "", ].join("\n"), { "bad-delete.txt": "content\n" }, { stderrOutput: true }); - assertCondition(deleteFileWithHunkMarkerV2.exitCode === 1 && deleteFileWithHunkMarkerV2.error === null, "v2 CLI path should return a visible parse error for Delete File @@ misuse", deleteFileWithHunkMarkerV2); - assertCondition(deleteFileWithHunkMarkerV2.stdout === "", "v2 Delete File parse failures should keep Codex-style empty stdout", deleteFileWithHunkMarkerV2); + assertCondition(deleteFileWithHunkMarkerV2.exitCode === 0 && deleteFileWithHunkMarkerV2.error === null, "v2 should accept MiniMax-style Delete File @@ misuse with a hint", deleteFileWithHunkMarkerV2); + assertCondition(deleteFileWithHunkMarkerV2.stdout.includes("D bad-delete.txt"), "v2 MiniMax-style Delete File should still delete the file", deleteFileWithHunkMarkerV2); assertCondition( - deleteFileWithHunkMarkerV2.stderr.includes("remove @@ for Delete File") - && deleteFileWithHunkMarkerV2.stderr.includes("do not include hunk markers or file content"), - "v2 Delete File @@ misuse should tell agents exactly how to repair the patch", + deleteFileWithHunkMarkerV2.stderr.includes("ignored extra MiniMax-style hunk/body lines after Delete File bad-delete.txt") + && deleteFileWithHunkMarkerV2.stderr.includes("Delete File only needs the header"), + "v2 MiniMax-style Delete File compatibility should still hint the canonical syntax", deleteFileWithHunkMarkerV2.stderr, ); - assertCondition(deleteFileWithHunkMarkerV2.files["bad-delete.txt"] === "content\n", "v2 Delete File parse failures must not delete remote files", deleteFileWithHunkMarkerV2); - assertCondition(!deleteFileWithHunkMarkerV2.commands.some((command) => command.startsWith("delete")), "v2 Delete File parse failures must not issue remote delete operations", deleteFileWithHunkMarkerV2.commands); + assertCondition(deleteFileWithHunkMarkerV2.files["bad-delete.txt"] === undefined, "v2 MiniMax-style Delete File compatibility should delete the file", deleteFileWithHunkMarkerV2); const sequentialCompoundV2 = await applyPatchV2Fixture([ "*** Begin Patch",