fix: accept minimax apply-patch add delete quirks
This commit is contained in:
@@ -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: <path>` 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 <route> apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content."
|
||||
"Prefer `trans <route> 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: <path>` 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: <path>` 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<num
|
||||
try {
|
||||
const parsed = parseApplyPatchV2(patchText);
|
||||
const plan = await applyPatchV2Hunks(options.executor, parsed.hunks);
|
||||
for (const hint of parsed.hints) stderr.write(`${hint}\n`);
|
||||
options.stdout.write("Success. Updated the following files:\n");
|
||||
for (const item of plan.changed) options.stdout.write(`${item}\n`);
|
||||
return 0;
|
||||
|
||||
@@ -913,15 +913,35 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
"*** 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<JsonRecord> {
|
||||
"*** 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",
|
||||
|
||||
Reference in New Issue
Block a user