forked from farhoodlabs/paperclip
feat(plugin): add kubernetes fast upload interceptor
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import plugin, {
|
||||
buildSandboxExecCommand,
|
||||
buildSandboxExecShellCommand,
|
||||
extractAdapterEnvFromProcess,
|
||||
} from "../../src/plugin.js";
|
||||
@@ -161,4 +162,21 @@ describe("plugin", () => {
|
||||
}),
|
||||
).toBe("pnpm test -- --runInBand");
|
||||
});
|
||||
|
||||
it("passes command and args directly to Kubernetes exec", () => {
|
||||
expect(
|
||||
buildSandboxExecCommand({
|
||||
command: "sh",
|
||||
args: ["-c", "printf '%s' ok"],
|
||||
}),
|
||||
).toEqual(["sh", "-c", "printf '%s' ok"]);
|
||||
});
|
||||
|
||||
it("wraps command-only execution in a login shell", () => {
|
||||
expect(
|
||||
buildSandboxExecCommand({
|
||||
command: "pnpm test -- --runInBand",
|
||||
}),
|
||||
).toEqual(["/bin/sh", "-lc", "pnpm test -- --runInBand"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@ describe("execInPod", () => {
|
||||
it("returns success when the Kubernetes exec status callback reports success", async () => {
|
||||
execMock.mockImplementation((_namespace, _pod, _container, _command, stdout, _stderr, _stdin, _tty, statusCallback) => {
|
||||
stdout.write("ok\n");
|
||||
stdout.end();
|
||||
_stderr.end();
|
||||
statusCallback({ status: "Success" });
|
||||
return Promise.resolve(new EventEmitter());
|
||||
});
|
||||
@@ -49,4 +51,31 @@ describe("execInPod", () => {
|
||||
expect(result.timedOut).toBe(true);
|
||||
expect(result.stderr).toContain("Kubernetes exec timed out after 5ms");
|
||||
});
|
||||
|
||||
it("wraps stdin commands with a byte-counted head prefix", async () => {
|
||||
let observedCommand: string[] | undefined;
|
||||
let observedStdin = "";
|
||||
let observedStdinFinished = false;
|
||||
|
||||
execMock.mockImplementation((_namespace, _pod, _container, command, stdout, stderr, stdin, _tty, statusCallback) => {
|
||||
observedCommand = command;
|
||||
stdin?.on("data", (chunk: Buffer) => {
|
||||
observedStdin += chunk.toString("utf8");
|
||||
});
|
||||
stdin?.on("finish", () => {
|
||||
observedStdinFinished = true;
|
||||
});
|
||||
stdout.end();
|
||||
stderr.end();
|
||||
statusCallback({ status: "Success" });
|
||||
return Promise.resolve(new EventEmitter());
|
||||
});
|
||||
|
||||
await execInPod({} as never, "ns", "pod-1", "agent", ["base64", "-d"], "abc");
|
||||
await Promise.resolve();
|
||||
|
||||
expect(observedCommand).toEqual(["/bin/sh", "-c", "head -c 3 | 'base64' '-d'"]);
|
||||
expect(observedStdin).toBe("abc");
|
||||
expect(observedStdinFinished).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { FastUploadInterceptor } from "../../src/upload-interceptor.js";
|
||||
|
||||
describe("FastUploadInterceptor", () => {
|
||||
it("collapses the adapter-utils chunked upload protocol into one flush", () => {
|
||||
const interceptor = new FastUploadInterceptor();
|
||||
const target = "/workspace/.paperclip-runtime/skills.tar";
|
||||
const chunkA = Buffer.from("hello ").toString("base64").slice(0, 4);
|
||||
const chunkB = Buffer.from("hello ").toString("base64").slice(4) + Buffer.from("world").toString("base64");
|
||||
|
||||
expect(
|
||||
interceptor.decide(
|
||||
`mkdir -p '/workspace/.paperclip-runtime' && rm -f '${target}.paperclip-upload.b64' && : > '${target}.paperclip-upload.b64'`,
|
||||
),
|
||||
).toMatchObject({ action: "ack" });
|
||||
expect(interceptor.pendingCount).toBe(1);
|
||||
|
||||
expect(
|
||||
interceptor.decide(`printf '%s' '${chunkA}' >> '${target}.paperclip-upload.b64'`),
|
||||
).toMatchObject({ action: "ack" });
|
||||
expect(
|
||||
interceptor.decide(`printf '%s' '${chunkB}' >> '${target}.paperclip-upload.b64'`),
|
||||
).toMatchObject({ action: "ack" });
|
||||
|
||||
const decision = interceptor.decide(
|
||||
`base64 -d < '${target}.paperclip-upload.b64' > '${target}' && rm -f '${target}.paperclip-upload.b64'`,
|
||||
);
|
||||
expect(decision.action).toBe("flush");
|
||||
if (decision.action !== "flush") throw new Error("expected flush");
|
||||
expect(decision.flush.targetPath).toBe(target);
|
||||
expect(decision.flush.payload.toString("utf8")).toBe("hello world");
|
||||
expect(interceptor.pendingCount).toBe(0);
|
||||
});
|
||||
|
||||
it("passes through chunks and finalizers without a matching init", () => {
|
||||
const interceptor = new FastUploadInterceptor();
|
||||
const target = "/workspace/file.bin";
|
||||
|
||||
expect(
|
||||
interceptor.decide(`printf '%s' 'aGVsbG8=' >> '${target}.paperclip-upload.b64'`),
|
||||
).toMatchObject({ action: "passthrough", reason: "chunk without prior init" });
|
||||
expect(
|
||||
interceptor.decide(
|
||||
`base64 -d < '${target}.paperclip-upload.b64' > '${target}' && rm -f '${target}.paperclip-upload.b64'`,
|
||||
),
|
||||
).toMatchObject({ action: "passthrough", reason: "finalize without buffered state" });
|
||||
});
|
||||
|
||||
it("falls through when the init command does not match the target parent directory", () => {
|
||||
const interceptor = new FastUploadInterceptor();
|
||||
|
||||
expect(
|
||||
interceptor.decide(
|
||||
"mkdir -p '/tmp' && rm -f '/workspace/file.bin.paperclip-upload.b64' && : > '/workspace/file.bin.paperclip-upload.b64'",
|
||||
),
|
||||
).toMatchObject({ action: "passthrough", reason: "init dir/target mismatch" });
|
||||
expect(interceptor.pendingCount).toBe(0);
|
||||
});
|
||||
|
||||
it("clears buffered uploads on reset", () => {
|
||||
const interceptor = new FastUploadInterceptor();
|
||||
const target = "/workspace/file.bin";
|
||||
|
||||
interceptor.decide(
|
||||
`mkdir -p '/workspace' && rm -f '${target}.paperclip-upload.b64' && : > '${target}.paperclip-upload.b64'`,
|
||||
);
|
||||
expect(interceptor.pendingCount).toBe(1);
|
||||
|
||||
interceptor.reset();
|
||||
expect(interceptor.pendingCount).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user