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:
+129
-1
@@ -13,6 +13,7 @@ vi.mock("./k8s-client.js", () => ({
|
|||||||
|
|
||||||
vi.mock("./job-manifest.js", () => ({
|
vi.mock("./job-manifest.js", () => ({
|
||||||
buildJobManifest: vi.fn(),
|
buildJobManifest: vi.fn(),
|
||||||
|
LARGE_PROMPT_THRESHOLD_BYTES: 256 * 1024,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const MOCK_SELF_POD = {
|
const MOCK_SELF_POD = {
|
||||||
@@ -68,7 +69,7 @@ function makeCtx(configOverrides: Record<string, unknown> = {}): AdapterExecutio
|
|||||||
function makeBatchApi(runningJobItems: unknown[] = []) {
|
function makeBatchApi(runningJobItems: unknown[] = []) {
|
||||||
return {
|
return {
|
||||||
listNamespacedJob: vi.fn().mockResolvedValue({ items: runningJobItems }),
|
listNamespacedJob: vi.fn().mockResolvedValue({ items: runningJobItems }),
|
||||||
createNamespacedJob: vi.fn().mockResolvedValue({}),
|
createNamespacedJob: vi.fn().mockResolvedValue({ metadata: { uid: "test-job-uid" } }),
|
||||||
readNamespacedJob: vi.fn().mockResolvedValue({
|
readNamespacedJob: vi.fn().mockResolvedValue({
|
||||||
status: { conditions: [{ type: "Complete", status: "True" }] },
|
status: { conditions: [{ type: "Complete", status: "True" }] },
|
||||||
}),
|
}),
|
||||||
@@ -111,6 +112,9 @@ function makeCoreApi(
|
|||||||
})
|
})
|
||||||
.mockResolvedValueOnce(exitCodePod),
|
.mockResolvedValueOnce(exitCodePod),
|
||||||
readNamespacedPodLog: vi.fn().mockResolvedValue(jsonl),
|
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);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user