@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
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { afterEach, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
7
|
import { handleTaskFile, handleTaskFileList } from "../endpoints/file-endpoints";
|
|
8
|
-
import {
|
|
8
|
+
import { resetPATHS } from "../config-bridge-node";
|
|
9
9
|
|
|
10
10
|
const tempRoots: string[] = [];
|
|
11
11
|
|
|
@@ -26,7 +26,6 @@ describe("file endpoints", () => {
|
|
|
26
26
|
it("lists task files from tasks-status metadata", async () => {
|
|
27
27
|
const root = await makeTempRoot();
|
|
28
28
|
process.env["PO_ROOT"] = root;
|
|
29
|
-
initPATHS(root);
|
|
30
29
|
|
|
31
30
|
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
32
31
|
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
@@ -46,7 +45,7 @@ describe("file endpoints", () => {
|
|
|
46
45
|
}),
|
|
47
46
|
);
|
|
48
47
|
|
|
49
|
-
const response = await handleTaskFileList(new Request("http://localhost/api/jobs/job-1/tasks/research/files?type=artifacts"), "job-1", "research");
|
|
48
|
+
const response = await handleTaskFileList(new Request("http://localhost/api/jobs/job-1/tasks/research/files?type=artifacts"), "job-1", "research", root);
|
|
50
49
|
|
|
51
50
|
expect(response.status).toBe(200);
|
|
52
51
|
await expect(response.json()).resolves.toEqual({ ok: true, data: ["research-output.json"] });
|
|
@@ -55,7 +54,6 @@ describe("file endpoints", () => {
|
|
|
55
54
|
it("reads task-owned files from the shared job files directory", async () => {
|
|
56
55
|
const root = await makeTempRoot();
|
|
57
56
|
process.env["PO_ROOT"] = root;
|
|
58
|
-
initPATHS(root);
|
|
59
57
|
|
|
60
58
|
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
61
59
|
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
@@ -80,6 +78,7 @@ describe("file endpoints", () => {
|
|
|
80
78
|
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=research-output.json"),
|
|
81
79
|
"job-1",
|
|
82
80
|
"research",
|
|
81
|
+
root,
|
|
83
82
|
);
|
|
84
83
|
|
|
85
84
|
expect(response.status).toBe(200);
|
|
@@ -93,7 +92,6 @@ describe("file endpoints", () => {
|
|
|
93
92
|
it("rejects files not declared for the task", async () => {
|
|
94
93
|
const root = await makeTempRoot();
|
|
95
94
|
process.env["PO_ROOT"] = root;
|
|
96
|
-
initPATHS(root);
|
|
97
95
|
|
|
98
96
|
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
99
97
|
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
@@ -118,6 +116,7 @@ describe("file endpoints", () => {
|
|
|
118
116
|
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=other.json"),
|
|
119
117
|
"job-1",
|
|
120
118
|
"research",
|
|
119
|
+
root,
|
|
121
120
|
);
|
|
122
121
|
|
|
123
122
|
expect(response.status).toBe(404);
|
|
@@ -127,4 +126,183 @@ describe("file endpoints", () => {
|
|
|
127
126
|
message: "file not found for task: artifacts/other.json",
|
|
128
127
|
});
|
|
129
128
|
});
|
|
129
|
+
|
|
130
|
+
it("reads files from the threaded root, ignoring PO_ROOT (AC-5)", async () => {
|
|
131
|
+
const threadedRoot = await makeTempRoot();
|
|
132
|
+
const envRoot = await makeTempRoot();
|
|
133
|
+
process.env["PO_ROOT"] = envRoot;
|
|
134
|
+
|
|
135
|
+
const jobDir = path.join(threadedRoot, "pipeline-data", "current", "job-1");
|
|
136
|
+
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
137
|
+
await writeFile(path.join(jobDir, "files", "artifacts", "threaded.json"), '{"source":"threaded"}\n');
|
|
138
|
+
await writeFile(
|
|
139
|
+
path.join(jobDir, "tasks-status.json"),
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
id: "job-1",
|
|
142
|
+
tasks: {
|
|
143
|
+
research: {
|
|
144
|
+
files: {
|
|
145
|
+
artifacts: ["threaded.json"],
|
|
146
|
+
logs: [],
|
|
147
|
+
tmp: [],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const response = await handleTaskFile(
|
|
155
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=threaded.json"),
|
|
156
|
+
"job-1",
|
|
157
|
+
"research",
|
|
158
|
+
threadedRoot,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(response.status).toBe(200);
|
|
162
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
163
|
+
ok: true,
|
|
164
|
+
data: '{"source":"threaded"}\n',
|
|
165
|
+
mime: "application/json",
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("rejects a traversal type query param with 400 (AC-5)", async () => {
|
|
170
|
+
const root = await makeTempRoot();
|
|
171
|
+
process.env["PO_ROOT"] = root;
|
|
172
|
+
|
|
173
|
+
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
174
|
+
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
175
|
+
await writeFile(
|
|
176
|
+
path.join(jobDir, "tasks-status.json"),
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
id: "job-1",
|
|
179
|
+
tasks: { research: { files: { artifacts: ["x.json"], logs: [], tmp: [] } } },
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const response = await handleTaskFile(
|
|
184
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=../..&filename=x.json"),
|
|
185
|
+
"job-1",
|
|
186
|
+
"research",
|
|
187
|
+
root,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(response.status).toBe(400);
|
|
191
|
+
await expect(response.json()).resolves.toEqual({ ok: false, code: "BAD_REQUEST", message: "invalid filename" });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("rejects a traversal filename query param with 400 (AC-5)", async () => {
|
|
195
|
+
const root = await makeTempRoot();
|
|
196
|
+
process.env["PO_ROOT"] = root;
|
|
197
|
+
|
|
198
|
+
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
199
|
+
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
200
|
+
await writeFile(
|
|
201
|
+
path.join(jobDir, "tasks-status.json"),
|
|
202
|
+
JSON.stringify({
|
|
203
|
+
id: "job-1",
|
|
204
|
+
tasks: { research: { files: { artifacts: ["x.json"], logs: [], tmp: [] } } },
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const response = await handleTaskFile(
|
|
209
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=../../secret"),
|
|
210
|
+
"job-1",
|
|
211
|
+
"research",
|
|
212
|
+
root,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(response.status).toBe(400);
|
|
216
|
+
await expect(response.json()).resolves.toEqual({ ok: false, code: "BAD_REQUEST", message: "invalid filename" });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("returns 404 for a listed-but-absent task file (AC-5)", async () => {
|
|
220
|
+
const root = await makeTempRoot();
|
|
221
|
+
process.env["PO_ROOT"] = root;
|
|
222
|
+
|
|
223
|
+
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
224
|
+
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
225
|
+
await writeFile(
|
|
226
|
+
path.join(jobDir, "tasks-status.json"),
|
|
227
|
+
JSON.stringify({
|
|
228
|
+
id: "job-1",
|
|
229
|
+
tasks: { research: { files: { artifacts: ["ghost.json"], logs: [], tmp: [] } } },
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const response = await handleTaskFile(
|
|
234
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=ghost.json"),
|
|
235
|
+
"job-1",
|
|
236
|
+
"research",
|
|
237
|
+
root,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(response.status).toBe(404);
|
|
241
|
+
await expect(response.json()).resolves.toEqual({
|
|
242
|
+
ok: false,
|
|
243
|
+
code: "NOT_FOUND",
|
|
244
|
+
message: "file not found for task: artifacts/ghost.json",
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("reads a nested filename listed in task metadata (AC-5)", async () => {
|
|
249
|
+
const root = await makeTempRoot();
|
|
250
|
+
process.env["PO_ROOT"] = root;
|
|
251
|
+
|
|
252
|
+
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
253
|
+
await mkdir(path.join(jobDir, "files", "artifacts", "data"), { recursive: true });
|
|
254
|
+
await writeFile(path.join(jobDir, "files", "artifacts", "data", "foo.json"), '{"nested":true}\n');
|
|
255
|
+
await writeFile(
|
|
256
|
+
path.join(jobDir, "tasks-status.json"),
|
|
257
|
+
JSON.stringify({
|
|
258
|
+
id: "job-1",
|
|
259
|
+
tasks: { research: { files: { artifacts: ["data/foo.json"], logs: [], tmp: [] } } },
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const response = await handleTaskFile(
|
|
264
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=data/foo.json"),
|
|
265
|
+
"job-1",
|
|
266
|
+
"research",
|
|
267
|
+
root,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(response.status).toBe(200);
|
|
271
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
272
|
+
ok: true,
|
|
273
|
+
data: '{"nested":true}\n',
|
|
274
|
+
mime: "application/json",
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("does not double-decode already-decoded query values (AC-5)", async () => {
|
|
279
|
+
const root = await makeTempRoot();
|
|
280
|
+
process.env["PO_ROOT"] = root;
|
|
281
|
+
|
|
282
|
+
const jobDir = path.join(root, "pipeline-data", "current", "job-1");
|
|
283
|
+
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
284
|
+
await writeFile(path.join(root, "secret.json"), "LEAKED");
|
|
285
|
+
await writeFile(
|
|
286
|
+
path.join(jobDir, "tasks-status.json"),
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
id: "job-1",
|
|
289
|
+
tasks: { research: { files: { artifacts: ["real.json"], logs: [], tmp: [] } } },
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const response = await handleTaskFile(
|
|
294
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=%252e%252e"),
|
|
295
|
+
"job-1",
|
|
296
|
+
"research",
|
|
297
|
+
root,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(response.status).toBe(404);
|
|
301
|
+
await expect(response.json()).resolves.toEqual({
|
|
302
|
+
ok: false,
|
|
303
|
+
code: "NOT_FOUND",
|
|
304
|
+
message: "file not found for task: artifacts/%2e%2e",
|
|
305
|
+
});
|
|
306
|
+
expect(await Bun.file(path.join(root, "secret.json")).text()).toBe("LEAKED");
|
|
307
|
+
});
|
|
130
308
|
});
|
|
@@ -168,6 +168,61 @@ describe("handleGateDecision", () => {
|
|
|
168
168
|
expect(events[0]).toMatchObject({ type: "gate_decided", action: "reject", afterTask: "plan", note: "needs another pass" });
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
it("rejecting a control_invalid gate marks the job failed while preserving task done + controlError (AC-10)", async () => {
|
|
172
|
+
const root = await makeTempRoot();
|
|
173
|
+
initPATHS(root);
|
|
174
|
+
const jobId = "reject-control-invalid-job";
|
|
175
|
+
const statusOverride = makeSnapshot({
|
|
176
|
+
gate: {
|
|
177
|
+
afterTask: "plan",
|
|
178
|
+
kind: "control_invalid",
|
|
179
|
+
message: "Control file invalid for plan: bad patch. Repair tasks/plan/control.json and approve to retry.",
|
|
180
|
+
requestedAt: "2026-04-01T10:00:00.000Z",
|
|
181
|
+
},
|
|
182
|
+
tasks: {
|
|
183
|
+
plan: {
|
|
184
|
+
state: "done",
|
|
185
|
+
executionTimeMs: 99,
|
|
186
|
+
refinementAttempts: 1,
|
|
187
|
+
controlApplied: false,
|
|
188
|
+
controlError: { message: "bad patch", violations: ["bad patch"] },
|
|
189
|
+
},
|
|
190
|
+
implement: { state: "pending" },
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const jobDir = await setupJob(root, jobId, statusOverride);
|
|
194
|
+
const spawnSpy = vi.spyOn(Bun, "spawn");
|
|
195
|
+
|
|
196
|
+
const res = await handleGateDecision(
|
|
197
|
+
jobId,
|
|
198
|
+
gateRequest(jobId, { action: "reject", note: "control file is too broken" }),
|
|
199
|
+
root,
|
|
200
|
+
);
|
|
201
|
+
const body = await res.json() as Record<string, unknown>;
|
|
202
|
+
|
|
203
|
+
expect(res.status).toBe(202);
|
|
204
|
+
expect(body).toMatchObject({ ok: true, jobId, action: "reject", spawned: false });
|
|
205
|
+
expect(spawnSpy).not.toHaveBeenCalled();
|
|
206
|
+
|
|
207
|
+
const snapshot = await readJobStatus(jobDir);
|
|
208
|
+
expect(snapshot!.state).toBe("failed");
|
|
209
|
+
expect(snapshot!.gate).toBeNull();
|
|
210
|
+
expect(snapshot!["error"]).toEqual({ name: "GateRejected", message: "control file is too broken" });
|
|
211
|
+
|
|
212
|
+
const task = snapshot!.tasks["plan"] as {
|
|
213
|
+
state?: string;
|
|
214
|
+
executionTimeMs?: number;
|
|
215
|
+
refinementAttempts?: number;
|
|
216
|
+
controlApplied?: boolean;
|
|
217
|
+
controlError?: { message?: string; violations?: string[] };
|
|
218
|
+
};
|
|
219
|
+
expect(task.state).toBe("done");
|
|
220
|
+
expect(task.executionTimeMs).toBe(99);
|
|
221
|
+
expect(task.refinementAttempts).toBe(1);
|
|
222
|
+
expect(task.controlApplied).toBe(false);
|
|
223
|
+
expect(task.controlError).toEqual({ message: "bad patch", violations: ["bad patch"] });
|
|
224
|
+
});
|
|
225
|
+
|
|
171
226
|
it("returns 409 when another gate decision already holds the job slot", async () => {
|
|
172
227
|
const root = await makeTempRoot();
|
|
173
228
|
initPATHS(root);
|
|
@@ -80,6 +80,28 @@ describe("server index", () => {
|
|
|
80
80
|
await handle.close();
|
|
81
81
|
}
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
it("rejects startServer before binding when env config is invalid", async () => {
|
|
85
|
+
const root = await makeTempRoot();
|
|
86
|
+
const priorPoRoot = process.env["PO_ROOT"];
|
|
87
|
+
const priorMaxRunningJobs = process.env["PO_MAX_RUNNING_JOBS"];
|
|
88
|
+
const serveSpy = vi.spyOn(Bun, "serve");
|
|
89
|
+
try {
|
|
90
|
+
process.env["PO_ROOT"] = root;
|
|
91
|
+
process.env["PO_MAX_RUNNING_JOBS"] = "foo";
|
|
92
|
+
resetConfig();
|
|
93
|
+
|
|
94
|
+
await expect(startServer({ dataDir: root, port: 4113 })).rejects.toThrow(/PO_MAX_RUNNING_JOBS/);
|
|
95
|
+
expect(serveSpy).not.toHaveBeenCalled();
|
|
96
|
+
} finally {
|
|
97
|
+
serveSpy.mockRestore();
|
|
98
|
+
if (priorMaxRunningJobs === undefined) delete process.env["PO_MAX_RUNNING_JOBS"];
|
|
99
|
+
else process.env["PO_MAX_RUNNING_JOBS"] = priorMaxRunningJobs;
|
|
100
|
+
if (priorPoRoot === undefined) delete process.env["PO_ROOT"];
|
|
101
|
+
else process.env["PO_ROOT"] = priorPoRoot;
|
|
102
|
+
resetConfig();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
83
105
|
});
|
|
84
106
|
|
|
85
107
|
describe("initializeWatcher concurrency paths", () => {
|
|
@@ -151,3 +173,44 @@ describe("initializeWatcher concurrency paths", () => {
|
|
|
151
173
|
spy.mockRestore();
|
|
152
174
|
});
|
|
153
175
|
});
|
|
176
|
+
|
|
177
|
+
describe("startServer workspace-root resolution (AC-7)", () => {
|
|
178
|
+
it("resolves via explicit dataDir even when PO_ROOT points elsewhere", async () => {
|
|
179
|
+
const explicitRoot = await makeTempRoot();
|
|
180
|
+
const envRoot = await makeTempRoot();
|
|
181
|
+
const priorPoRoot = process.env["PO_ROOT"];
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
process.env["PO_ROOT"] = envRoot;
|
|
185
|
+
await mkdir(path.join(explicitRoot, "pipeline-data", "current", "explicit-job"), { recursive: true });
|
|
186
|
+
await writeFile(
|
|
187
|
+
path.join(explicitRoot, "pipeline-data", "current", "explicit-job", "tasks-status.json"),
|
|
188
|
+
'{"id":"explicit-job","tasks":{}}',
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const handle = await startServer({ dataDir: explicitRoot, port: 4114 });
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(`${handle.url}/api/jobs/explicit-job`);
|
|
194
|
+
expect(response.status).toBe(200);
|
|
195
|
+
} finally {
|
|
196
|
+
await handle.close();
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
if (priorPoRoot === undefined) delete process.env["PO_ROOT"];
|
|
200
|
+
else process.env["PO_ROOT"] = priorPoRoot;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("throws the resolver error when PO_ROOT is unset and dataDir is empty", async () => {
|
|
205
|
+
const priorPoRoot = process.env["PO_ROOT"];
|
|
206
|
+
try {
|
|
207
|
+
delete process.env["PO_ROOT"];
|
|
208
|
+
await expect(
|
|
209
|
+
startServer({ dataDir: "" as string, port: 4115 }),
|
|
210
|
+
).rejects.toThrow(/Workspace root is required/);
|
|
211
|
+
} finally {
|
|
212
|
+
if (priorPoRoot === undefined) delete process.env["PO_ROOT"];
|
|
213
|
+
else process.env["PO_ROOT"] = priorPoRoot;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|