fix: detect 404 from @kubernetes/client-node v1.x ApiException (FAR-85)
The v1.x ApiException exposes the HTTP status as `code`, not `statusCode`. Both `isNotFound` (k8s-client) and `isK8s404` (execute) only checked `statusCode`/`response.statusCode`, so 404s were never recognized: - `getPvc` re-threw the 404 instead of returning null, which bubbled up through `ensureAgentDbPvc` as `k8s_job_create_failed` with the raw "persistentvolumeclaims X not found" body — the symptom in FAR-85. - The PVC was never actually created, because the existence check threw before reaching `createPvc`. Add `code === 404` to both predicates and a regression test for `isK8s404`. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.1.26",
|
||||
"version": "0.1.27",
|
||||
"description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1185,3 +1185,30 @@ describe("ensureAgentDbPvc — unit", () => {
|
||||
expect(vi.mocked(getPvc)).toHaveBeenCalledWith(NAMESPACE, `opencode-db-${expectedSlug}`, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isK8s404", () => {
|
||||
it("recognizes @kubernetes/client-node v1.x ApiException with code=404", async () => {
|
||||
const { isK8s404 } = await import("./execute.js");
|
||||
const err = Object.assign(new Error("not found"), { code: 404 });
|
||||
expect(isK8s404(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("still recognizes legacy errors with statusCode=404", async () => {
|
||||
const { isK8s404 } = await import("./execute.js");
|
||||
const err = Object.assign(new Error("not found"), { statusCode: 404 });
|
||||
expect(isK8s404(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("still recognizes errors with response.statusCode=404", async () => {
|
||||
const { isK8s404 } = await import("./execute.js");
|
||||
const err = Object.assign(new Error("not found"), { response: { statusCode: 404 } });
|
||||
expect(isK8s404(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for non-404 errors", async () => {
|
||||
const { isK8s404 } = await import("./execute.js");
|
||||
expect(isK8s404(Object.assign(new Error("server error"), { code: 500 }))).toBe(false);
|
||||
expect(isK8s404(new Error("plain error"))).toBe(false);
|
||||
expect(isK8s404(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,8 @@ const LOG_EXIT_COMPLETION_GRACE_MS = parseInt(process.env.LOG_EXIT_COMPLETION_GR
|
||||
export function isK8s404(err: unknown): boolean {
|
||||
if (!(err instanceof Error)) return false;
|
||||
const asAny = err as unknown as Record<string, unknown>;
|
||||
// @kubernetes/client-node v1.x ApiException exposes HTTP status as `code`.
|
||||
if (typeof asAny.code === "number" && asAny.code === 404) return true;
|
||||
if (typeof asAny.statusCode === "number" && asAny.statusCode === 404) return true;
|
||||
const resp = asAny.response as Record<string, unknown> | undefined;
|
||||
if (typeof resp?.statusCode === "number" && resp.statusCode === 404) return true;
|
||||
|
||||
@@ -178,6 +178,8 @@ export function resetCache(): void {
|
||||
function isNotFound(err: unknown): boolean {
|
||||
if (!(err instanceof Error)) return false;
|
||||
const asAny = err as unknown as Record<string, unknown>;
|
||||
// @kubernetes/client-node v1.x ApiException exposes HTTP status as `code`.
|
||||
if (typeof asAny.code === "number" && asAny.code === 404) return true;
|
||||
if (typeof asAny.statusCode === "number" && asAny.statusCode === 404) return true;
|
||||
const resp = asAny.response as Record<string, unknown> | undefined;
|
||||
return typeof resp?.statusCode === "number" && resp.statusCode === 404;
|
||||
|
||||
Reference in New Issue
Block a user