diff --git a/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts b/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts index e67ddfe5..91010d8a 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts @@ -59,6 +59,8 @@ export async function execInPod( stderrStream.on("data", (chunk: Buffer) => { stderrData += chunk.toString("utf-8"); }); + stdoutStream.on("error", () => {}); + stderrStream.on("error", () => {}); return await new Promise( (resolve, reject) => { diff --git a/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts b/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts index 28db96e4..42a90035 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/upload-interceptor.ts @@ -17,7 +17,7 @@ import { posix as pathPosix } from "node:path"; const INIT_RE = /^mkdir -p '([^']+)' && rm -f '([^']+)\.paperclip-upload\.b64' && : > '\2\.paperclip-upload\.b64'$/; const CHUNK_RE = - /^printf '%s' '([A-Za-z0-9+/=]+)' >> '([^']+)\.paperclip-upload\.b64'$/; + /^printf '%s' '([A-Za-z0-9+/]+={0,2})' >> '([^']+)\.paperclip-upload\.b64'$/; const FINALIZE_RE = /^base64 -d < '([^']+)\.paperclip-upload\.b64' > '\1' && rm -f '\1\.paperclip-upload\.b64'$/; diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts index 0caea527..d3184f64 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts @@ -38,6 +38,17 @@ describe("execInPod", () => { expect(result).toEqual({ exitCode: 0, timedOut: false, stdout: "ok\n", stderr: "" }); }); + it("handles output stream errors after status completion", async () => { + execMock.mockImplementation((_namespace, _pod, _container, _command, stdout, _stderr, _stdin, _tty, statusCallback) => { + statusCallback({ status: "Success" }); + stdout.emit("error", new Error("write after end")); + return Promise.resolve(new EventEmitter()); + }); + + const result = await execInPod({} as never, "ns", "pod-1", "agent", ["echo", "ok"]); + expect(result).toEqual({ exitCode: 0, timedOut: false, stdout: "", stderr: "" }); + }); + it("returns an execution failure if the websocket closes before a status frame", async () => { const ws = new EventEmitter(); execMock.mockResolvedValue(ws); diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts index 9b91ff40..3c20959a 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/upload-interceptor.test.ts @@ -46,6 +46,22 @@ describe("FastUploadInterceptor", () => { ).toMatchObject({ action: "passthrough", reason: "finalize without buffered state" }); }); + it("does not intercept chunks with padding before the end", () => { + const interceptor = new FastUploadInterceptor(); + const target = "/workspace/file.bin"; + + expect( + interceptor.decide( + `mkdir -p '/workspace' && rm -f '${target}.paperclip-upload.b64' && : > '${target}.paperclip-upload.b64'`, + ), + ).toMatchObject({ action: "ack" }); + + expect( + interceptor.decide(`printf '%s' 'aGVs=bG8=' >> '${target}.paperclip-upload.b64'`), + ).toMatchObject({ action: "passthrough", reason: "no upload pattern" }); + expect(interceptor.pendingCount).toBe(1); + }); + it("falls through when the init command does not match the target parent directory", () => { const interceptor = new FastUploadInterceptor();