@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.4
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 +80 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +443 -32
- package/src/api/index.ts +108 -127
- package/src/cli/__tests__/analyze-task.test.ts +40 -7
- package/src/cli/__tests__/index.test.ts +337 -17
- package/src/cli/__tests__/types.test.ts +0 -18
- package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
- package/src/cli/analyze-task.ts +3 -14
- package/src/cli/index.ts +73 -61
- package/src/cli/types.ts +0 -13
- package/src/cli/update-pipeline-json.ts +3 -14
- package/src/config/__tests__/paths.test.ts +46 -1
- package/src/config/__tests__/pipeline-registry.test.ts +767 -0
- package/src/config/__tests__/sse-events.test.ts +470 -0
- package/src/config/__tests__/statuses.test.ts +41 -0
- package/src/config/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/config/statuses.ts +20 -1
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +545 -105
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +260 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +1341 -0
- package/src/core/__tests__/json-file.test.ts +111 -0
- package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
- package/src/core/__tests__/logger.test.ts +22 -0
- package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
- package/src/core/__tests__/orchestrator.test.ts +615 -2
- package/src/core/__tests__/pipeline-runner.test.ts +946 -9
- package/src/core/__tests__/redact.test.ts +153 -0
- package/src/core/__tests__/runner-liveness.test.ts +910 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +159 -0
- package/src/core/__tests__/task-runner.test.ts +594 -3
- package/src/core/__tests__/task-telemetry.test.ts +241 -0
- package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
- package/src/core/agent-step.ts +4 -1
- package/src/core/agent-types.ts +5 -4
- package/src/core/config.ts +134 -222
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +56 -20
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +473 -0
- package/src/core/json-file.ts +45 -0
- package/src/core/lifecycle-policy.ts +23 -8
- package/src/core/logger.ts +0 -29
- package/src/core/orchestrator.ts +200 -74
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +280 -0
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +27 -33
- package/src/core/task-runner.ts +356 -319
- package/src/core/task-telemetry.ts +107 -0
- package/src/harness/__tests__/subprocess.test.ts +112 -1
- package/src/harness/subprocess.ts +55 -16
- package/src/llm/__tests__/index.test.ts +684 -33
- package/src/llm/index.ts +310 -67
- package/src/providers/__tests__/alibaba.test.ts +67 -6
- package/src/providers/__tests__/anthropic.test.ts +35 -14
- package/src/providers/__tests__/base.test.ts +62 -0
- package/src/providers/__tests__/claude-code.test.ts +99 -14
- package/src/providers/__tests__/deepseek.test.ts +16 -6
- package/src/providers/__tests__/gemini.test.ts +47 -25
- package/src/providers/__tests__/moonshot.test.ts +27 -0
- package/src/providers/__tests__/openai.test.ts +262 -74
- package/src/providers/__tests__/opencode.test.ts +77 -0
- package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
- package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
- package/src/providers/__tests__/types.test.ts +85 -0
- package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
- package/src/providers/__tests__/zhipu.test.ts +27 -14
- package/src/providers/alibaba.ts +20 -39
- package/src/providers/anthropic.ts +23 -11
- package/src/providers/base.ts +19 -0
- package/src/providers/claude-code.ts +27 -18
- package/src/providers/deepseek.ts +9 -28
- package/src/providers/gemini.ts +20 -58
- package/src/providers/moonshot.ts +15 -11
- package/src/providers/openai.ts +79 -61
- package/src/providers/opencode.ts +16 -33
- package/src/providers/stream-accumulator.ts +27 -21
- package/src/providers/types.ts +29 -4
- package/src/providers/zhipu.ts +15 -44
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
- package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
- package/src/task-analysis/__tests__/repository.test.ts +65 -0
- package/src/task-analysis/__tests__/types.test.ts +63 -130
- package/src/task-analysis/analyzer.ts +91 -0
- package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
- package/src/task-analysis/index.ts +2 -36
- package/src/task-analysis/repository.ts +45 -0
- package/src/task-analysis/types.ts +42 -58
- package/src/ui/client/__tests__/api.test.ts +143 -1
- package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
- package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
- package/src/ui/client/__tests__/load-state.test.ts +70 -0
- package/src/ui/client/__tests__/types.test.ts +66 -3
- package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
- package/src/ui/client/__tests__/useJobList.test.ts +198 -23
- package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
- package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
- package/src/ui/client/adapters/job-adapter.ts +41 -16
- package/src/ui/client/api.ts +38 -15
- package/src/ui/client/bootstrap.ts +19 -14
- package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
- package/src/ui/client/hooks/useJobList.ts +26 -31
- package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
- package/src/ui/client/load-state.ts +20 -0
- package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
- package/src/ui/client/reducers/job-events.ts +137 -0
- package/src/ui/client/types.ts +16 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +41 -13
- package/src/ui/components/StageTimeline.tsx +8 -20
- package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
- package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
- package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
- package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
- package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
- package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
- package/src/ui/components/types.ts +35 -15
- package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
- package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
- package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/pages/PipelineDetail.tsx +60 -4
- package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
- package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
- package/src/ui/pages/__tests__/pages.test.tsx +236 -42
- package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
- package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
- package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
- package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
- package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
- package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
- package/src/ui/server/__tests__/index.test.ts +63 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
- package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
- package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
- package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
- package/src/ui/server/__tests__/router-threading.test.ts +148 -0
- package/src/ui/server/__tests__/router.test.ts +104 -0
- package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
- package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- package/src/ui/server/config-bridge.ts +3 -4
- package/src/ui/server/embedded-assets-imports.d.ts +44 -0
- package/src/ui/server/embedded-assets.ts +13 -2
- package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
- package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
- package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
- package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
- package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
- package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
- package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
- package/src/ui/server/endpoints/file-endpoints.ts +15 -10
- package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
- package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
- package/src/ui/server/endpoints/job-endpoints.ts +7 -0
- package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
- package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
- package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
- package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
- package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
- package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
- package/src/ui/server/index.ts +19 -14
- package/src/ui/server/job-reader.ts +14 -2
- package/src/ui/server/router.ts +33 -10
- package/src/ui/server/sse-broadcast.ts +14 -3
- package/src/ui/server/sse-enhancer.ts +12 -2
- package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
- package/src/ui/state/__tests__/snapshot.test.ts +120 -14
- package/src/ui/state/__tests__/types.test.ts +104 -5
- package/src/ui/state/snapshot.ts +2 -2
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
- package/src/ui/state/transformers/list-transformer.ts +13 -3
- package/src/ui/state/transformers/status-transformer.ts +36 -170
- package/src/ui/state/types.ts +15 -48
- package/src/utils/__tests__/path-containment.test.ts +160 -0
- package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
- package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
- package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
- package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
- package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
- package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
- package/src/task-analysis/__tests__/index.test.ts +0 -143
- package/src/task-analysis/__tests__/parser.test.ts +0 -41
- package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
- package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
- package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
- package/src/task-analysis/extractors/artifacts.ts +0 -143
- package/src/task-analysis/extractors/llm-calls.ts +0 -117
- package/src/task-analysis/extractors/stages.ts +0 -45
- package/src/task-analysis/parser.ts +0 -20
- package/src/task-analysis/utils/ast.ts +0 -45
- package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
- package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
- package/src/ui/embedded-assets.js +0 -12
- package/src/ui/server/__tests__/path-containment.test.ts +0 -54
|
@@ -7,11 +7,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
7
7
|
|
|
8
8
|
import { initPATHS, resetPATHS } from "../config-bridge-node";
|
|
9
9
|
import {
|
|
10
|
-
classifyStaleRunningStatus,
|
|
11
10
|
handleJobRestart,
|
|
12
11
|
handleJobStop,
|
|
13
12
|
handleTaskStart,
|
|
14
13
|
} from "../endpoints/job-control-endpoints";
|
|
14
|
+
import { classifyStaleRunningStatus } from "../../../core/runner-liveness";
|
|
15
15
|
import { resetConfig } from "../../../core/config";
|
|
16
16
|
import {
|
|
17
17
|
getConcurrencyRuntimePaths,
|
|
@@ -56,8 +56,9 @@ async function setupPipelineConfig(
|
|
|
56
56
|
await mkdir(configDir, { recursive: true });
|
|
57
57
|
await writeFile(path.join(configDir, "pipeline.json"), JSON.stringify({ name: slug, tasks }));
|
|
58
58
|
await writeFile(path.join(root, "pipeline-config", "registry.json"), JSON.stringify({
|
|
59
|
+
version: 1,
|
|
59
60
|
pipelines: {
|
|
60
|
-
[slug]: {},
|
|
61
|
+
[slug]: { name: slug, description: "test pipeline" },
|
|
61
62
|
},
|
|
62
63
|
}));
|
|
63
64
|
}
|
|
@@ -122,6 +123,8 @@ afterEach(async () => {
|
|
|
122
123
|
vi.restoreAllMocks();
|
|
123
124
|
resetPATHS();
|
|
124
125
|
delete process.env["PO_MAX_RUNNING_JOBS"];
|
|
126
|
+
delete process.env["PO_STALE_RUNNING_GRACE_MS"];
|
|
127
|
+
delete process.env["PO_STALE_LEASE_TIMEOUT_MS"];
|
|
125
128
|
resetConfig();
|
|
126
129
|
for (const proc of childProcs.splice(0)) {
|
|
127
130
|
try { proc.kill(); } catch {}
|
|
@@ -514,6 +517,120 @@ describe("handleJobStop", () => {
|
|
|
514
517
|
});
|
|
515
518
|
});
|
|
516
519
|
|
|
520
|
+
describe("handleJobStop slot release (AC-1, AC-9)", () => {
|
|
521
|
+
it("releases the active slot lease after stopping (AC-1)", async () => {
|
|
522
|
+
const root = await makeTempRoot();
|
|
523
|
+
initPATHS(root);
|
|
524
|
+
|
|
525
|
+
await setupJob(root, "stop-release-active", {
|
|
526
|
+
id: "stop-release-active",
|
|
527
|
+
state: "running",
|
|
528
|
+
current: "research",
|
|
529
|
+
currentStage: null,
|
|
530
|
+
tasks: { research: { state: "running", currentStage: null } },
|
|
531
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
532
|
+
}, 999999);
|
|
533
|
+
|
|
534
|
+
const acquired = await tryAcquireJobSlot({
|
|
535
|
+
dataDir: path.join(root, "pipeline-data"),
|
|
536
|
+
jobId: "stop-release-active",
|
|
537
|
+
maxConcurrentJobs: 2,
|
|
538
|
+
source: "task-start",
|
|
539
|
+
pid: 999999,
|
|
540
|
+
});
|
|
541
|
+
expect(acquired.ok).toBe(true);
|
|
542
|
+
|
|
543
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(path.join(root, "pipeline-data"));
|
|
544
|
+
const leasePath = path.join(runningJobsDir, "stop-release-active.json");
|
|
545
|
+
expect(await Bun.file(leasePath).exists()).toBe(true);
|
|
546
|
+
|
|
547
|
+
const req = new Request("http://localhost/api/jobs/stop-release-active/stop", { method: "POST" });
|
|
548
|
+
const res = await handleJobStop(req, "stop-release-active", root);
|
|
549
|
+
const body = await res.json() as Record<string, unknown>;
|
|
550
|
+
|
|
551
|
+
expect(res.status).toBe(202);
|
|
552
|
+
expect(body["ok"]).toBe(true);
|
|
553
|
+
expect(body["stopped"]).toBe(true);
|
|
554
|
+
|
|
555
|
+
expect(await Bun.file(leasePath).exists()).toBe(false);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("does not throw and returns 202 when no lease exists (AC-9)", async () => {
|
|
559
|
+
const root = await makeTempRoot();
|
|
560
|
+
initPATHS(root);
|
|
561
|
+
|
|
562
|
+
await setupJob(root, "stop-release-no-lease", {
|
|
563
|
+
id: "stop-release-no-lease",
|
|
564
|
+
state: "running",
|
|
565
|
+
current: "research",
|
|
566
|
+
currentStage: null,
|
|
567
|
+
tasks: { research: { state: "running", currentStage: null } },
|
|
568
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const req = new Request("http://localhost/api/jobs/stop-release-no-lease/stop", { method: "POST" });
|
|
572
|
+
const res = await handleJobStop(req, "stop-release-no-lease", root);
|
|
573
|
+
const body = await res.json() as Record<string, unknown>;
|
|
574
|
+
|
|
575
|
+
expect(res.status).toBe(202);
|
|
576
|
+
expect(body["ok"]).toBe(true);
|
|
577
|
+
expect(body["stopped"]).toBe(false);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("does not throw and returns 202 when stopping a complete/ job (AC-9)", async () => {
|
|
581
|
+
const root = await makeTempRoot();
|
|
582
|
+
initPATHS(root);
|
|
583
|
+
|
|
584
|
+
const completeDir = path.join(root, "pipeline-data", "complete", "stop-release-complete");
|
|
585
|
+
await mkdir(completeDir, { recursive: true });
|
|
586
|
+
await writeFile(path.join(completeDir, "tasks-status.json"), JSON.stringify({
|
|
587
|
+
id: "stop-release-complete",
|
|
588
|
+
state: "complete",
|
|
589
|
+
current: null,
|
|
590
|
+
currentStage: null,
|
|
591
|
+
tasks: { research: { state: "done", currentStage: null } },
|
|
592
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
593
|
+
}));
|
|
594
|
+
|
|
595
|
+
const req = new Request("http://localhost/api/jobs/stop-release-complete/stop", { method: "POST" });
|
|
596
|
+
const res = await handleJobStop(req, "stop-release-complete", root);
|
|
597
|
+
const body = await res.json() as Record<string, unknown>;
|
|
598
|
+
|
|
599
|
+
expect(res.status).toBe(202);
|
|
600
|
+
expect(body["ok"]).toBe(true);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("still releases the slot when the status is corrupt (AC-1)", async () => {
|
|
604
|
+
const root = await makeTempRoot();
|
|
605
|
+
initPATHS(root);
|
|
606
|
+
|
|
607
|
+
const jobDir = path.join(root, "pipeline-data", "current", "stop-release-corrupt");
|
|
608
|
+
await mkdir(jobDir, { recursive: true });
|
|
609
|
+
await writeFile(path.join(jobDir, "tasks-status.json"), "not-valid-json{{{");
|
|
610
|
+
|
|
611
|
+
const acquired = await tryAcquireJobSlot({
|
|
612
|
+
dataDir: path.join(root, "pipeline-data"),
|
|
613
|
+
jobId: "stop-release-corrupt",
|
|
614
|
+
maxConcurrentJobs: 2,
|
|
615
|
+
source: "task-start",
|
|
616
|
+
});
|
|
617
|
+
expect(acquired.ok).toBe(true);
|
|
618
|
+
|
|
619
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(path.join(root, "pipeline-data"));
|
|
620
|
+
const leasePath = path.join(runningJobsDir, "stop-release-corrupt.json");
|
|
621
|
+
expect(await Bun.file(leasePath).exists()).toBe(true);
|
|
622
|
+
|
|
623
|
+
const req = new Request("http://localhost/api/jobs/stop-release-corrupt/stop", { method: "POST" });
|
|
624
|
+
const res = await handleJobStop(req, "stop-release-corrupt", root);
|
|
625
|
+
const body = await res.json() as Record<string, unknown>;
|
|
626
|
+
|
|
627
|
+
expect(res.status).toBe(409);
|
|
628
|
+
expect(body["code"]).toBe("STATUS_CORRUPT");
|
|
629
|
+
|
|
630
|
+
expect(await Bun.file(leasePath).exists()).toBe(false);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
517
634
|
describe("handleJobRestart", () => {
|
|
518
635
|
it("returns 409 when runner PID is alive", async () => {
|
|
519
636
|
const root = await makeTempRoot();
|
|
@@ -566,6 +683,33 @@ describe("handleJobRestart", () => {
|
|
|
566
683
|
expect(body["code"]).toBe("job_running");
|
|
567
684
|
});
|
|
568
685
|
|
|
686
|
+
it("uses configured staleRunningGraceMs when deciding whether restart can recover a running task", async () => {
|
|
687
|
+
const root = await makeTempRoot();
|
|
688
|
+
initPATHS(root);
|
|
689
|
+
process.env["PO_STALE_RUNNING_GRACE_MS"] = "60000";
|
|
690
|
+
resetConfig();
|
|
691
|
+
|
|
692
|
+
vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-04-01T10:00:45.000Z"));
|
|
693
|
+
await setupJob(root, "restart-configured-grace", {
|
|
694
|
+
id: "restart-configured-grace",
|
|
695
|
+
state: "running",
|
|
696
|
+
current: "research",
|
|
697
|
+
currentStage: "prompt",
|
|
698
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
699
|
+
tasks: {
|
|
700
|
+
research: { state: "running", currentStage: "prompt" },
|
|
701
|
+
},
|
|
702
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
703
|
+
}, 999999);
|
|
704
|
+
|
|
705
|
+
const req = new Request("http://localhost/api/jobs/restart-configured-grace/restart", { method: "POST" });
|
|
706
|
+
const res = await handleJobRestart(req, "restart-configured-grace", root);
|
|
707
|
+
const body = await res.json() as Record<string, unknown>;
|
|
708
|
+
|
|
709
|
+
expect(res.status).toBe(409);
|
|
710
|
+
expect(body["code"]).toBe("job_running");
|
|
711
|
+
});
|
|
712
|
+
|
|
569
713
|
it("proceeds when task-derived running status is stale and the runner is gone", async () => {
|
|
570
714
|
const root = await makeTempRoot();
|
|
571
715
|
initPATHS(root);
|
|
@@ -813,6 +957,265 @@ describe("handleJobRestart", () => {
|
|
|
813
957
|
});
|
|
814
958
|
});
|
|
815
959
|
|
|
960
|
+
describe("handleJobRestart dependency gate (AC-5 restart, AC-8, AC-9)", () => {
|
|
961
|
+
it("returns 412 and leaves status/lease untouched when a from-task predecessor is pending", async () => {
|
|
962
|
+
const root = await makeTempRoot();
|
|
963
|
+
initPATHS(root);
|
|
964
|
+
await setupPipelineConfig(root, "restart-deps-pending-pipeline", ["research", "analysis"]);
|
|
965
|
+
|
|
966
|
+
const statusObj = {
|
|
967
|
+
id: "restart-deps-pending",
|
|
968
|
+
pipeline: "restart-deps-pending-pipeline",
|
|
969
|
+
state: "failed",
|
|
970
|
+
current: "analysis",
|
|
971
|
+
currentStage: null,
|
|
972
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
973
|
+
tasks: {
|
|
974
|
+
research: { state: "pending", currentStage: null },
|
|
975
|
+
analysis: { state: "pending", currentStage: null },
|
|
976
|
+
},
|
|
977
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
978
|
+
};
|
|
979
|
+
const jobDir = await setupJob(root, "restart-deps-pending", statusObj);
|
|
980
|
+
|
|
981
|
+
const statusPath = path.join(jobDir, "tasks-status.json");
|
|
982
|
+
const snapshotBefore = await Bun.file(statusPath).text();
|
|
983
|
+
const spawnSpy = vi.spyOn(Bun, "spawn");
|
|
984
|
+
|
|
985
|
+
const req = new Request("http://localhost/api/jobs/restart-deps-pending/restart", {
|
|
986
|
+
method: "POST",
|
|
987
|
+
body: JSON.stringify({ fromTask: "analysis" }),
|
|
988
|
+
});
|
|
989
|
+
const res = await handleJobRestart(req, "restart-deps-pending", root);
|
|
990
|
+
const body = await res.json() as Record<string, unknown>;
|
|
991
|
+
|
|
992
|
+
expect(res.status).toBe(412);
|
|
993
|
+
expect(body["code"]).toBe("dependencies_not_satisfied");
|
|
994
|
+
expect(body["message"]).toContain("research");
|
|
995
|
+
|
|
996
|
+
expect(await Bun.file(statusPath).text()).toBe(snapshotBefore);
|
|
997
|
+
expect(spawnSpy).not.toHaveBeenCalled();
|
|
998
|
+
|
|
999
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(path.join(root, "pipeline-data"));
|
|
1000
|
+
expect(await Bun.file(path.join(runningJobsDir, "restart-deps-pending.json")).exists()).toBe(false);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it("proceeds when from-task predecessor is done", async () => {
|
|
1004
|
+
const root = await makeTempRoot();
|
|
1005
|
+
initPATHS(root);
|
|
1006
|
+
await setupPipelineConfig(root, "restart-deps-done-pipeline", ["research", "analysis"]);
|
|
1007
|
+
mockRunnerSpawn();
|
|
1008
|
+
|
|
1009
|
+
await setupJob(root, "restart-deps-done", {
|
|
1010
|
+
id: "restart-deps-done",
|
|
1011
|
+
pipeline: "restart-deps-done-pipeline",
|
|
1012
|
+
state: "failed",
|
|
1013
|
+
current: "analysis",
|
|
1014
|
+
currentStage: null,
|
|
1015
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
1016
|
+
tasks: {
|
|
1017
|
+
research: { state: "done", currentStage: null },
|
|
1018
|
+
analysis: { state: "pending", currentStage: null },
|
|
1019
|
+
},
|
|
1020
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
const req = new Request("http://localhost/api/jobs/restart-deps-done/restart", {
|
|
1024
|
+
method: "POST",
|
|
1025
|
+
body: JSON.stringify({ fromTask: "analysis" }),
|
|
1026
|
+
});
|
|
1027
|
+
const res = await handleJobRestart(req, "restart-deps-done", root);
|
|
1028
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1029
|
+
|
|
1030
|
+
expect(res.status).toBe(202);
|
|
1031
|
+
expect(body["ok"]).toBe(true);
|
|
1032
|
+
expect(body["mode"]).toBe("from-task");
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
it("proceeds when from-task predecessor is skipped", async () => {
|
|
1036
|
+
const root = await makeTempRoot();
|
|
1037
|
+
initPATHS(root);
|
|
1038
|
+
await setupPipelineConfig(root, "restart-deps-skipped-pipeline", ["research", "analysis"]);
|
|
1039
|
+
mockRunnerSpawn();
|
|
1040
|
+
|
|
1041
|
+
await setupJob(root, "restart-deps-skipped", {
|
|
1042
|
+
id: "restart-deps-skipped",
|
|
1043
|
+
pipeline: "restart-deps-skipped-pipeline",
|
|
1044
|
+
state: "failed",
|
|
1045
|
+
current: "analysis",
|
|
1046
|
+
currentStage: null,
|
|
1047
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
1048
|
+
tasks: {
|
|
1049
|
+
research: { state: "skipped", currentStage: null },
|
|
1050
|
+
analysis: { state: "pending", currentStage: null },
|
|
1051
|
+
},
|
|
1052
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
const req = new Request("http://localhost/api/jobs/restart-deps-skipped/restart", {
|
|
1056
|
+
method: "POST",
|
|
1057
|
+
body: JSON.stringify({ fromTask: "analysis" }),
|
|
1058
|
+
});
|
|
1059
|
+
const res = await handleJobRestart(req, "restart-deps-skipped", root);
|
|
1060
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1061
|
+
|
|
1062
|
+
expect(res.status).toBe(202);
|
|
1063
|
+
expect(body["ok"]).toBe(true);
|
|
1064
|
+
expect(body["mode"]).toBe("from-task");
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it("gates single-task restart identically when a predecessor is pending", async () => {
|
|
1068
|
+
const root = await makeTempRoot();
|
|
1069
|
+
initPATHS(root);
|
|
1070
|
+
await setupPipelineConfig(root, "restart-single-deps-pending-pipeline", ["review", "deploy"]);
|
|
1071
|
+
|
|
1072
|
+
const statusObj = {
|
|
1073
|
+
id: "restart-single-deps-pending",
|
|
1074
|
+
pipeline: "restart-single-deps-pending-pipeline",
|
|
1075
|
+
state: "failed",
|
|
1076
|
+
current: "deploy",
|
|
1077
|
+
currentStage: null,
|
|
1078
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
1079
|
+
tasks: {
|
|
1080
|
+
review: { state: "pending", currentStage: null },
|
|
1081
|
+
deploy: { state: "pending", currentStage: null },
|
|
1082
|
+
},
|
|
1083
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1084
|
+
};
|
|
1085
|
+
const jobDir = await setupJob(root, "restart-single-deps-pending", statusObj);
|
|
1086
|
+
|
|
1087
|
+
const statusPath = path.join(jobDir, "tasks-status.json");
|
|
1088
|
+
const snapshotBefore = await Bun.file(statusPath).text();
|
|
1089
|
+
const spawnSpy = vi.spyOn(Bun, "spawn");
|
|
1090
|
+
|
|
1091
|
+
const req = new Request("http://localhost/api/jobs/restart-single-deps-pending/restart", {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
body: JSON.stringify({ fromTask: "deploy", singleTask: true }),
|
|
1094
|
+
});
|
|
1095
|
+
const res = await handleJobRestart(req, "restart-single-deps-pending", root);
|
|
1096
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1097
|
+
|
|
1098
|
+
expect(res.status).toBe(412);
|
|
1099
|
+
expect(body["code"]).toBe("dependencies_not_satisfied");
|
|
1100
|
+
expect(body["message"]).toContain("review");
|
|
1101
|
+
|
|
1102
|
+
expect(await Bun.file(statusPath).text()).toBe(snapshotBefore);
|
|
1103
|
+
expect(spawnSpy).not.toHaveBeenCalled();
|
|
1104
|
+
|
|
1105
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(path.join(root, "pipeline-data"));
|
|
1106
|
+
expect(await Bun.file(path.join(runningJobsDir, "restart-single-deps-pending.json")).exists()).toBe(false);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it("does not gate clean-slate restart (no fromTask) even when a predecessor is pending", async () => {
|
|
1110
|
+
const root = await makeTempRoot();
|
|
1111
|
+
initPATHS(root);
|
|
1112
|
+
await setupPipelineConfig(root, "restart-clean-ungated-pipeline", ["research", "analysis"]);
|
|
1113
|
+
mockRunnerSpawn();
|
|
1114
|
+
|
|
1115
|
+
await setupJob(root, "restart-clean-ungated", {
|
|
1116
|
+
id: "restart-clean-ungated",
|
|
1117
|
+
pipeline: "restart-clean-ungated-pipeline",
|
|
1118
|
+
state: "failed",
|
|
1119
|
+
current: "analysis",
|
|
1120
|
+
currentStage: null,
|
|
1121
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
1122
|
+
tasks: {
|
|
1123
|
+
research: { state: "pending", currentStage: null },
|
|
1124
|
+
analysis: { state: "pending", currentStage: null },
|
|
1125
|
+
},
|
|
1126
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
const req = new Request("http://localhost/api/jobs/restart-clean-ungated/restart", { method: "POST" });
|
|
1130
|
+
const res = await handleJobRestart(req, "restart-clean-ungated", root);
|
|
1131
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1132
|
+
|
|
1133
|
+
expect(res.status).toBe(202);
|
|
1134
|
+
expect(body["ok"]).toBe(true);
|
|
1135
|
+
expect(body["mode"]).toBe("clean-slate");
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it("returns 500 status_unavailable when the pipeline task order cannot be loaded", async () => {
|
|
1139
|
+
const root = await makeTempRoot();
|
|
1140
|
+
initPATHS(root);
|
|
1141
|
+
|
|
1142
|
+
const statusObj = {
|
|
1143
|
+
id: "restart-no-order",
|
|
1144
|
+
state: "failed",
|
|
1145
|
+
current: "analysis",
|
|
1146
|
+
currentStage: null,
|
|
1147
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
1148
|
+
tasks: {
|
|
1149
|
+
research: { state: "done", currentStage: null },
|
|
1150
|
+
analysis: { state: "pending", currentStage: null },
|
|
1151
|
+
},
|
|
1152
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1153
|
+
};
|
|
1154
|
+
const jobDir = await setupJob(root, "restart-no-order", statusObj);
|
|
1155
|
+
|
|
1156
|
+
const statusPath = path.join(jobDir, "tasks-status.json");
|
|
1157
|
+
const snapshotBefore = await Bun.file(statusPath).text();
|
|
1158
|
+
const spawnSpy = vi.spyOn(Bun, "spawn");
|
|
1159
|
+
|
|
1160
|
+
const req = new Request("http://localhost/api/jobs/restart-no-order/restart", {
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
body: JSON.stringify({ fromTask: "analysis" }),
|
|
1163
|
+
});
|
|
1164
|
+
const res = await handleJobRestart(req, "restart-no-order", root);
|
|
1165
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1166
|
+
|
|
1167
|
+
expect(res.status).toBe(500);
|
|
1168
|
+
expect(body["code"]).toBe("status_unavailable");
|
|
1169
|
+
|
|
1170
|
+
expect(await Bun.file(statusPath).text()).toBe(snapshotBefore);
|
|
1171
|
+
expect(spawnSpy).not.toHaveBeenCalled();
|
|
1172
|
+
|
|
1173
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(path.join(root, "pipeline-data"));
|
|
1174
|
+
expect(await Bun.file(path.join(runningJobsDir, "restart-no-order.json")).exists()).toBe(false);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it("returns 404 task_not_found when fromTask is absent from the pipeline order", async () => {
|
|
1178
|
+
const root = await makeTempRoot();
|
|
1179
|
+
initPATHS(root);
|
|
1180
|
+
await setupPipelineConfig(root, "restart-unknown-task-pipeline", ["research", "analysis"]);
|
|
1181
|
+
|
|
1182
|
+
const statusObj = {
|
|
1183
|
+
id: "restart-unknown-task",
|
|
1184
|
+
pipeline: "restart-unknown-task-pipeline",
|
|
1185
|
+
state: "failed",
|
|
1186
|
+
current: "analysis",
|
|
1187
|
+
currentStage: null,
|
|
1188
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
1189
|
+
tasks: {
|
|
1190
|
+
research: { state: "done", currentStage: null },
|
|
1191
|
+
analysis: { state: "pending", currentStage: null },
|
|
1192
|
+
},
|
|
1193
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1194
|
+
};
|
|
1195
|
+
const jobDir = await setupJob(root, "restart-unknown-task", statusObj);
|
|
1196
|
+
|
|
1197
|
+
const statusPath = path.join(jobDir, "tasks-status.json");
|
|
1198
|
+
const snapshotBefore = await Bun.file(statusPath).text();
|
|
1199
|
+
const spawnSpy = vi.spyOn(Bun, "spawn");
|
|
1200
|
+
|
|
1201
|
+
const req = new Request("http://localhost/api/jobs/restart-unknown-task/restart", {
|
|
1202
|
+
method: "POST",
|
|
1203
|
+
body: JSON.stringify({ fromTask: "synthesis" }),
|
|
1204
|
+
});
|
|
1205
|
+
const res = await handleJobRestart(req, "restart-unknown-task", root);
|
|
1206
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1207
|
+
|
|
1208
|
+
expect(res.status).toBe(404);
|
|
1209
|
+
expect(body["code"]).toBe("task_not_found");
|
|
1210
|
+
|
|
1211
|
+
expect(await Bun.file(statusPath).text()).toBe(snapshotBefore);
|
|
1212
|
+
expect(spawnSpy).not.toHaveBeenCalled();
|
|
1213
|
+
|
|
1214
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(path.join(root, "pipeline-data"));
|
|
1215
|
+
expect(await Bun.file(path.join(runningJobsDir, "restart-unknown-task.json")).exists()).toBe(false);
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
|
|
816
1219
|
describe("handleTaskStart", () => {
|
|
817
1220
|
it("allows restart/start when snapshot has no valid lastUpdated and no live PID (crashed runner scenario)", async () => {
|
|
818
1221
|
// A runner that crashed before writing a valid lastUpdated should not permanently block
|
|
@@ -939,6 +1342,34 @@ describe("handleTaskStart", () => {
|
|
|
939
1342
|
expect(body["code"]).toBe("job_running");
|
|
940
1343
|
});
|
|
941
1344
|
|
|
1345
|
+
it("uses configured staleRunningGraceMs when deciding whether task start can recover a running task", async () => {
|
|
1346
|
+
const root = await makeTempRoot();
|
|
1347
|
+
initPATHS(root);
|
|
1348
|
+
process.env["PO_STALE_RUNNING_GRACE_MS"] = "60000";
|
|
1349
|
+
resetConfig();
|
|
1350
|
+
|
|
1351
|
+
vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-04-01T10:00:45.000Z"));
|
|
1352
|
+
await setupJob(root, "task-start-configured-grace", {
|
|
1353
|
+
id: "task-start-configured-grace",
|
|
1354
|
+
state: "running",
|
|
1355
|
+
current: "research",
|
|
1356
|
+
currentStage: "prompt",
|
|
1357
|
+
lastUpdated: "2026-04-01T10:00:00.000Z",
|
|
1358
|
+
tasks: {
|
|
1359
|
+
research: { state: "running", currentStage: "prompt" },
|
|
1360
|
+
analysis: { state: "pending", currentStage: null },
|
|
1361
|
+
},
|
|
1362
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1363
|
+
}, 999999);
|
|
1364
|
+
|
|
1365
|
+
const req = new Request("http://localhost/api/jobs/task-start-configured-grace/tasks/analysis/start", { method: "POST" });
|
|
1366
|
+
const res = await handleTaskStart(req, "task-start-configured-grace", "analysis", root);
|
|
1367
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1368
|
+
|
|
1369
|
+
expect(res.status).toBe(409);
|
|
1370
|
+
expect(body["code"]).toBe("job_running");
|
|
1371
|
+
});
|
|
1372
|
+
|
|
942
1373
|
it("returns 202 when stale-running status is the only blocker for an eligible pending task", async () => {
|
|
943
1374
|
const root = await makeTempRoot();
|
|
944
1375
|
initPATHS(root);
|
|
@@ -1239,6 +1670,7 @@ describe("handleTaskStart", () => {
|
|
|
1239
1670
|
|
|
1240
1671
|
expect(res.status).toBe(412);
|
|
1241
1672
|
expect(body["code"]).toBe("dependencies_not_satisfied");
|
|
1673
|
+
expect(body["message"]).toContain("research");
|
|
1242
1674
|
});
|
|
1243
1675
|
|
|
1244
1676
|
it("returns 412 dependencies_not_satisfied when an earlier task is \"failed\"", async () => {
|
|
@@ -1265,6 +1697,80 @@ describe("handleTaskStart", () => {
|
|
|
1265
1697
|
|
|
1266
1698
|
expect(res.status).toBe(412);
|
|
1267
1699
|
expect(body["code"]).toBe("dependencies_not_satisfied");
|
|
1700
|
+
expect(body["message"]).toContain("research");
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
it("returns 202 when an earlier task is \"skipped\" (done-or-skipped dependency rule)", async () => {
|
|
1704
|
+
const root = await makeTempRoot();
|
|
1705
|
+
initPATHS(root);
|
|
1706
|
+
await setupPipelineConfig(root, "task-start-skipped-deps-pipeline", ["research", "analysis"]);
|
|
1707
|
+
const spawnSpy = mockRunnerSpawn(88888);
|
|
1708
|
+
|
|
1709
|
+
await setupJob(root, "task-start-skipped-deps", {
|
|
1710
|
+
id: "task-start-skipped-deps",
|
|
1711
|
+
pipeline: "task-start-skipped-deps-pipeline",
|
|
1712
|
+
state: "pending",
|
|
1713
|
+
current: null,
|
|
1714
|
+
currentStage: null,
|
|
1715
|
+
tasks: {
|
|
1716
|
+
research: { state: "skipped", currentStage: null },
|
|
1717
|
+
analysis: { state: "pending", currentStage: null },
|
|
1718
|
+
},
|
|
1719
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
const req = new Request("http://localhost/api/jobs/task-start-skipped-deps/tasks/analysis/start", { method: "POST" });
|
|
1723
|
+
const res = await handleTaskStart(req, "task-start-skipped-deps", "analysis", root);
|
|
1724
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1725
|
+
|
|
1726
|
+
expect(res.status).toBe(202);
|
|
1727
|
+
expect(body["ok"]).toBe(true);
|
|
1728
|
+
expect(body["taskId"]).toBe("analysis");
|
|
1729
|
+
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
|
1730
|
+
});
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
describe("acquireConcurrencySlot stale-lease age (AC-5)", () => {
|
|
1734
|
+
it("does not prune a fresh null-pid lease when lockFileTimeout is short relative to PO_STALE_LEASE_TIMEOUT_MS", async () => {
|
|
1735
|
+
const root = await makeTempRoot();
|
|
1736
|
+
initPATHS(root);
|
|
1737
|
+
process.env["PO_STALE_LEASE_TIMEOUT_MS"] = "60000";
|
|
1738
|
+
resetConfig();
|
|
1739
|
+
|
|
1740
|
+
const now = Date.parse("2026-04-01T10:01:00.000Z");
|
|
1741
|
+
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
1742
|
+
|
|
1743
|
+
const jobDir = await setupJob(root, "ac-5-held", {
|
|
1744
|
+
id: "ac-5-held",
|
|
1745
|
+
state: "running",
|
|
1746
|
+
current: "research",
|
|
1747
|
+
currentStage: "prompt",
|
|
1748
|
+
lastUpdated: "2026-04-01T10:00:15.000Z",
|
|
1749
|
+
tasks: {
|
|
1750
|
+
research: { state: "running", currentStage: "prompt" },
|
|
1751
|
+
},
|
|
1752
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
1753
|
+
}, 999999);
|
|
1754
|
+
|
|
1755
|
+
const runningJobsDir = path.join(root, "pipeline-data", "runtime", "running-jobs");
|
|
1756
|
+
await mkdir(runningJobsDir, { recursive: true });
|
|
1757
|
+
const slotPath = path.join(runningJobsDir, "ac-5-held.json");
|
|
1758
|
+
await writeFile(slotPath, JSON.stringify({
|
|
1759
|
+
jobId: "ac-5-held",
|
|
1760
|
+
pid: null,
|
|
1761
|
+
acquiredAt: new Date(now - 40_000).toISOString(),
|
|
1762
|
+
source: "restart",
|
|
1763
|
+
slotPath,
|
|
1764
|
+
}));
|
|
1765
|
+
|
|
1766
|
+
const req = new Request("http://localhost/api/jobs/ac-5-held/restart", { method: "POST" });
|
|
1767
|
+
const res = await handleJobRestart(req, "ac-5-held", root);
|
|
1768
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1769
|
+
|
|
1770
|
+
expect(res.status).toBe(409);
|
|
1771
|
+
expect(body["code"]).toBe("job_running");
|
|
1772
|
+
|
|
1773
|
+
expect(await Bun.file(slotPath).text()).toBeDefined();
|
|
1268
1774
|
});
|
|
1269
1775
|
});
|
|
1270
1776
|
|
|
@@ -1811,6 +2317,10 @@ describe("corrupt status handling", () => {
|
|
|
1811
2317
|
tasks: { research: { state: "pending" } },
|
|
1812
2318
|
files: { artifacts: [], logs: [], tmp: [] },
|
|
1813
2319
|
});
|
|
2320
|
+
await writeFile(path.join(jobDir, "pipeline.json"), JSON.stringify({
|
|
2321
|
+
name: "write-corrupt-restart-pipeline",
|
|
2322
|
+
tasks: [{ name: "research" }],
|
|
2323
|
+
}));
|
|
1814
2324
|
const statusPath = path.join(jobDir, "tasks-status.json");
|
|
1815
2325
|
corruptStatusOnTextRead(statusPath, 3);
|
|
1816
2326
|
|