@ryanfw/prompt-orchestration-pipeline 1.3.3 → 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 +14 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +144 -41
- package/src/api/index.ts +15 -83
- 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/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +517 -107
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +200 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +485 -3
- 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 +373 -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 +45 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +1 -1
- 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 +127 -234
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +50 -19
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +162 -18
- 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 +124 -70
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +8 -4
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +3 -28
- 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 +125 -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 +31 -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 +14 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +13 -2
- 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 +83 -0
- 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 +33 -15
- package/src/ui/dist/assets/{index-L6cvsCAx.js → index-DN3-zvtP.js} +4299 -251
- 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__/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 +455 -1
- package/src/ui/server/__tests__/job-endpoints.test.ts +43 -1
- 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 +39 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- 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__/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/job-control-endpoints.ts +122 -73
- package/src/ui/server/endpoints/job-endpoints.ts +1 -0
- 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 +70 -15
- package/src/ui/state/__tests__/types.test.ts +6 -0
- package/src/ui/state/snapshot.ts +1 -1
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +42 -0
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +45 -3
- package/src/ui/state/transformers/list-transformer.ts +6 -0
- package/src/ui/state/types.ts +6 -1
- 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-L6cvsCAx.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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
5
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { startOrchestrator } from "../orchestrator";
|
|
7
|
+
import { resolveJobConfig, runPipelineJob } from "../pipeline-runner";
|
|
8
|
+
import { resetConfig } from "../config";
|
|
9
|
+
|
|
10
|
+
// PO_* keys this test mutates (capture original values in beforeEach-equivalent,
|
|
11
|
+
// restore by deletion-or-assignment in afterEach per unit-testing.md §6).
|
|
12
|
+
const PO_ENV_KEYS = [
|
|
13
|
+
"PO_ROOT",
|
|
14
|
+
"PO_DATA_DIR",
|
|
15
|
+
"PO_CURRENT_DIR",
|
|
16
|
+
"PO_COMPLETE_DIR",
|
|
17
|
+
"PO_PIPELINE_PATH",
|
|
18
|
+
] as const;
|
|
19
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
20
|
+
const cleanupDirs: string[] = [];
|
|
21
|
+
|
|
22
|
+
function snapshotEnv(): void {
|
|
23
|
+
for (const key of PO_ENV_KEYS) {
|
|
24
|
+
savedEnv[key] = process.env[key];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function restoreEnv(): void {
|
|
29
|
+
for (const key of PO_ENV_KEYS) {
|
|
30
|
+
if (savedEnv[key] === undefined) delete process.env[key];
|
|
31
|
+
else process.env[key] = savedEnv[key];
|
|
32
|
+
}
|
|
33
|
+
resetConfig();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FakeWatcher {
|
|
37
|
+
on(event: "add", cb: (path: string) => void): FakeWatcher;
|
|
38
|
+
on(event: "ready", cb: () => void): FakeWatcher;
|
|
39
|
+
on(event: "error", cb: (err: Error) => void): FakeWatcher;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fakeWatcherFactory(): (
|
|
44
|
+
path: string,
|
|
45
|
+
options: Record<string, unknown>,
|
|
46
|
+
) => FakeWatcher {
|
|
47
|
+
const watcher = {
|
|
48
|
+
on(event: "add" | "ready" | "error", cb: () => void): FakeWatcher {
|
|
49
|
+
if (event === "ready") queueMicrotask(() => cb());
|
|
50
|
+
return watcher;
|
|
51
|
+
},
|
|
52
|
+
close(): Promise<void> {
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
},
|
|
55
|
+
} as unknown as FakeWatcher;
|
|
56
|
+
return (_p: string, _o: Record<string, unknown>) => watcher;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("core boot resolution (AC-7)", () => {
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
restoreEnv();
|
|
62
|
+
await Promise.all(
|
|
63
|
+
cleanupDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("startOrchestrator", () => {
|
|
68
|
+
test("with explicit dataDir, creates pipeline-data subdirs under the resolved root", async () => {
|
|
69
|
+
snapshotEnv();
|
|
70
|
+
delete process.env["PO_ROOT"];
|
|
71
|
+
|
|
72
|
+
const root = mkdtempSync(join(tmpdir(), "boot-resolve-explicit-"));
|
|
73
|
+
cleanupDirs.push(root);
|
|
74
|
+
const dataDir = resolvePath(join(root, "pipeline-data"));
|
|
75
|
+
|
|
76
|
+
const handle = await startOrchestrator({
|
|
77
|
+
dataDir,
|
|
78
|
+
watcherFactory: fakeWatcherFactory(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
for (const sub of ["pending", "current", "complete", "rejected", "staging"]) {
|
|
83
|
+
expect(existsSync(join(root, "pipeline-data", sub))).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
await handle.stop();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("with PO_ROOT set and no explicit dataDir, resolves under PO_ROOT", async () => {
|
|
91
|
+
snapshotEnv();
|
|
92
|
+
const root = mkdtempSync(join(tmpdir(), "boot-resolve-envroot-"));
|
|
93
|
+
cleanupDirs.push(root);
|
|
94
|
+
process.env["PO_ROOT"] = root;
|
|
95
|
+
|
|
96
|
+
const handle = await startOrchestrator({
|
|
97
|
+
dataDir: undefined,
|
|
98
|
+
watcherFactory: fakeWatcherFactory(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
for (const sub of ["pending", "current", "complete", "rejected", "staging"]) {
|
|
103
|
+
expect(existsSync(join(root, "pipeline-data", sub))).toBe(true);
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
await handle.stop();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("with neither explicit dataDir nor PO_ROOT, throws the resolver message", () => {
|
|
111
|
+
snapshotEnv();
|
|
112
|
+
delete process.env["PO_ROOT"];
|
|
113
|
+
|
|
114
|
+
expect(() =>
|
|
115
|
+
startOrchestrator({
|
|
116
|
+
dataDir: undefined,
|
|
117
|
+
watcherFactory: fakeWatcherFactory(),
|
|
118
|
+
}),
|
|
119
|
+
).toThrow(/Workspace root is required/);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("resolveJobConfig", () => {
|
|
124
|
+
async function setupJobDir(tmpRoot: string, jobId: string): Promise<void> {
|
|
125
|
+
const currentDir = join(tmpRoot, "pipeline-data", "current");
|
|
126
|
+
await mkdir(currentDir, { recursive: true });
|
|
127
|
+
const jobDir = join(currentDir, jobId);
|
|
128
|
+
await mkdir(jobDir, { recursive: true });
|
|
129
|
+
await writeFile(
|
|
130
|
+
join(jobDir, "seed.json"),
|
|
131
|
+
JSON.stringify({ pipeline: "boot-test" }),
|
|
132
|
+
);
|
|
133
|
+
await writeFile(
|
|
134
|
+
join(jobDir, "tasks-status.json"),
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
id: jobId,
|
|
137
|
+
state: "pending",
|
|
138
|
+
current: null,
|
|
139
|
+
currentStage: null,
|
|
140
|
+
lastUpdated: new Date().toISOString(),
|
|
141
|
+
tasks: {},
|
|
142
|
+
files: { artifacts: [], logs: [], tmp: [] },
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test("with a resolved root, poRoot equals the supplied root", async () => {
|
|
148
|
+
snapshotEnv();
|
|
149
|
+
const root = mkdtempSync(join(tmpdir(), "boot-resolvejob-po-"));
|
|
150
|
+
cleanupDirs.push(root);
|
|
151
|
+
process.env["PO_PIPELINE_PATH"] = join(root, "pipeline.json");
|
|
152
|
+
await setupJobDir(root, "boot-test-job");
|
|
153
|
+
|
|
154
|
+
const config = await resolveJobConfig("boot-test-job", resolvePath(root));
|
|
155
|
+
expect(config.poRoot).toBe(resolvePath(root));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("with PO_ROOT deleted, still uses the supplied resolved root", async () => {
|
|
159
|
+
snapshotEnv();
|
|
160
|
+
const root = mkdtempSync(join(tmpdir(), "boot-resolvejob-explicit-"));
|
|
161
|
+
cleanupDirs.push(root);
|
|
162
|
+
delete process.env["PO_ROOT"];
|
|
163
|
+
process.env["PO_PIPELINE_PATH"] = join(root, "pipeline.json");
|
|
164
|
+
await setupJobDir(root, "boot-test-job-explicit");
|
|
165
|
+
|
|
166
|
+
const config = await resolveJobConfig("boot-test-job-explicit", resolvePath(root));
|
|
167
|
+
expect(config.poRoot).toBe(resolvePath(root));
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("runPipelineJob", () => {
|
|
172
|
+
test("with PO_ROOT deleted, rejects with the resolver message at boot", async () => {
|
|
173
|
+
snapshotEnv();
|
|
174
|
+
delete process.env["PO_ROOT"];
|
|
175
|
+
|
|
176
|
+
await expect(runPipelineJob("any-job-id")).rejects.toThrow(
|
|
177
|
+
/Workspace root is required/,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process";
|
|
|
7
7
|
import {
|
|
8
8
|
getConcurrencyRuntimePaths,
|
|
9
9
|
getJobConcurrencyStatus,
|
|
10
|
+
listActiveJobLeases,
|
|
10
11
|
listQueuedSeeds,
|
|
11
12
|
pruneStaleJobSlots,
|
|
12
13
|
readJobSlotLease,
|
|
@@ -140,6 +141,21 @@ describe("listQueuedSeeds", () => {
|
|
|
140
141
|
await rm(dir, { recursive: true });
|
|
141
142
|
}
|
|
142
143
|
});
|
|
144
|
+
|
|
145
|
+
test("matches a UUID <id>-seed.json and ignores foo.json under the shared regex", async () => {
|
|
146
|
+
const dir = await mkdtemp(join(tmpdir(), "queued-seeds-shared-"));
|
|
147
|
+
const pendingDir = join(dir, "pending");
|
|
148
|
+
await mkdir(pendingDir, { recursive: true });
|
|
149
|
+
const uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
|
|
150
|
+
await writeFile(join(pendingDir, `${uuid}-seed.json`), JSON.stringify({ pipeline: "p" }));
|
|
151
|
+
await writeFile(join(pendingDir, "foo.json"), "{}");
|
|
152
|
+
try {
|
|
153
|
+
const result = await listQueuedSeeds(dir);
|
|
154
|
+
expect(result.map((s) => s.jobId)).toEqual([uuid]);
|
|
155
|
+
} finally {
|
|
156
|
+
await rm(dir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
143
159
|
});
|
|
144
160
|
|
|
145
161
|
async function writeLease(
|
|
@@ -335,6 +351,121 @@ describe("pruneStaleJobSlots", () => {
|
|
|
335
351
|
});
|
|
336
352
|
});
|
|
337
353
|
|
|
354
|
+
describe("pruneStaleJobSlots — stale_runner backstop", () => {
|
|
355
|
+
test("reclaims an aged live-pid lease when runner.pid is absent (AC-3)", async () => {
|
|
356
|
+
const dir = await mkdtemp(join(tmpdir(), "prune-stale-runner-"));
|
|
357
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
358
|
+
await mkdir(runningJobsDir, { recursive: true });
|
|
359
|
+
await mkdir(join(dir, "current", "job-reuse"), { recursive: true });
|
|
360
|
+
const slotPath = await writeLease(runningJobsDir, "job-reuse", {
|
|
361
|
+
jobId: "job-reuse",
|
|
362
|
+
pid: process.pid,
|
|
363
|
+
acquiredAt: new Date(Date.now() - 60_000).toISOString(),
|
|
364
|
+
source: "orchestrator",
|
|
365
|
+
slotPath: join(runningJobsDir, "job-reuse.json"),
|
|
366
|
+
});
|
|
367
|
+
try {
|
|
368
|
+
const result = await pruneStaleJobSlots(dir, 1000, 60_000);
|
|
369
|
+
expect(result).toEqual([
|
|
370
|
+
{ jobId: "job-reuse", slotPath, reason: "stale_runner" },
|
|
371
|
+
]);
|
|
372
|
+
expect(existsSync(slotPath)).toBe(false);
|
|
373
|
+
} finally {
|
|
374
|
+
await rm(dir, { recursive: true });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("retains an aged live-pid lease when runner.pid matches (AC-8)", async () => {
|
|
379
|
+
const dir = await mkdtemp(join(tmpdir(), "prune-stale-runner-live-"));
|
|
380
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
381
|
+
await mkdir(runningJobsDir, { recursive: true });
|
|
382
|
+
await mkdir(join(dir, "current", "job-running"), { recursive: true });
|
|
383
|
+
await writeFile(join(dir, "current", "job-running", "runner.pid"), `${process.pid}\n`);
|
|
384
|
+
const slotPath = await writeLease(runningJobsDir, "job-running", {
|
|
385
|
+
jobId: "job-running",
|
|
386
|
+
pid: process.pid,
|
|
387
|
+
acquiredAt: new Date(Date.now() - 60_000).toISOString(),
|
|
388
|
+
source: "orchestrator",
|
|
389
|
+
slotPath: join(runningJobsDir, "job-running.json"),
|
|
390
|
+
});
|
|
391
|
+
try {
|
|
392
|
+
const result = await pruneStaleJobSlots(dir, 1000, 60_000);
|
|
393
|
+
expect(result).toEqual([]);
|
|
394
|
+
expect(existsSync(slotPath)).toBe(true);
|
|
395
|
+
} finally {
|
|
396
|
+
await rm(dir, { recursive: true });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("does not prune a null-pid lease aged past lockTimeoutMs but within staleLeaseTimeoutMs (AC-5)", async () => {
|
|
401
|
+
const dir = await mkdtemp(join(tmpdir(), "prune-stale-split-"));
|
|
402
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
403
|
+
await mkdir(runningJobsDir, { recursive: true });
|
|
404
|
+
await mkdir(join(dir, "current", "job-inflight"), { recursive: true });
|
|
405
|
+
const slotPath = await writeLease(runningJobsDir, "job-inflight", {
|
|
406
|
+
jobId: "job-inflight",
|
|
407
|
+
pid: null,
|
|
408
|
+
acquiredAt: new Date(Date.now() - 5_000).toISOString(),
|
|
409
|
+
source: "orchestrator",
|
|
410
|
+
slotPath: join(runningJobsDir, "job-inflight.json"),
|
|
411
|
+
});
|
|
412
|
+
try {
|
|
413
|
+
const result = await pruneStaleJobSlots(dir, 1000, 60_000);
|
|
414
|
+
expect(result).toEqual([]);
|
|
415
|
+
expect(existsSync(slotPath)).toBe(true);
|
|
416
|
+
} finally {
|
|
417
|
+
await rm(dir, { recursive: true });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("omitting staleLeaseTimeoutMs reproduces current missing_pid behavior (AC-6)", async () => {
|
|
422
|
+
const dir = await mkdtemp(join(tmpdir(), "prune-stale-default-"));
|
|
423
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
424
|
+
await mkdir(runningJobsDir, { recursive: true });
|
|
425
|
+
await mkdir(join(dir, "current", "job-default"), { recursive: true });
|
|
426
|
+
const slotPath = await writeLease(runningJobsDir, "job-default", {
|
|
427
|
+
jobId: "job-default",
|
|
428
|
+
pid: null,
|
|
429
|
+
acquiredAt: new Date(Date.now() - 60_000).toISOString(),
|
|
430
|
+
source: "orchestrator",
|
|
431
|
+
slotPath: join(runningJobsDir, "job-default.json"),
|
|
432
|
+
});
|
|
433
|
+
try {
|
|
434
|
+
const result = await pruneStaleJobSlots(dir, 1000);
|
|
435
|
+
expect(result).toEqual([
|
|
436
|
+
{ jobId: "job-default", slotPath, reason: "missing_pid" },
|
|
437
|
+
]);
|
|
438
|
+
expect(existsSync(slotPath)).toBe(false);
|
|
439
|
+
} finally {
|
|
440
|
+
await rm(dir, { recursive: true });
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("omitting staleLeaseTimeoutMs reproduces current dead_pid behavior (AC-6)", async () => {
|
|
445
|
+
const dir = await mkdtemp(join(tmpdir(), "prune-stale-default-dead-"));
|
|
446
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
447
|
+
await mkdir(runningJobsDir, { recursive: true });
|
|
448
|
+
await mkdir(join(dir, "current", "job-dead-default"), { recursive: true });
|
|
449
|
+
const deadPid = getDeadPid();
|
|
450
|
+
const slotPath = await writeLease(runningJobsDir, "job-dead-default", {
|
|
451
|
+
jobId: "job-dead-default",
|
|
452
|
+
pid: deadPid,
|
|
453
|
+
acquiredAt: new Date().toISOString(),
|
|
454
|
+
source: "orchestrator",
|
|
455
|
+
slotPath: join(runningJobsDir, "job-dead-default.json"),
|
|
456
|
+
});
|
|
457
|
+
try {
|
|
458
|
+
const result = await pruneStaleJobSlots(dir, 1000);
|
|
459
|
+
expect(result).toEqual([
|
|
460
|
+
{ jobId: "job-dead-default", slotPath, reason: "dead_pid" },
|
|
461
|
+
]);
|
|
462
|
+
expect(existsSync(slotPath)).toBe(false);
|
|
463
|
+
} finally {
|
|
464
|
+
await rm(dir, { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
338
469
|
async function setupJobsDir(prefix: string): Promise<string> {
|
|
339
470
|
const dir = await mkdtemp(join(tmpdir(), prefix));
|
|
340
471
|
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
@@ -589,17 +720,64 @@ describe("readJobSlotLease", () => {
|
|
|
589
720
|
});
|
|
590
721
|
});
|
|
591
722
|
|
|
723
|
+
describe("listActiveJobLeases", () => {
|
|
724
|
+
test("returns the leases written by tryAcquireJobSlot", async () => {
|
|
725
|
+
const dir = await setupJobsDir("list-active-acquired-");
|
|
726
|
+
try {
|
|
727
|
+
await makeCurrent(dir, "job-a");
|
|
728
|
+
await makeCurrent(dir, "job-b");
|
|
729
|
+
await tryAcquireJobSlot({
|
|
730
|
+
dataDir: dir,
|
|
731
|
+
jobId: "job-a",
|
|
732
|
+
maxConcurrentJobs: 2,
|
|
733
|
+
source: "orchestrator",
|
|
734
|
+
pid: process.pid,
|
|
735
|
+
});
|
|
736
|
+
await tryAcquireJobSlot({
|
|
737
|
+
dataDir: dir,
|
|
738
|
+
jobId: "job-b",
|
|
739
|
+
maxConcurrentJobs: 2,
|
|
740
|
+
source: "restart",
|
|
741
|
+
pid: process.pid,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const leases = await listActiveJobLeases(dir);
|
|
745
|
+
expect(leases.map((l) => l.jobId)).toEqual(["job-a", "job-b"]);
|
|
746
|
+
const first = leases[0]!;
|
|
747
|
+
expect(first.pid).toBe(process.pid);
|
|
748
|
+
expect(first.source).toBe("orchestrator");
|
|
749
|
+
} finally {
|
|
750
|
+
await rm(dir, { recursive: true });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("returns [] for an absent runtime dir", async () => {
|
|
755
|
+
const dir = await mkdtemp(join(tmpdir(), "list-active-absent-"));
|
|
756
|
+
try {
|
|
757
|
+
expect(await listActiveJobLeases(dir)).toEqual([]);
|
|
758
|
+
} finally {
|
|
759
|
+
await rm(dir, { recursive: true });
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
592
764
|
describe("withRuntimeLock", () => {
|
|
593
765
|
test("serializes two concurrent callers", async () => {
|
|
594
766
|
const dir = await setupJobsDir("runtime-lock-serialize-");
|
|
595
767
|
const markerPath = join(dir, "marker.txt");
|
|
596
768
|
const events: string[] = [];
|
|
597
769
|
try {
|
|
770
|
+
let markAEntered!: () => void;
|
|
771
|
+
const aEntered = new Promise<void>((resolve) => {
|
|
772
|
+
markAEntered = resolve;
|
|
773
|
+
});
|
|
598
774
|
const a = withRuntimeLock(dir, 5000, async () => {
|
|
775
|
+
markAEntered();
|
|
599
776
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
600
777
|
await writeFile(markerPath, "from-a");
|
|
601
778
|
events.push("a-wrote");
|
|
602
779
|
});
|
|
780
|
+
await aEntered;
|
|
603
781
|
const b = withRuntimeLock(dir, 5000, async () => {
|
|
604
782
|
const observed = await readFile(markerPath, "utf-8");
|
|
605
783
|
events.push(`b-read:${observed}`);
|
|
@@ -611,4 +789,26 @@ describe("withRuntimeLock", () => {
|
|
|
611
789
|
await rm(dir, { recursive: true });
|
|
612
790
|
}
|
|
613
791
|
});
|
|
792
|
+
|
|
793
|
+
test("reclaims a stale owner file without a numeric pid using the stale age, not the acquire wait", async () => {
|
|
794
|
+
const dir = await setupJobsDir("runtime-lock-stale-owner-");
|
|
795
|
+
const { lockDir, runtimeDir } = getConcurrencyRuntimePaths(dir);
|
|
796
|
+
await mkdir(runtimeDir, { recursive: true });
|
|
797
|
+
await mkdir(lockDir, { recursive: false });
|
|
798
|
+
await writeFile(
|
|
799
|
+
join(lockDir, "owner.json"),
|
|
800
|
+
JSON.stringify({ acquiredAt: new Date(Date.now() - 60_000).toISOString() }),
|
|
801
|
+
);
|
|
802
|
+
try {
|
|
803
|
+
const result = await withRuntimeLock(
|
|
804
|
+
dir,
|
|
805
|
+
100,
|
|
806
|
+
async () => "reclaimed",
|
|
807
|
+
60_000,
|
|
808
|
+
);
|
|
809
|
+
expect(result).toBe("reclaimed");
|
|
810
|
+
} finally {
|
|
811
|
+
await rm(dir, { recursive: true });
|
|
812
|
+
}
|
|
813
|
+
});
|
|
614
814
|
});
|