test(execute): large-prompt Secret path coverage

- Add describe block "execute — large-prompt Secret path" with 5 cases:
  buildJobManifest called twice (promptSecretName on second call),
  Secret created before Job, ownerReference patched after Job creation,
  Secret deleted in finally block, Secret cleaned up on Job create failure
- Update vi.mock for job-manifest to export LARGE_PROMPT_THRESHOLD_BYTES
- Add createNamespacedSecret/deleteNamespacedSecret/patchNamespacedSecret
  to makeCoreApi for completeness
- Update makeBatchApi to return { metadata: { uid } } so ownerRef tests work

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-24 22:16:34 +00:00
parent c05d1d7515
commit 0df00f7d95
+129 -1
View File
@@ -13,6 +13,7 @@ vi.mock("./k8s-client.js", () => ({
vi.mock("./job-manifest.js", () => ({
buildJobManifest: vi.fn(),
LARGE_PROMPT_THRESHOLD_BYTES: 256 * 1024,
}));
const MOCK_SELF_POD = {
@@ -68,7 +69,7 @@ function makeCtx(configOverrides: Record<string, unknown> = {}): AdapterExecutio
function makeBatchApi(runningJobItems: unknown[] = []) {
return {
listNamespacedJob: vi.fn().mockResolvedValue({ items: runningJobItems }),
createNamespacedJob: vi.fn().mockResolvedValue({}),
createNamespacedJob: vi.fn().mockResolvedValue({ metadata: { uid: "test-job-uid" } }),
readNamespacedJob: vi.fn().mockResolvedValue({
status: { conditions: [{ type: "Complete", status: "True" }] },
}),
@@ -111,6 +112,9 @@ function makeCoreApi(
})
.mockResolvedValueOnce(exitCodePod),
readNamespacedPodLog: vi.fn().mockResolvedValue(jsonl),
createNamespacedSecret: vi.fn().mockResolvedValue({}),
deleteNamespacedSecret: vi.fn().mockResolvedValue({}),
patchNamespacedSecret: vi.fn().mockResolvedValue({}),
};
}
@@ -665,3 +669,127 @@ describe("execute — log dedup (waitForPod status dedup)", () => {
expect(pendingMsgs.length).toBe(1);
});
});
describe("execute — large-prompt Secret path", () => {
const LARGE_PROMPT = "x".repeat(300 * 1024); // 300 KiB > 256 KiB threshold
function mockLargePrompt() {
vi.mocked(buildJobManifest).mockReturnValue({
job: MOCK_JOB as ReturnType<typeof buildJobManifest>["job"],
jobName: JOB_NAME,
namespace: NAMESPACE,
prompt: LARGE_PROMPT,
opencodeArgs: [],
promptMetrics: null,
} as unknown as ReturnType<typeof buildJobManifest>);
}
it("calls buildJobManifest twice and passes promptSecretName on second call", async () => {
mockLargePrompt();
const ctx = makeCtx();
await execute(ctx);
expect(vi.mocked(buildJobManifest)).toHaveBeenCalledTimes(2);
const secondCall = vi.mocked(buildJobManifest).mock.calls[1][0];
expect(secondCall.promptSecretName).toBe(`${JOB_NAME}-prompt`);
});
it("creates a Secret with the prompt content before creating the Job", async () => {
mockLargePrompt();
const coreApi = makeCoreApi();
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
const batchApi = makeBatchApi();
vi.mocked(getBatchApi).mockReturnValue(batchApi as unknown as ReturnType<typeof getBatchApi>);
const ctx = makeCtx();
await execute(ctx);
expect(coreApi.createNamespacedSecret).toHaveBeenCalledWith(
expect.objectContaining({
namespace: NAMESPACE,
body: expect.objectContaining({
metadata: expect.objectContaining({ name: `${JOB_NAME}-prompt` }),
stringData: expect.objectContaining({ prompt: LARGE_PROMPT }),
}),
}),
);
// Secret must be created before Job
const secretOrder = coreApi.createNamespacedSecret.mock.invocationCallOrder[0];
const jobOrder = batchApi.createNamespacedJob.mock.invocationCallOrder[0];
expect(secretOrder).toBeLessThan(jobOrder);
});
it("patches the Secret with a Job ownerReference after Job creation", async () => {
mockLargePrompt();
const batchApi = makeBatchApi();
batchApi.createNamespacedJob.mockResolvedValue({ metadata: { uid: "uid-abc-123" } });
vi.mocked(getBatchApi).mockReturnValue(batchApi as unknown as ReturnType<typeof getBatchApi>);
const coreApi = makeCoreApi();
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
const ctx = makeCtx();
await execute(ctx);
expect(coreApi.patchNamespacedSecret).toHaveBeenCalledWith(
expect.objectContaining({
name: `${JOB_NAME}-prompt`,
namespace: NAMESPACE,
body: expect.objectContaining({
metadata: expect.objectContaining({
ownerReferences: [
expect.objectContaining({
kind: "Job",
name: JOB_NAME,
uid: "uid-abc-123",
controller: true,
}),
],
}),
}),
}),
);
});
it("cleans up the Secret in the finally block", async () => {
mockLargePrompt();
const coreApi = makeCoreApi();
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
const ctx = makeCtx();
await execute(ctx);
expect(coreApi.deleteNamespacedSecret).toHaveBeenCalledWith(
expect.objectContaining({ name: `${JOB_NAME}-prompt`, namespace: NAMESPACE }),
);
});
it("cleans up the Secret when Job creation fails", async () => {
mockLargePrompt();
const batchApi = makeBatchApi();
batchApi.createNamespacedJob.mockRejectedValue(new Error("quota exceeded"));
vi.mocked(getBatchApi).mockReturnValue(batchApi as unknown as ReturnType<typeof getBatchApi>);
const coreApi = makeCoreApi();
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
const ctx = makeCtx();
const result = await execute(ctx);
expect(result.errorCode).toBe("k8s_job_create_failed");
expect(coreApi.deleteNamespacedSecret).toHaveBeenCalledWith(
expect.objectContaining({ name: `${JOB_NAME}-prompt` }),
);
});
it("does not create a Secret for prompts within threshold", async () => {
// Default beforeEach mock returns "Test prompt" (11 bytes < 256 KiB)
const coreApi = makeCoreApi();
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
const ctx = makeCtx();
await execute(ctx);
expect(vi.mocked(buildJobManifest)).toHaveBeenCalledTimes(1);
expect(coreApi.createNamespacedSecret).not.toHaveBeenCalled();
});
});