fix: bulk windows apply-patch updates
This commit is contained in:
@@ -5,7 +5,7 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, wr
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { sshHelp } from "./src/help";
|
||||
import { runApplyPatchV2, type ApplyPatchV2TimingSummary } from "./src/apply-patch-v2";
|
||||
import { runApplyPatchV2, type ApplyPatchV2TimingSummary, type ApplyPatchV2BulkReplacementWritePlan } from "./src/apply-patch-v2";
|
||||
import { providerTriageRecommendedCrossChecks } from "./src/provider-triage";
|
||||
import { extractRemoteCliOptions, remoteSshFrontendPlanForTest } from "./src/remote";
|
||||
import { runSshFileTransferOperation, type SshFileTransferCommandBuilders, type SshRemoteCommandExecutor } from "./src/ssh-file-transfer";
|
||||
@@ -341,6 +341,95 @@ async function applyPatchV2Fixture(patch: string, files: Record<string, string>)
|
||||
return { stdout: result.stdout, files: result.files, commands: result.commands };
|
||||
}
|
||||
|
||||
async function applyPatchV2FsBulkFixtureAttempt(patch: string, files: Record<string, string>): Promise<{ stdout: string; stderr: string; exitCode: number | null; files: Record<string, string>; operations: string[]; error: unknown | null }> {
|
||||
const state = new Map(Object.entries(files));
|
||||
const operations: string[] = [];
|
||||
const stdin = new PassThrough();
|
||||
stdin.end(patch);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const stdoutSink = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
stdout += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const stderrSink = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
stderr += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
let error: unknown | null = null;
|
||||
let exitCode: number | null = null;
|
||||
try {
|
||||
exitCode = await runApplyPatchV2({
|
||||
stdin,
|
||||
stdout: stdoutSink,
|
||||
stderr: stderrSink,
|
||||
executor: {
|
||||
fs: {
|
||||
async stat(filePath) {
|
||||
operations.push(`stat ${filePath}`);
|
||||
const content = state.get(filePath);
|
||||
if (content === undefined) throw new Error(`missing ${filePath}`);
|
||||
const buffer = Buffer.from(content, "utf8");
|
||||
return { bytes: buffer.length, sha256: sha256BufferHex(buffer) };
|
||||
},
|
||||
async readBlock(filePath, blockIndex, blockBytes) {
|
||||
operations.push(`readBlock ${filePath}`);
|
||||
const content = state.get(filePath);
|
||||
if (content === undefined) throw new Error(`missing ${filePath}`);
|
||||
const buffer = Buffer.from(content, "utf8");
|
||||
return buffer.subarray(blockIndex * blockBytes, (blockIndex + 1) * blockBytes);
|
||||
},
|
||||
async writeFile(filePath, content) {
|
||||
operations.push(`writeFile ${filePath}`);
|
||||
state.set(filePath, content.toString("utf8"));
|
||||
},
|
||||
async deleteFile(filePath) {
|
||||
operations.push(`deleteFile ${filePath}`);
|
||||
state.delete(filePath);
|
||||
},
|
||||
async readFiles(paths) {
|
||||
operations.push(`readFiles ${paths.join(",")}`);
|
||||
const result = new Map<string, string>();
|
||||
for (const filePath of paths) {
|
||||
const content = state.get(filePath);
|
||||
if (content === undefined) throw new Error(`missing ${filePath}`);
|
||||
result.set(filePath, content);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
async applyReplacementsBulk(paths: Iterable<string>, plans: Map<string, ApplyPatchV2BulkReplacementWritePlan>) {
|
||||
const targets = Array.from(paths);
|
||||
operations.push(`applyReplacementsBulk ${targets.join(",")}`);
|
||||
for (const filePath of targets) {
|
||||
const plan = plans.get(filePath);
|
||||
const original = state.get(filePath);
|
||||
if (plan === undefined || original === undefined) throw new Error(`missing replacement plan ${filePath}`);
|
||||
const originalBuffer = Buffer.from(original, "utf8");
|
||||
assertCondition(originalBuffer.length === plan.originalBytes && sha256BufferHex(originalBuffer) === plan.originalSha256, "fs bulk fixture original integrity mismatch", { filePath, plan });
|
||||
const lines = original.split("\n");
|
||||
if (lines.at(-1) === "") lines.pop();
|
||||
for (const [start, oldLength, newLines] of [...plan.replacements].reverse()) {
|
||||
lines.splice(start, oldLength, ...newLines);
|
||||
}
|
||||
const updated = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
||||
const updatedBuffer = Buffer.from(updated, "utf8");
|
||||
assertCondition(updatedBuffer.length === plan.finalBytes && sha256BufferHex(updatedBuffer) === plan.finalSha256, "fs bulk fixture final integrity mismatch", { filePath, plan });
|
||||
state.set(filePath, updated);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
return { stdout, stderr, exitCode, files: Object.fromEntries(state), operations, error };
|
||||
}
|
||||
|
||||
function fileTransferFixture(initial: Record<string, Buffer> = {}, options: { emptyReadOnce?: Record<string, number[]>; shortReadOnce?: Record<string, Record<string, number>> } = {}): {
|
||||
state: Map<string, Buffer>;
|
||||
commands: Array<{ operation: string; stdin: boolean }>;
|
||||
@@ -865,6 +954,22 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
const bulkTiming = applyPatchTimingFromStderr(bulkV2.stderr);
|
||||
assertCondition(bulkTiming.remoteOperationCount === 2 && bulkTiming.remoteOperationCounts["read-bulk-b64"] === 1 && bulkTiming.remoteOperationCounts["apply-replacements-bulk-stdin"] === 1, "v2 timing summary should classify bulk read and line-level apply operations", bulkTiming);
|
||||
|
||||
const fsBulkV2 = await applyPatchV2FsBulkFixtureAttempt(bulkPatchLines.join("\n"), bulkFiles);
|
||||
assertCondition(fsBulkV2.exitCode === 0 && fsBulkV2.error === null, "v2 fs executor should support the same multi-file bulk update path", fsBulkV2);
|
||||
assertCondition(
|
||||
fsBulkV2.operations.length === 2
|
||||
&& fsBulkV2.operations[0]?.startsWith("readFiles ")
|
||||
&& fsBulkV2.operations[1]?.startsWith("applyReplacementsBulk "),
|
||||
"v2 fs multi-file update path should use fs bulk read and line-level apply operations",
|
||||
fsBulkV2.operations,
|
||||
);
|
||||
assertCondition(!fsBulkV2.operations.some((operation) => operation.startsWith("stat ") || operation.startsWith("readBlock ") || operation.startsWith("writeFile ")), "v2 fs bulk path should avoid per-file stat/read/write operations", fsBulkV2.operations);
|
||||
for (let fileIndex = 0; fileIndex < 4; fileIndex += 1) {
|
||||
assertCondition(fsBulkV2.files[`bulk-${fileIndex}.txt`]?.includes(`file=${fileIndex} line=40 value=beta`), "v2 fs bulk path should write all changed files", { fileIndex, files: fsBulkV2.files });
|
||||
}
|
||||
const fsBulkTiming = applyPatchTimingFromStderr(fsBulkV2.stderr);
|
||||
assertCondition(fsBulkTiming.remoteOperationCount === 2 && fsBulkTiming.remoteOperationCounts["fs.readFiles"] === 1 && fsBulkTiming.remoteOperationCounts["fs.applyReplacementsBulk"] === 1, "v2 timing summary should classify fs bulk read and line-level apply operations", fsBulkTiming);
|
||||
|
||||
const unprefixedUpdateContextV2 = await applyPatchV2FixtureAttempt([
|
||||
"*** Begin Patch",
|
||||
"*** Update File: internal/cloud/access-control.ts",
|
||||
|
||||
Reference in New Issue
Block a user