@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/http-api.md +66 -0
- package/package.json +1 -1
- package/src/api/__tests__/index.test.ts +311 -3
- package/src/api/index.ts +94 -45
- package/src/config/__tests__/statuses.test.ts +41 -0
- package/src/config/statuses.ts +20 -1
- package/src/core/__tests__/config.test.ts +31 -1
- package/src/core/__tests__/job-concurrency.test.ts +60 -0
- package/src/core/__tests__/job-view.test.ts +859 -0
- package/src/core/__tests__/orchestrator.test.ts +242 -0
- package/src/core/__tests__/runner-liveness.test.ts +865 -0
- package/src/core/__tests__/single-derivation.test.ts +159 -0
- package/src/core/config.ts +19 -0
- package/src/core/job-concurrency.ts +6 -1
- package/src/core/job-view.ts +329 -0
- package/src/core/orchestrator.ts +78 -6
- package/src/core/runner-liveness.ts +276 -0
- package/src/core/status-writer.ts +24 -5
- package/src/ui/client/__tests__/job-adapter.test.ts +78 -0
- package/src/ui/client/adapters/job-adapter.ts +10 -0
- package/src/ui/client/types.ts +2 -0
- package/src/ui/components/JobTable.tsx +29 -12
- package/src/ui/components/__tests__/JobTable.test.tsx +54 -1
- package/src/ui/components/types.ts +2 -0
- package/src/ui/dist/assets/{index--RH3sAt3.js → index-L6cvsCAx.js} +25 -12
- package/src/ui/dist/assets/{index--RH3sAt3.js.map → index-L6cvsCAx.js.map} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/src/ui/embedded-assets.js +6 -6
- package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +57 -1
- package/src/ui/server/__tests__/job-endpoints.test.ts +115 -1
- package/src/ui/server/__tests__/sse-enhancer.test.ts +31 -0
- package/src/ui/server/config-bridge.ts +3 -4
- package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
- package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
- package/src/ui/server/endpoints/job-control-endpoints.ts +7 -68
- package/src/ui/server/endpoints/job-endpoints.ts +6 -0
- package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
- package/src/ui/state/__tests__/snapshot.test.ts +51 -0
- package/src/ui/state/__tests__/types.test.ts +98 -5
- package/src/ui/state/snapshot.ts +1 -1
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +43 -2
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +208 -22
- package/src/ui/state/transformers/list-transformer.ts +7 -3
- package/src/ui/state/transformers/status-transformer.ts +36 -170
- package/src/ui/state/types.ts +9 -47
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
VALID_JOB_LOCATIONS,
|
|
9
9
|
normalizeTaskState,
|
|
10
10
|
normalizeJobStatus,
|
|
11
|
+
jobStateToStatus,
|
|
11
12
|
deriveJobStatusFromTasks,
|
|
12
13
|
} from "../statuses";
|
|
13
14
|
|
|
@@ -159,6 +160,10 @@ describe("normalizeJobStatus", () => {
|
|
|
159
160
|
expect(normalizeJobStatus("completed")).toBe("complete");
|
|
160
161
|
});
|
|
161
162
|
|
|
163
|
+
it("handles synonym: done → complete", () => {
|
|
164
|
+
expect(normalizeJobStatus("done")).toBe("complete");
|
|
165
|
+
});
|
|
166
|
+
|
|
162
167
|
it("handles synonym: error → failed", () => {
|
|
163
168
|
expect(normalizeJobStatus("error")).toBe("failed");
|
|
164
169
|
});
|
|
@@ -193,6 +198,42 @@ describe("normalizeJobStatus", () => {
|
|
|
193
198
|
});
|
|
194
199
|
});
|
|
195
200
|
|
|
201
|
+
describe("jobStateToStatus", () => {
|
|
202
|
+
it("returns canonical for each recognized JobState", () => {
|
|
203
|
+
expect(jobStateToStatus("pending")).toBe("pending");
|
|
204
|
+
expect(jobStateToStatus("running")).toBe("running");
|
|
205
|
+
expect(jobStateToStatus("waiting")).toBe("waiting");
|
|
206
|
+
expect(jobStateToStatus("failed")).toBe("failed");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("maps done (persisted JobState) to complete (canonical JobStatusValue)", () => {
|
|
210
|
+
expect(jobStateToStatus("done")).toBe("complete");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("is case-insensitive and trims whitespace", () => {
|
|
214
|
+
expect(jobStateToStatus("DONE")).toBe("complete");
|
|
215
|
+
expect(jobStateToStatus(" failed ")).toBe("failed");
|
|
216
|
+
expect(jobStateToStatus("Waiting")).toBe("waiting");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("returns null for non-string input", () => {
|
|
220
|
+
expect(jobStateToStatus(null)).toBeNull();
|
|
221
|
+
expect(jobStateToStatus(undefined)).toBeNull();
|
|
222
|
+
expect(jobStateToStatus(42)).toBeNull();
|
|
223
|
+
expect(jobStateToStatus({})).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("returns null for unrecognized strings", () => {
|
|
227
|
+
expect(jobStateToStatus("bogus-state")).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("does not accept JobStatus synonyms that are not JobState values", () => {
|
|
231
|
+
expect(jobStateToStatus("error")).toBeNull();
|
|
232
|
+
expect(jobStateToStatus("completed")).toBeNull();
|
|
233
|
+
expect(jobStateToStatus("complete")).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
196
237
|
describe("deriveJobStatusFromTasks", () => {
|
|
197
238
|
it("returns failed when any task is failed (failed priority)", () => {
|
|
198
239
|
expect(deriveJobStatusFromTasks([{ state: "failed" }, { state: "done" }])).toBe("failed");
|
package/src/config/statuses.ts
CHANGED
|
@@ -53,8 +53,9 @@ const TASK_STATE_SYNONYMS: Readonly<Record<string, TaskStateValue>> = Object.fre
|
|
|
53
53
|
succeeded: "done",
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
const JOB_STATUS_SYNONYMS: Readonly<Record<string, JobStatusValue>> = Object.freeze({
|
|
56
|
+
export const JOB_STATUS_SYNONYMS: Readonly<Record<string, JobStatusValue>> = Object.freeze({
|
|
57
57
|
completed: "complete",
|
|
58
|
+
done: "complete",
|
|
58
59
|
error: "failed",
|
|
59
60
|
});
|
|
60
61
|
|
|
@@ -93,6 +94,24 @@ export function normalizeJobStatus(status: unknown): JobStatusValue {
|
|
|
93
94
|
return "pending";
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
export function jobStateToStatus(state: unknown): JobStatusValue | null {
|
|
98
|
+
if (typeof state !== "string") return null;
|
|
99
|
+
switch (state.toLowerCase().trim()) {
|
|
100
|
+
case "pending":
|
|
101
|
+
return "pending";
|
|
102
|
+
case "running":
|
|
103
|
+
return "running";
|
|
104
|
+
case "waiting":
|
|
105
|
+
return "waiting";
|
|
106
|
+
case "failed":
|
|
107
|
+
return "failed";
|
|
108
|
+
case "done":
|
|
109
|
+
return "complete";
|
|
110
|
+
default:
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
export function deriveJobStatusFromTasks(
|
|
97
116
|
tasks: ReadonlyArray<{ state: unknown }>,
|
|
98
117
|
): JobStatusValue {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
-
import { defaultConfig, resetConfig, loadConfig, getConfig, getConfigValue, getPipelineConfig } from "../config";
|
|
2
|
+
import { defaultConfig, resetConfig, loadConfig, getConfig, getConfigValue, getPipelineConfig, getOrchestratorConfig } from "../config";
|
|
3
3
|
import type { AppConfig } from "../config";
|
|
4
4
|
import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
@@ -194,6 +194,36 @@ describe("PO_MAX_RUNNING_JOBS env override", () => {
|
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
describe("PO_STALE_RUNNING_GRACE_MS env override", () => {
|
|
198
|
+
afterEach(() => {
|
|
199
|
+
delete process.env.PO_STALE_RUNNING_GRACE_MS;
|
|
200
|
+
resetConfig();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("getOrchestratorConfig defaults staleRunningGraceMs to 30000", () => {
|
|
204
|
+
resetConfig();
|
|
205
|
+
expect(getOrchestratorConfig().staleRunningGraceMs).toBe(30000);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("getOrchestratorConfig reads PO_STALE_RUNNING_GRACE_MS into orchestrator.staleRunningGraceMs", () => {
|
|
209
|
+
process.env.PO_STALE_RUNNING_GRACE_MS = "60000";
|
|
210
|
+
resetConfig();
|
|
211
|
+
expect(getOrchestratorConfig().staleRunningGraceMs).toBe(60000);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("getOrchestratorConfig rejects non-positive PO_STALE_RUNNING_GRACE_MS", () => {
|
|
215
|
+
process.env.PO_STALE_RUNNING_GRACE_MS = "-1";
|
|
216
|
+
resetConfig();
|
|
217
|
+
expect(() => getOrchestratorConfig()).toThrow("orchestrator.staleRunningGraceMs");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("getOrchestratorConfig rejects non-integer PO_STALE_RUNNING_GRACE_MS", () => {
|
|
221
|
+
process.env.PO_STALE_RUNNING_GRACE_MS = "1.5";
|
|
222
|
+
resetConfig();
|
|
223
|
+
expect(() => getOrchestratorConfig()).toThrow("orchestrator.staleRunningGraceMs");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
197
227
|
describe("getConfig", () => {
|
|
198
228
|
afterEach(() => {
|
|
199
229
|
resetConfig();
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
getJobConcurrencyStatus,
|
|
10
10
|
listQueuedSeeds,
|
|
11
11
|
pruneStaleJobSlots,
|
|
12
|
+
readJobSlotLease,
|
|
12
13
|
releaseJobSlot,
|
|
13
14
|
tryAcquireJobSlot,
|
|
14
15
|
updateJobSlotPid,
|
|
16
|
+
withRuntimeLock,
|
|
15
17
|
type JobSlotLease,
|
|
16
18
|
} from "../job-concurrency";
|
|
17
19
|
import { readFile } from "node:fs/promises";
|
|
@@ -552,3 +554,61 @@ describe("getJobConcurrencyStatus", () => {
|
|
|
552
554
|
}
|
|
553
555
|
});
|
|
554
556
|
});
|
|
557
|
+
|
|
558
|
+
describe("readJobSlotLease", () => {
|
|
559
|
+
test("returns the lease written by tryAcquireJobSlot", async () => {
|
|
560
|
+
const dir = await setupJobsDir("read-lease-acquired-");
|
|
561
|
+
try {
|
|
562
|
+
await makeCurrent(dir, "job-a");
|
|
563
|
+
const acquired = await tryAcquireJobSlot({
|
|
564
|
+
dataDir: dir,
|
|
565
|
+
jobId: "job-a",
|
|
566
|
+
maxConcurrentJobs: 2,
|
|
567
|
+
source: "orchestrator",
|
|
568
|
+
pid: process.pid,
|
|
569
|
+
});
|
|
570
|
+
expect(acquired.ok).toBe(true);
|
|
571
|
+
|
|
572
|
+
const lease = await readJobSlotLease(dir, "job-a");
|
|
573
|
+
expect(lease).not.toBeNull();
|
|
574
|
+
expect(lease!.jobId).toBe("job-a");
|
|
575
|
+
expect(lease!.pid).toBe(process.pid);
|
|
576
|
+
expect(lease!.source).toBe("orchestrator");
|
|
577
|
+
} finally {
|
|
578
|
+
await rm(dir, { recursive: true });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("returns null for an unknown job", async () => {
|
|
583
|
+
const dir = await setupJobsDir("read-lease-unknown-");
|
|
584
|
+
try {
|
|
585
|
+
expect(await readJobSlotLease(dir, "unknown")).toBeNull();
|
|
586
|
+
} finally {
|
|
587
|
+
await rm(dir, { recursive: true });
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
describe("withRuntimeLock", () => {
|
|
593
|
+
test("serializes two concurrent callers", async () => {
|
|
594
|
+
const dir = await setupJobsDir("runtime-lock-serialize-");
|
|
595
|
+
const markerPath = join(dir, "marker.txt");
|
|
596
|
+
const events: string[] = [];
|
|
597
|
+
try {
|
|
598
|
+
const a = withRuntimeLock(dir, 5000, async () => {
|
|
599
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
600
|
+
await writeFile(markerPath, "from-a");
|
|
601
|
+
events.push("a-wrote");
|
|
602
|
+
});
|
|
603
|
+
const b = withRuntimeLock(dir, 5000, async () => {
|
|
604
|
+
const observed = await readFile(markerPath, "utf-8");
|
|
605
|
+
events.push(`b-read:${observed}`);
|
|
606
|
+
});
|
|
607
|
+
await Promise.all([a, b]);
|
|
608
|
+
|
|
609
|
+
expect(events).toEqual(["a-wrote", "b-read:from-a"]);
|
|
610
|
+
} finally {
|
|
611
|
+
await rm(dir, { recursive: true });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
});
|