fix: add transactional remote patch v2

This commit is contained in:
Codex
2026-05-26 17:38:09 +00:00
parent 6a596bc452
commit 98715ce2be
8 changed files with 1278 additions and 40 deletions
+332 -3
View File
@@ -1,8 +1,11 @@
import { PassThrough, Writable } from "node:stream";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { sshHelp } from "./src/help";
import { runApplyPatchV2 } from "./src/apply-patch-v2";
import { providerTriageRecommendedCrossChecks } from "./src/provider-triage";
import { extractRemoteCliOptions, remoteSshFrontendPlanForTest } from "./src/remote";
import {
@@ -35,6 +38,10 @@ function assertThrows(fn: () => unknown, pattern: RegExp, message: string): void
throw new Error(`${message}: expected throw`);
}
function sha256Hex(value: string): string {
return createHash("sha256").update(Buffer.from(value, "utf8")).digest("hex");
}
function decodeWinEncodedCommand(remoteCommand: string | null | undefined): string {
const text = String(remoteCommand ?? "");
const match = /'-EncodedCommand' '([^']+)'/u.exec(text);
@@ -71,7 +78,135 @@ function applyPatchFixture(args: string[], patch: string, files: Record<string,
}
}
export function runSshArgvGuidanceContract(): JsonRecord {
async function applyPatchV2FixtureAttempt(patch: string, files: Record<string, string>): Promise<{ stdout: string; files: Record<string, string>; commands: string[]; error: unknown | null }> {
const state = new Map(Object.entries(files));
const commands: string[] = [];
const stdin = new PassThrough();
stdin.end(patch);
let stdout = "";
const stdoutSink = new Writable({
write(chunk, _encoding, callback) {
stdout += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
callback();
},
});
let error: unknown | null = null;
try {
await runApplyPatchV2({
stdin,
stdout: stdoutSink,
executor: {
async run(command, input) {
const operation = command[4] ?? "";
const target = command[5] ?? "";
commands.push([operation, ...command.slice(5)].join(" "));
if (operation === "read") {
if (!state.has(target)) return { exitCode: 1, stdout: "", stderr: `missing ${target}` };
return { exitCode: 0, stdout: state.get(target) ?? "", stderr: "" };
}
if (operation === "write-b64-argv") {
const expectedBytes = Number(command[6] ?? "-1");
const expectedSha256 = command[7] ?? "";
const content = Buffer.from(command.slice(8).join(""), "base64").toString("utf8");
if (Buffer.byteLength(content, "utf8") !== expectedBytes || sha256Hex(content) !== expectedSha256) {
return { exitCode: 23, stdout: "", stderr: "mock integrity mismatch" };
}
state.set(target, content);
return { exitCode: 0, stdout: "", stderr: "" };
}
if (operation === "write-b64-stdin") {
const expectedBytes = Number(command[6] ?? "-1");
const expectedSha256 = command[7] ?? "";
const content = Buffer.from(input ?? "", "base64").toString("utf8");
if (Buffer.byteLength(content, "utf8") !== expectedBytes || sha256Hex(content) !== expectedSha256) {
return { exitCode: 23, stdout: "", stderr: "mock integrity mismatch" };
}
state.set(target, content);
return { exitCode: 0, stdout: "", stderr: "" };
}
if (operation === "delete") {
state.delete(target);
return { exitCode: 0, stdout: "", stderr: "" };
}
if (operation === "move") {
state.set(command[6] ?? "", state.get(target) ?? "");
state.delete(target);
return { exitCode: 0, stdout: "", stderr: "" };
}
return { exitCode: 2, stdout: "", stderr: "bad op" };
},
},
});
} catch (caught) {
error = caught;
}
return { stdout, files: Object.fromEntries(state), commands, error };
}
async function applyPatchV2ActualShellFixtureAttempt(
patch: string,
files: Record<string, string>,
mutateInput?: (operation: string, input: string | undefined) => string | undefined,
): Promise<{ stdout: string; files: Record<string, string>; commands: string[]; error: unknown | null }> {
const root = mkdtempSync(path.join(os.tmpdir(), "unidesk-apply-patch-v2-shell-"));
const commands: string[] = [];
const stdin = new PassThrough();
stdin.end(patch);
let stdout = "";
const stdoutSink = new Writable({
write(chunk, _encoding, callback) {
stdout += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
callback();
},
});
try {
for (const [relativePath, content] of Object.entries(files)) {
const target = path.join(root, relativePath);
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, content, "utf8");
}
let error: unknown | null = null;
try {
await runApplyPatchV2({
stdin,
stdout: stdoutSink,
executor: {
async run(command, input) {
const operation = command[4] ?? "";
commands.push([operation, ...command.slice(5)].join(" "));
const run = spawnSync(command[0] ?? "sh", command.slice(1), {
cwd: root,
input: mutateInput ? mutateInput(operation, input) : input,
encoding: "utf8",
});
return {
exitCode: run.status ?? 255,
stdout: run.stdout,
stderr: run.stderr,
};
},
},
});
} catch (caught) {
error = caught;
}
const outputFiles: Record<string, string> = {};
for (const relativePath of Object.keys(files)) {
outputFiles[relativePath] = readFileSync(path.join(root, relativePath), "utf8");
}
return { stdout, files: outputFiles, commands, error };
} finally {
rmSync(root, { recursive: true, force: true });
}
}
async function applyPatchV2Fixture(patch: string, files: Record<string, string>): Promise<{ stdout: string; files: Record<string, string>; commands: string[] }> {
const result = await applyPatchV2FixtureAttempt(patch, files);
if (result.error !== null) throw result.error;
return { stdout: result.stdout, files: result.files, commands: result.commands };
}
export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
const argv = parseSshArgs(["argv", "true"]);
assertCondition(argv.invocationKind === "argv", "argv subcommand must be classified as argv", argv);
assertCondition(argv.remoteCommand === "'true'", "argv command must shell-quote each token", argv);
@@ -129,6 +264,11 @@ export function runSshArgvGuidanceContract(): JsonRecord {
assertCondition(directScriptCommand.remoteCommand === "'sed' '-n' '1,2p' 'file.txt'", "script -- command form must preserve dash-prefixed command args", directScriptCommand);
assertCondition(directScriptCommand.requiresStdin === false, "script -- command form must not wait for stdin", directScriptCommand);
const directScriptOneLiner = parseSshArgs(["script", "--", "cd /root/hwlab && git status --short --branch"]);
assertCondition(directScriptOneLiner.invocationKind === "helper", "script -- single-string command should run through a remote shell", directScriptOneLiner);
assertCondition(directScriptOneLiner.remoteCommand === "'sh' '-c' 'cd /root/hwlab && git status --short --branch'", "script -- single-string command should match the intuitive remote shell one-liner form", directScriptOneLiner);
assertCondition(directScriptOneLiner.requiresStdin === false, "script -- single-string command should not wait for stdin", directScriptOneLiner);
const shellOneLiner = parseSshArgs(["shell", "sed -n '1,2p' a && sed -n '1,2p' b"]);
assertCondition(shellOneLiner.invocationKind === "helper", "shell one-liner must be a helper operation", shellOneLiner);
assertCondition(shellOneLiner.remoteCommand === "'sh' '-c' 'sed -n '\\''1,2p'\\'' a && sed -n '\\''1,2p'\\'' b'", "shell one-liner must keep command operators inside the remote shell", shellOneLiner);
@@ -202,6 +342,191 @@ export function runSshArgvGuidanceContract(): JsonRecord {
assertCondition(hostApplyPatchLoose.requiredHelpers?.length === 1 && hostApplyPatchLoose.requiredHelpers.includes("apply_patch"), "host apply-patch must request only the apply_patch helper bootstrap", hostApplyPatchLoose);
assertCondition(remoteApplyPatchSource.includes("replace_once_with_perl") && remoteApplyPatchSource.includes("perl -0777"), "apply_patch helper must keep a fast path for large files", {});
const hostApplyPatchV2 = parseSshArgs(["v2"]);
assertCondition(hostApplyPatchV2.requiresStdin === true && hostApplyPatchV2.requiredHelpers === undefined && hostApplyPatchV2.remoteCommand === null, "host v2 must be a local engine operation, not a remote helper bootstrap", hostApplyPatchV2);
const hostApplyPatchV2Help = parseSshArgs(["v2", "--help"]);
assertCondition(hostApplyPatchV2Help.requiresStdin === false && hostApplyPatchV2Help.remoteCommand === null, "host v2 --help must not wait for patch stdin", hostApplyPatchV2Help);
const podApplyPatchV2 = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api/app", ["v2"]);
assertCondition(podApplyPatchV2.parsed.requiresStdin === true && podApplyPatchV2.parsed.remoteCommand === null, "pod v2 must be handled by the local v2 engine instead of injecting the legacy helper", podApplyPatchV2);
const longChinesePatch = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: story.md",
"@@",
"+这是一个很长很长的中文段落,用来证明远端 v2 不再依赖 shell hunk 拼接和手写长中文 search block。它只是通过本地行级 patch engine 计算新内容,然后把完整文件写回远端,所以中文、标点、长句都不应该影响 patch 解析和匹配。",
"*** End Patch",
"",
].join("\n"), {
"story.md": "开头\n",
});
assertCondition(longChinesePatch.files["story.md"]?.includes("很长很长的中文段落"), "v2 should accept pure insertion with long Chinese text", longChinesePatch);
assertCondition(longChinesePatch.stdout.includes("Success. Updated the following files:"), "v2 must print visible success output", longChinesePatch);
const lowContextV1Baseline = applyPatchFixture([], [
"*** Begin Patch",
"*** Update File: story.md",
"@@",
" 开头",
"+低上下文纯插入在 v1 会失败,但 v2 应该按 Codex 行级语义允许。",
"*** End Patch",
"",
].join("\n"), {
"story.md": "开头\n结尾\n",
});
assertCondition(lowContextV1Baseline.status !== 0 && lowContextV1Baseline.stderr.includes("insert-only without both leading and trailing context"), "v1 baseline should reject low-context pure insertion", lowContextV1Baseline);
const lowContextV2 = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: story.md",
"@@",
" 开头",
"+低上下文纯插入在 v1 会失败,但 v2 应该按 Codex 行级语义允许。",
"*** End Patch",
"",
].join("\n"), {
"story.md": "开头\n结尾\n",
});
assertCondition(lowContextV2.files["story.md"]?.includes("v2 应该按 Codex 行级语义允许"), "v2 should fix v1 low-context insertion friction", lowContextV2);
assertCondition(lowContextV2.commands.some((command) => command.includes("write-b64-argv")), "v2 should use argv write path for small remote files to work inside k3s pod exec capture", lowContextV2);
const unicodePunctuationV1Baseline = applyPatchFixture([], [
"*** Begin Patch",
"*** Update File: notes.txt",
"@@",
"-alpha - beta",
"+alpha - gamma",
"*** End Patch",
"",
].join("\n"), {
"notes.txt": "alpha beta\n",
});
assertCondition(unicodePunctuationV1Baseline.status !== 0, "v1 baseline should miss ASCII dash against typographic dash", unicodePunctuationV1Baseline);
const unicodePunctuationV2 = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: notes.txt",
"@@",
"-alpha - beta",
"+alpha - gamma",
"*** End Patch",
"",
].join("\n"), {
"notes.txt": "alpha beta\n",
});
assertCondition(unicodePunctuationV2.files["notes.txt"] === "alpha - gamma\n", "v2 should normalize common Unicode punctuation while matching expected lines", unicodePunctuationV2);
const repeatedBlockWithContext = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: repeated.txt",
"@@ section two",
"-marker",
"+patched",
"*** End Patch",
"",
].join("\n"), {
"repeated.txt": "section one\nmarker\nsection two\nmarker\n",
});
assertCondition(repeatedBlockWithContext.files["repeated.txt"] === "section one\nmarker\nsection two\npatched\n", "v2 should use @@ context to target repeated blocks", repeatedBlockWithContext);
const longChineseReplace = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: novel.md",
"@@",
"-林深在透明的舷窗前停下脚步,远处的群星像被压进黑色玻璃里的碎银,安静得让人怀疑整个宇宙都屏住了呼吸。",
"+林深在透明的舷窗前停下脚步,远处的群星像被压进黑色玻璃里的碎银,安静得让人怀疑整个宇宙正在等待他重新命名。",
"*** End Patch",
"",
].join("\n"), {
"novel.md": "林深在透明的舷窗前停下脚步,远处的群星像被压进黑色玻璃里的碎银,安静得让人怀疑整个宇宙都屏住了呼吸。\n",
});
assertCondition(longChineseReplace.files["novel.md"]?.includes("等待他重新命名"), "v2 should replace long Chinese lines without remote shell search blocks", longChineseReplace);
const largeV2 = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: large.txt",
"@@",
"+large insert",
"*** End Patch",
"",
].join("\n"), {
"large.txt": `${"0123456789abcdef\n".repeat(4096)}`,
});
assertCondition(largeV2.commands.some((command) => command.includes("write-b64-stdin")), "v2 should use stdin write path for large remote files to avoid E2BIG", largeV2.commands);
const multiChunkTailV2 = await applyPatchV2ActualShellFixtureAttempt([
"*** Begin Patch",
"*** Update File: two_chunks.txt",
"@@",
"-b",
"+B",
"@@",
"-d",
"+D",
"*** End Patch",
"",
].join("\n"), {
"two_chunks.txt": "a\nb\nc\nd\ne\nf\n",
});
assertCondition(multiChunkTailV2.error === null, "v2 should apply explicit multi-chunk patches through the real shell writer", multiChunkTailV2);
assertCondition(multiChunkTailV2.files["two_chunks.txt"] === "a\nB\nc\nD\ne\nf\n", "v2 must preserve untouched tail lines when applying multiple chunks", multiChunkTailV2);
const truncatedLargeWriteV2 = await applyPatchV2ActualShellFixtureAttempt([
"*** Begin Patch",
"*** Update File: large.txt",
"@@",
"+large insert that forces a rewritten full-file payload",
"*** End Patch",
"",
].join("\n"), {
"large.txt": `${"0123456789abcdef\n".repeat(4096)}`,
}, (operation, input) => {
if (operation !== "write-b64-stdin" || input === undefined) return input;
return input.slice(0, Math.max(0, input.length - 32));
});
assertCondition(truncatedLargeWriteV2.error !== null, "v2 should reject truncated stdin write payloads", truncatedLargeWriteV2);
assertCondition(truncatedLargeWriteV2.files["large.txt"] === `${"0123456789abcdef\n".repeat(4096)}`, "v2 must keep the original file when decoded payload integrity fails", truncatedLargeWriteV2);
assertCondition(
String((truncatedLargeWriteV2.error as Error | null)?.message ?? "").includes("remote apply-patch v2 operation failed"),
"v2 truncated payload failure should be visible to the caller",
truncatedLargeWriteV2,
);
const failedCompoundV2 = await applyPatchV2FixtureAttempt([
"*** Begin Patch",
"*** Update File: first.txt",
"@@",
"-old first",
"+new first",
"*** Update File: second.txt",
"@@",
"-missing second",
"+new second",
"*** End Patch",
"",
].join("\n"), {
"first.txt": "old first\n",
"second.txt": "old second\n",
});
assertCondition(failedCompoundV2.error !== null, "v2 compound patch should fail when a later hunk does not match", failedCompoundV2);
assertCondition(failedCompoundV2.files["first.txt"] === "old first\n", "v2 must not partially write earlier files when a later hunk fails", failedCompoundV2);
assertCondition(failedCompoundV2.files["second.txt"] === "old second\n", "v2 must leave later failed files unchanged", failedCompoundV2);
assertCondition(!failedCompoundV2.commands.some((command) => command.startsWith("write-b64") || command.startsWith("delete")), "v2 must finish all local planning before any remote mutation", failedCompoundV2.commands);
const sequentialCompoundV2 = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: sequence.txt",
"@@",
"-alpha",
"+beta",
"*** Update File: sequence.txt",
"@@",
"-beta",
"+gamma",
"*** End Patch",
"",
].join("\n"), {
"sequence.txt": "alpha\n",
});
assertCondition(sequentialCompoundV2.files["sequence.txt"] === "gamma\n", "v2 should plan later hunks against earlier planned edits before remote writes", sequentialCompoundV2);
const safePatch = applyPatchFixture([], [
"*** Begin Patch",
"*** Update File: sample.txt",
@@ -411,6 +736,9 @@ export function runSshArgvGuidanceContract(): JsonRecord {
assertCondition(String(frontendRemoteHostPatchPlan.wrappedRemoteCommand ?? "").includes("UNIDESK_SSH_TOOL_DIR=/tmp/unidesk-ssh-tools"), "host apply-patch must bootstrap the remote apply_patch helper", frontendRemoteHostPatchPlan);
assertCondition(String(frontendRemoteHostPatchPlan.wrappedRemoteCommand ?? "").includes("/apply_patch") && !String(frontendRemoteHostPatchPlan.wrappedRemoteCommand ?? "").includes("/glob") && !String(frontendRemoteHostPatchPlan.wrappedRemoteCommand ?? "").includes("/skill-discover"), "host apply-patch must not bootstrap unrelated helper tools", frontendRemoteHostPatchPlan);
const frontendRemoteV2Plan = remoteSshFrontendPlanForTest("D601:/tmp", ["v2"]);
assertCondition(frontendRemoteV2Plan.requiresStdin === true && frontendRemoteV2Plan.remoteCommand === null && !String(frontendRemoteV2Plan.wrappedRemoteCommand ?? "").includes("UNIDESK_SSH_TOOL_DIR"), "frontend v2 plan must stay a local engine operation and not bootstrap legacy helpers", frontendRemoteV2Plan);
const frontendRemotePodArgvPlan = remoteSshFrontendPlanForTest("G14:k3s:unidesk:code-queue", ["argv", "sh", "-c", "command -v tran"]);
assertCondition(frontendRemotePodArgvPlan.providerId === "G14", "remote frontend pod route must dispatch through G14 provider", frontendRemotePodArgvPlan);
assertCondition(frontendRemotePodArgvPlan.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'unidesk' 'deployment/code-queue' '--' 'sh' '-c' 'command -v tran'", "remote frontend pod argv route must be fully assembled before dispatch", frontendRemotePodArgvPlan);
@@ -454,7 +782,7 @@ export function runSshArgvGuidanceContract(): JsonRecord {
checks: [
"argv form is classified and quoted as the success path for non-interactive commands",
"stdin script form removes shell-command strings for host and k3s workload scripts",
"script -- command form executes dash-prefixed argv without waiting for stdin",
"script -- single-string runs as a remote shell one-liner while multi-token form keeps dash-prefixed argv",
"pod apply-patch operation injects helper and forwards patch stdin",
"pod exec --stdin streams arbitrary local stdin through workload routes without shell wrapping",
"apply-patch uses one sh helper for host and pod paths and rejects low-context hunks unless --allow-loose is explicit",
@@ -480,5 +808,6 @@ export function runSshArgvGuidanceContract(): JsonRecord {
}
if (import.meta.main) {
process.stdout.write(`${JSON.stringify(runSshArgvGuidanceContract(), null, 2)}\n`);
const result = await runSshArgvGuidanceContract();
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
}