fix: expose apply-patch missing-file errors
This commit is contained in:
@@ -425,7 +425,7 @@ async function readRemoteText(executor: ApplyPatchV2Executor, target: string): P
|
||||
const [bytesText, expectedSha256] = stat.stdout.trim().split(/\s+/u);
|
||||
const expectedBytes = Number(bytesText);
|
||||
if (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0 || !/^[0-9a-f]{64}$/u.test(expectedSha256 ?? "")) {
|
||||
throw new ApplyPatchV2Error("remote apply-patch v2 stat returned invalid metadata", { path: target, stdout: stat.stdout.slice(0, 500) });
|
||||
throw new ApplyPatchV2Error("remote apply-patch v2 stat returned invalid metadata", { path: target, stdout: stat.stdout.slice(0, 500), stderr: stat.stderr.slice(-500) });
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
@@ -606,6 +606,8 @@ function remoteV2Script(operation: RemoteV2Operation, args: string[]): string[]
|
||||
"case \"$op\" in",
|
||||
" stat)",
|
||||
" target=$1",
|
||||
" if [ ! -e \"$target\" ]; then printf 'file not found: %s\\n' \"$target\" >&2; exit 1; fi",
|
||||
" if [ -d \"$target\" ]; then printf 'not a file: %s\\n' \"$target\" >&2; exit 1; fi",
|
||||
" bytes=$(wc -c < \"$target\" | tr -d '[:space:]')",
|
||||
" digest=$(sha256_file \"$target\")",
|
||||
" printf '%s %s\\n' \"$bytes\" \"$digest\"",
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
shellArgv,
|
||||
sshFailureHint,
|
||||
sshShellCompatibilityPrelude,
|
||||
sshUserToolPathPrelude,
|
||||
sshRuntimeTimeoutHint,
|
||||
sshRuntimeTimeoutMs,
|
||||
sshRuntimeTimingHint,
|
||||
@@ -49,6 +50,10 @@ function sha256BufferHex(value: Buffer): string {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function sshShellScriptPrelude(): string {
|
||||
return `${sshUserToolPathPrelude}\n${sshShellCompatibilityPrelude}`;
|
||||
}
|
||||
|
||||
async function captureStdout(fn: () => Promise<number>): Promise<{ exitCode: number; stdout: string }> {
|
||||
const originalWrite = process.stdout.write;
|
||||
let stdout = "";
|
||||
@@ -464,12 +469,12 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
|
||||
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 === shellArgv(["sh", "-c", `${sshShellCompatibilityPrelude}\ncd /root/hwlab && git status --short --branch`]), "script -- single-string command should match the intuitive remote shell one-liner form with the compatibility prelude", directScriptOneLiner);
|
||||
assertCondition(directScriptOneLiner.remoteCommand === shellArgv(["sh", "-c", `${sshShellScriptPrelude()}\ncd /root/hwlab && git status --short --branch`]), "script -- single-string command should match the intuitive remote shell one-liner form with the compatibility prelude", 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 === shellArgv(["sh", "-c", `${sshShellCompatibilityPrelude}\nsed -n '1,2p' a && sed -n '1,2p' b`]), "shell one-liner must keep command operators inside the remote shell", shellOneLiner);
|
||||
assertCondition(shellOneLiner.remoteCommand === shellArgv(["sh", "-c", `${sshShellScriptPrelude()}\nsed -n '1,2p' a && sed -n '1,2p' b`]), "shell one-liner must keep command operators inside the remote shell", shellOneLiner);
|
||||
assertCondition(shellOneLiner.requiresStdin === false, "shell one-liner must not require stdin", shellOneLiner);
|
||||
|
||||
for (const shell of ["sh", "bash"]) {
|
||||
@@ -491,7 +496,7 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
assertCondition(routeKubectl.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'get' 'pods' '-n' 'hwlab-dev'", "D601:k3s kubectl must map to kubectl argv", routeKubectl);
|
||||
|
||||
const routeK3sShell = parseSshInvocation("D601:k3s", ["shell", "kubectl get nodes && kubectl get pods -A"]);
|
||||
assertCondition(routeK3sShell.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "sh", "-c", `${sshShellCompatibilityPrelude}\nkubectl get nodes && kubectl get pods -A`]), "D601:k3s shell must run one-line shell logic on the k3s host with native kubeconfig", routeK3sShell);
|
||||
assertCondition(routeK3sShell.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "sh", "-c", `${sshShellScriptPrelude()}\nkubectl get nodes && kubectl get pods -A`]), "D601:k3s shell must run one-line shell logic on the k3s host with native kubeconfig", routeK3sShell);
|
||||
|
||||
const g14Guard = parseSshInvocation("G14:k3s", []);
|
||||
assertCondition(g14Guard.providerId === "G14" && g14Guard.route.plane === "k3s", "G14:k3s must parse as a native k3s route", g14Guard);
|
||||
@@ -512,25 +517,25 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
assertCondition(routeTargetArgv.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'sh' '-c' 'printf ok'", "D601:k3s:<namespace>:<workload> argv must exec the argv payload instead of treating argv as a pod command", routeTargetArgv);
|
||||
|
||||
const routeTargetShell = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api/app", ["shell", "pwd && ls"]);
|
||||
assertCondition(routeTargetShell.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "kubectl", "exec", "-n", "hwlab-dev", "deployment/hwlab-cloud-api", "--", "sh", "-c", 'cd "$1" || exit; shift; exec "$@"', "unidesk-cwd", "/app", "sh", "-c", `${sshShellCompatibilityPrelude}\npwd && ls`]), "D601:k3s:<namespace>:<workload>/<workspace> shell must run shell logic after cd inside the pod", routeTargetShell);
|
||||
assertCondition(routeTargetShell.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "kubectl", "exec", "-n", "hwlab-dev", "deployment/hwlab-cloud-api", "--", "sh", "-c", 'cd "$1" || exit; shift; exec "$@"', "unidesk-cwd", "/app", "sh", "-c", `${sshShellScriptPrelude()}\npwd && ls`]), "D601:k3s:<namespace>:<workload>/<workspace> shell must run shell logic after cd inside the pod", routeTargetShell);
|
||||
|
||||
const routeScript = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["script", "--shell", "bash", "--", "arg"]);
|
||||
assertCondition(routeScript.parsed.requiresStdin === true, "k3s script operation must stream local stdin", routeScript);
|
||||
assertCondition(routeScript.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-i' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'bash' '-s' '--' 'arg'", "D601:k3s:<namespace>:<workload> script must map stdin to shell -s", routeScript);
|
||||
assertCondition(routeScript.parsed.stdinPrefix === `${sshShellCompatibilityPrelude}\n`, "k3s script stdin must inject the shell compatibility prelude before user script text", routeScript);
|
||||
assertCondition(routeScript.parsed.stdinPrefix === `${sshShellScriptPrelude()}\n`, "k3s script stdin must inject the shell compatibility prelude before user script text", routeScript);
|
||||
|
||||
const routeControlScript = parseSshInvocation("D601:k3s", ["script", "--shell", "bash", "--", "arg"]);
|
||||
assertCondition(routeControlScript.parsed.requiresStdin === true, "k3s control-plane script operation must stream local stdin", routeControlScript);
|
||||
assertCondition(routeControlScript.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'bash' '-s' '--' 'arg'", "D601:k3s script must inject native kubeconfig without manual export", routeControlScript);
|
||||
assertCondition(routeControlScript.parsed.stdinPrefix === `${sshShellCompatibilityPrelude}\n`, "k3s control-plane script stdin must inject the shell compatibility prelude before user script text", routeControlScript);
|
||||
assertCondition(routeControlScript.parsed.stdinPrefix === `${sshShellScriptPrelude()}\n`, "k3s control-plane script stdin must inject the shell compatibility prelude before user script text", routeControlScript);
|
||||
|
||||
const routeControlScriptOneLiner = parseSshInvocation("D601:k3s", ["script", "--", "echo k3s-script-ok"]);
|
||||
assertCondition(routeControlScriptOneLiner.parsed.requiresStdin === false, "k3s control-plane script -- one-liner must not wait for stdin", routeControlScriptOneLiner);
|
||||
assertCondition(routeControlScriptOneLiner.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "sh", "-c", `${sshShellCompatibilityPrelude}\necho k3s-script-ok`]), "k3s control-plane script -- one-liner must run through the native kubeconfig shell path", routeControlScriptOneLiner);
|
||||
assertCondition(routeControlScriptOneLiner.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "sh", "-c", `${sshShellScriptPrelude()}\necho k3s-script-ok`]), "k3s control-plane script -- one-liner must run through the native kubeconfig shell path", routeControlScriptOneLiner);
|
||||
|
||||
const routePodScriptOneLiner = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["script", "--", "echo pod-script-ok"]);
|
||||
assertCondition(routePodScriptOneLiner.parsed.requiresStdin === false, "k3s workload script -- one-liner must not wait for stdin", routePodScriptOneLiner);
|
||||
assertCondition(routePodScriptOneLiner.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "kubectl", "exec", "-n", "hwlab-dev", "deployment/hwlab-cloud-api", "--", "sh", "-c", `${sshShellCompatibilityPrelude}\necho pod-script-ok`]), "k3s workload script -- one-liner must run as sh -c inside the workload", routePodScriptOneLiner);
|
||||
assertCondition(routePodScriptOneLiner.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "kubectl", "exec", "-n", "hwlab-dev", "deployment/hwlab-cloud-api", "--", "sh", "-c", `${sshShellScriptPrelude()}\necho pod-script-ok`]), "k3s workload script -- one-liner must run as sh -c inside the workload", routePodScriptOneLiner);
|
||||
|
||||
const topLevelScriptSeparator = extractRemoteCliOptions(["ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]);
|
||||
assertCondition(JSON.stringify(topLevelScriptSeparator.args) === JSON.stringify(["ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]), "top-level remote option parser must preserve command-local -- after the command starts", topLevelScriptSeparator);
|
||||
@@ -724,6 +729,25 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
assertCondition(truncatedLargeReadV2.files["large-read.txt"] === largeOriginal, "v2 must keep the original file when bridge stdout truncates a read block", truncatedLargeReadV2);
|
||||
assertCondition(!truncatedLargeReadV2.commands.some((command) => command.startsWith("write-b64")), "v2 must not write after read integrity fails", truncatedLargeReadV2.commands);
|
||||
|
||||
const missingUpdateShellV2 = await applyPatchV2ActualShellFixtureAttempt([
|
||||
"*** Begin Patch",
|
||||
"*** Update File: missing-dist.js",
|
||||
"@@",
|
||||
"-old",
|
||||
"+new",
|
||||
"*** End Patch",
|
||||
"",
|
||||
].join("\n"), {});
|
||||
const missingUpdateShellError = missingUpdateShellV2.error instanceof Error ? missingUpdateShellV2.error : null;
|
||||
assertCondition(missingUpdateShellError !== null, "v2 real shell stat should reject missing update targets", missingUpdateShellV2);
|
||||
assertCondition(
|
||||
missingUpdateShellError.message.includes("remote apply-patch v2 operation failed")
|
||||
&& JSON.stringify((missingUpdateShellError as { details?: unknown }).details ?? {}).includes("file not found: missing-dist.js"),
|
||||
"v2 real shell stat must expose file-not-found instead of invalid metadata",
|
||||
missingUpdateShellError,
|
||||
);
|
||||
assertCondition(!missingUpdateShellV2.commands.some((command) => command.startsWith("read-b64") || command.startsWith("write-b64")), "missing update target must fail before remote read/write", missingUpdateShellV2.commands);
|
||||
|
||||
const truncatedLargeWriteV2 = await applyPatchV2ActualShellFixtureAttempt([
|
||||
"*** Begin Patch",
|
||||
"*** Update File: large.txt",
|
||||
|
||||
Reference in New Issue
Block a user