c8968598e4
When the Paperclip pod restarts mid-run, the in-process setInterval keepalive dies, `updatedAt` goes stale, and the server's orphan reaper fails the run with the (misleading) "child pid 1 is no longer running" message. Paperclip then dispatches a continuation run, whose execute() finds the previous run's K8s Job still happily running and deletes it as an "orphan" — throwing away work and producing the transcript/run cascade reported on FAR-124. Changes: - job-manifest: add `paperclip.io/task-id` and `paperclip.io/session-id` labels (sanitized via new `sanitizeLabelValue` helper) so a later execute() can identify an orphan as the continuation of the same logical unit of work. - execute: in the concurrency guard, when `reattachOrphanedJobs` is on (default) and an orphan matches agent + task + session + is not terminal, pick it as the reattach target; delete only the other orphans. Branch the build/create/waitForPod block so the reattach path skips manifest building, Secret creation, Job creation, and scheduling wait — it jumps straight to streaming logs and waiting for the existing pod's completion. - config-schema: expose `reattachOrphanedJobs` toggle (default true). - Tests: `sanitizeLabelValue`, `isReattachableOrphan`, new label presence/absence, config default. No server-side changes; the misleading reaper message and lack of a non-local retry path will be addressed in a follow-up upstream PR. Co-Authored-By: Paperclip <noreply@paperclip.ing>
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { getConfigSchema } from "./config-schema.js";
|
|
|
|
interface ConfigFieldSchema {
|
|
key: string;
|
|
label: string;
|
|
type: string;
|
|
default?: unknown;
|
|
options?: { label: string; value: string }[];
|
|
}
|
|
|
|
describe("getConfigSchema", () => {
|
|
it("returns a non-empty schema", () => {
|
|
const schema = getConfigSchema();
|
|
expect(schema.fields.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("does not include platform-provided fields", () => {
|
|
const schema = getConfigSchema();
|
|
const keys = schema.fields.map((f: ConfigFieldSchema) => f.key);
|
|
// These fields are provided by the platform and should not be duplicated
|
|
expect(keys).not.toContain("model");
|
|
expect(keys).not.toContain("effort");
|
|
expect(keys).not.toContain("instructionsFilePath");
|
|
expect(keys).not.toContain("timeoutSec");
|
|
expect(keys).not.toContain("graceSec");
|
|
});
|
|
|
|
it("maxTurnsPerRun defaults to 1000", () => {
|
|
const schema = getConfigSchema();
|
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "maxTurnsPerRun");
|
|
expect(field).toBeDefined();
|
|
expect(field!.type).toBe("number");
|
|
expect(field!.default).toBe(1000);
|
|
});
|
|
|
|
it("dangerouslySkipPermissions defaults to true", () => {
|
|
const schema = getConfigSchema();
|
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "dangerouslySkipPermissions");
|
|
expect(field).toBeDefined();
|
|
expect(field!.type).toBe("toggle");
|
|
expect(field!.default).toBe(true);
|
|
});
|
|
|
|
it("reattachOrphanedJobs defaults to true", () => {
|
|
const schema = getConfigSchema();
|
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "reattachOrphanedJobs");
|
|
expect(field).toBeDefined();
|
|
expect(field!.type).toBe("toggle");
|
|
expect(field!.default).toBe(true);
|
|
});
|
|
|
|
it("has imagePullPolicy as select with correct options", () => {
|
|
const schema = getConfigSchema();
|
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "imagePullPolicy");
|
|
expect(field).toBeDefined();
|
|
expect(field!.type).toBe("select");
|
|
expect(field!.options).toEqual([
|
|
{ label: "IfNotPresent", value: "IfNotPresent" },
|
|
{ label: "Always", value: "Always" },
|
|
{ label: "Never", value: "Never" },
|
|
]);
|
|
});
|
|
});
|