@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, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
7
|
import { resetConfig } from "../../../core/config";
|
|
8
|
-
import { handleJobDetail } from "../endpoints/job-endpoints";
|
|
8
|
+
import { handleJobDetail, handleJobList } from "../endpoints/job-endpoints";
|
|
9
9
|
import { initPATHS, resetPATHS } from "../config-bridge-node";
|
|
10
10
|
|
|
11
11
|
const tempRoots: string[] = [];
|
|
@@ -25,7 +25,12 @@ async function writePipelineRegistry(root: string): Promise<void> {
|
|
|
25
25
|
await mkdir(tasksDir, { recursive: true });
|
|
26
26
|
await writeFile(
|
|
27
27
|
path.join(root, "pipeline-config", "registry.json"),
|
|
28
|
-
JSON.stringify({
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
version: 1,
|
|
30
|
+
pipelines: {
|
|
31
|
+
demo: { name: "Demo", description: "demo pipeline", configDir, tasksDir },
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
29
34
|
);
|
|
30
35
|
await writeFile(
|
|
31
36
|
path.join(configDir, "pipeline.json"),
|
|
@@ -121,4 +126,155 @@ describe("job endpoints pipeline config loading", () => {
|
|
|
121
126
|
expect(body.data.pipelineConfig?.["marker"]).toBe("shared");
|
|
122
127
|
expect(body.data.pipelineConfig?.["tasks"]).toEqual([{ name: "shared-task" }]);
|
|
123
128
|
});
|
|
129
|
+
|
|
130
|
+
it("preserves per-task costs in job detail responses", async () => {
|
|
131
|
+
const root = await makeTempRoot();
|
|
132
|
+
process.env["PO_ROOT"] = root;
|
|
133
|
+
resetConfig();
|
|
134
|
+
initPATHS(root);
|
|
135
|
+
await writePipelineRegistry(root);
|
|
136
|
+
const jobDir = await writeJob(root, "current", "job-costs");
|
|
137
|
+
await writeFile(
|
|
138
|
+
path.join(jobDir, "tasks-status.json"),
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
id: "job-costs",
|
|
141
|
+
name: "job-costs",
|
|
142
|
+
pipeline: "demo",
|
|
143
|
+
createdAt: "2026-06-12T12:00:00.000Z",
|
|
144
|
+
state: "done",
|
|
145
|
+
tasks: {
|
|
146
|
+
build: {
|
|
147
|
+
state: "done",
|
|
148
|
+
tokenUsage: [["openai:gpt-4o", 10, 5, 0.25, "estimated", false, true]],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const res = await handleJobDetail("job-costs");
|
|
155
|
+
const body = await res.json() as {
|
|
156
|
+
data: { costs?: Record<string, Record<string, unknown>> };
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
expect(res.status).toBe(200);
|
|
160
|
+
expect(body.data.costs?.["build"]).toMatchObject({
|
|
161
|
+
source: "estimated",
|
|
162
|
+
totalCost: 0.25,
|
|
163
|
+
costEstimated: true,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("handleJobDetail corrupt status", () => {
|
|
169
|
+
it("returns 409 STATUS_CORRUPT with the corrupt tasks-status.json path", async () => {
|
|
170
|
+
const root = await makeTempRoot();
|
|
171
|
+
process.env["PO_ROOT"] = root;
|
|
172
|
+
resetConfig();
|
|
173
|
+
initPATHS(root);
|
|
174
|
+
await writePipelineRegistry(root);
|
|
175
|
+
const jobDir = await writeJob(root, "current", "job-corrupt");
|
|
176
|
+
await writeFile(path.join(jobDir, "tasks-status.json"), "{ not valid json");
|
|
177
|
+
|
|
178
|
+
const res = await handleJobDetail("job-corrupt");
|
|
179
|
+
const body = (await res.json()) as { code: string; path?: string };
|
|
180
|
+
|
|
181
|
+
expect(res.status).toBe(409);
|
|
182
|
+
expect(body.code).toBe("STATUS_CORRUPT");
|
|
183
|
+
expect(body.path).toBe(path.join(jobDir, "tasks-status.json"));
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("canonical status in HTTP responses", () => {
|
|
188
|
+
it("handleJobDetail returns canonical status:'failed' for a failed-state fixture", async () => {
|
|
189
|
+
const root = await makeTempRoot();
|
|
190
|
+
process.env["PO_ROOT"] = root;
|
|
191
|
+
resetConfig();
|
|
192
|
+
initPATHS(root);
|
|
193
|
+
await writePipelineRegistry(root);
|
|
194
|
+
const jobDir = await writeJob(root, "current", "job-failed");
|
|
195
|
+
await writeFile(
|
|
196
|
+
path.join(jobDir, "tasks-status.json"),
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
id: "job-failed",
|
|
199
|
+
name: "job-failed",
|
|
200
|
+
pipeline: "demo",
|
|
201
|
+
createdAt: "2026-06-12T12:00:00.000Z",
|
|
202
|
+
state: "failed",
|
|
203
|
+
tasks: { a: { state: "failed" } },
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const res = await handleJobDetail("job-failed");
|
|
208
|
+
const body = (await res.json()) as { data: { status: string } };
|
|
209
|
+
|
|
210
|
+
expect(res.status).toBe(200);
|
|
211
|
+
expect(body.data.status).toBe("failed");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("handleJobDetail returns canonical status:'complete' for a complete-state fixture", async () => {
|
|
215
|
+
const root = await makeTempRoot();
|
|
216
|
+
process.env["PO_ROOT"] = root;
|
|
217
|
+
resetConfig();
|
|
218
|
+
initPATHS(root);
|
|
219
|
+
await writePipelineRegistry(root);
|
|
220
|
+
const jobDir = await writeJob(root, "current", "job-complete");
|
|
221
|
+
await writeFile(
|
|
222
|
+
path.join(jobDir, "tasks-status.json"),
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
id: "job-complete",
|
|
225
|
+
name: "job-complete",
|
|
226
|
+
pipeline: "demo",
|
|
227
|
+
createdAt: "2026-06-12T12:00:00.000Z",
|
|
228
|
+
state: "done",
|
|
229
|
+
tasks: { a: { state: "done" } },
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const res = await handleJobDetail("job-complete");
|
|
234
|
+
const body = (await res.json()) as { data: { status: string } };
|
|
235
|
+
|
|
236
|
+
expect(res.status).toBe(200);
|
|
237
|
+
expect(body.data.status).toBe("complete");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("handleJobList returns canonical status for every job in the listing", async () => {
|
|
241
|
+
const root = await makeTempRoot();
|
|
242
|
+
process.env["PO_ROOT"] = root;
|
|
243
|
+
resetConfig();
|
|
244
|
+
initPATHS(root);
|
|
245
|
+
await writePipelineRegistry(root);
|
|
246
|
+
const failedDir = await writeJob(root, "current", "job-list-failed");
|
|
247
|
+
await writeFile(
|
|
248
|
+
path.join(failedDir, "tasks-status.json"),
|
|
249
|
+
JSON.stringify({
|
|
250
|
+
id: "job-list-failed",
|
|
251
|
+
name: "job-list-failed",
|
|
252
|
+
pipeline: "demo",
|
|
253
|
+
createdAt: "2026-06-12T12:00:00.000Z",
|
|
254
|
+
state: "failed",
|
|
255
|
+
tasks: { a: { state: "failed" } },
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
const completeDir = await writeJob(root, "complete", "job-list-complete");
|
|
259
|
+
await writeFile(
|
|
260
|
+
path.join(completeDir, "tasks-status.json"),
|
|
261
|
+
JSON.stringify({
|
|
262
|
+
id: "job-list-complete",
|
|
263
|
+
name: "job-list-complete",
|
|
264
|
+
pipeline: "demo",
|
|
265
|
+
createdAt: "2026-06-12T12:00:00.000Z",
|
|
266
|
+
state: "done",
|
|
267
|
+
tasks: { a: { state: "done" } },
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const res = await handleJobList();
|
|
272
|
+
const body = (await res.json()) as { data: Array<{ jobId: string; status: string }> };
|
|
273
|
+
|
|
274
|
+
expect(res.status).toBe(200);
|
|
275
|
+
expect(body.data).toHaveLength(2);
|
|
276
|
+
const byId = new Map(body.data.map((job) => [job.jobId, job.status]));
|
|
277
|
+
expect(byId.get("job-list-failed")).toBe("failed");
|
|
278
|
+
expect(byId.get("job-list-complete")).toBe("complete");
|
|
279
|
+
});
|
|
124
280
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Source-scan guard (AC-8 / spec §7 Step 6).
|
|
2
|
+
//
|
|
3
|
+
// Asserts that none of the four read/static HTTP boundary files builds its final
|
|
4
|
+
// filesystem read path via a bare `path.join`/`join` from URL-derived params
|
|
5
|
+
// outside the containment helpers `resolveUnderRoot` / `resolveWithin`. The
|
|
6
|
+
// current code is clean; this test exists so a future edit like
|
|
7
|
+
// `const p = path.join(root, slug); await Bun.file(p).text()` fails CI.
|
|
8
|
+
//
|
|
9
|
+
// The scan is span-based, not line-based: each join call's full balanced-paren
|
|
10
|
+
// argument span is evaluated as one unit, so a join split across multiple lines
|
|
11
|
+
// (`path.join(\n root,\n slug,\n)`) is still checked. A join whose call site is
|
|
12
|
+
// lexically enclosed by a `resolveUnderRoot(...)`/`resolveWithin(...)` argument
|
|
13
|
+
// span is allowed — the param is being contained.
|
|
14
|
+
//
|
|
15
|
+
// Known lexical-guard limitation: a URL param first copied to a local variable
|
|
16
|
+
// and then joined (`const s = slug; path.join(root, s)`) is not detected — that
|
|
17
|
+
// needs dataflow, which is out of scope for a source scan.
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
const BOUNDARY_FILES = [
|
|
24
|
+
"../endpoints/schema-file-endpoint.ts",
|
|
25
|
+
"../endpoints/file-endpoints.ts",
|
|
26
|
+
"../endpoints/task-analysis-endpoint.ts",
|
|
27
|
+
"../router.ts",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Identifiers treated as URL-derived when they appear in a join argument span.
|
|
31
|
+
// Documented exclusions: `type`/`params`/`jobId`/`location` are intentionally
|
|
32
|
+
// omitted (see subspec 6-spec.md) — those inputs are out of this guard's scope
|
|
33
|
+
// because they are not part of the encoded-path containment boundary.
|
|
34
|
+
const URL_DERIVED = /\b(slug|filename|taskId|taskName|pathname|searchParams)\b/;
|
|
35
|
+
// Matches `path.join(`, `path.posix.join(`, `path.win32.join(`, or a bare `join(`
|
|
36
|
+
// not preceded by a word char, `$`, or `.` (the `.` exclusion skips array `.join(`).
|
|
37
|
+
const JOIN_CALL = /(?:path(?:\.posix|\.win32)?\.join|(?<![\w$.])join)\s*\(/g;
|
|
38
|
+
// Containment helpers whose argument span makes a join nested inside them safe.
|
|
39
|
+
const CONTAINMENT_CALL = /(?:resolveUnderRoot|resolveWithin)\s*\(/g;
|
|
40
|
+
|
|
41
|
+
type Span = { open: number; close: number };
|
|
42
|
+
type Violation = { file: string; line: number; snippet: string };
|
|
43
|
+
|
|
44
|
+
// Given the index of an opening `(`, return the index of its matching `)`.
|
|
45
|
+
function matchingParen(src: string, openIndex: number): number {
|
|
46
|
+
let depth = 0;
|
|
47
|
+
for (let i = openIndex; i < src.length; i++) {
|
|
48
|
+
if (src[i] === "(") depth++;
|
|
49
|
+
else if (src[i] === ")") {
|
|
50
|
+
depth--;
|
|
51
|
+
if (depth === 0) return i;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return src.length - 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// All call spans for a regex that matches up to and including the `(`.
|
|
58
|
+
function callSpans(src: string, callRe: RegExp): Span[] {
|
|
59
|
+
const spans: Span[] = [];
|
|
60
|
+
callRe.lastIndex = 0;
|
|
61
|
+
for (let m = callRe.exec(src); m !== null; m = callRe.exec(src)) {
|
|
62
|
+
const open = m.index + m[0].length - 1; // index of the `(`
|
|
63
|
+
spans.push({ open, close: matchingParen(src, open) });
|
|
64
|
+
}
|
|
65
|
+
return spans;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function scan(filePath: string): Promise<Violation[]> {
|
|
69
|
+
const src = await Bun.file(filePath).text();
|
|
70
|
+
const containment = callSpans(src, CONTAINMENT_CALL);
|
|
71
|
+
const violations: Violation[] = [];
|
|
72
|
+
for (const join of callSpans(src, JOIN_CALL)) {
|
|
73
|
+
const args = src.slice(join.open + 1, join.close);
|
|
74
|
+
if (!URL_DERIVED.test(args) && !args.includes("${")) continue;
|
|
75
|
+
// Allowed when the join call sits inside a containment call's argument span.
|
|
76
|
+
const contained = containment.some((c) => c.open < join.open && join.close <= c.close);
|
|
77
|
+
if (contained) continue;
|
|
78
|
+
const line = src.slice(0, join.open).split("\n").length;
|
|
79
|
+
violations.push({ file: path.basename(filePath), line, snippet: args.replace(/\s+/g, " ").trim() });
|
|
80
|
+
}
|
|
81
|
+
return violations;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("read/static path-building source guard (AC-8)", () => {
|
|
85
|
+
it("no boundary file builds a final read path via bare path.join(<root>, <url-param>)", async () => {
|
|
86
|
+
const base = import.meta.dirname;
|
|
87
|
+
const all: Violation[] = [];
|
|
88
|
+
for (const rel of BOUNDARY_FILES) {
|
|
89
|
+
const full = path.resolve(base, rel);
|
|
90
|
+
all.push(...(await scan(full)));
|
|
91
|
+
}
|
|
92
|
+
expect(all, all.map((v) => `${v.file}:${v.line} → ${v.snippet}`).join("\n")).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import { resetPATHS } from "../config-bridge-node";
|
|
8
|
+
import { createRouter } from "../router";
|
|
9
|
+
|
|
10
|
+
const ORIGINAL_PO_ROOT = process.env["PO_ROOT"];
|
|
11
|
+
|
|
12
|
+
const tempRoots: string[] = [];
|
|
13
|
+
|
|
14
|
+
async function makeTempRoot(prefix: string): Promise<string> {
|
|
15
|
+
const root = await Bun.$`mktemp -d ${path.join(os.tmpdir(), `${prefix}-XXXXXX`)}`.text();
|
|
16
|
+
const trimmed = root.trim();
|
|
17
|
+
tempRoots.push(trimmed);
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
resetPATHS();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
if (ORIGINAL_PO_ROOT === undefined) {
|
|
27
|
+
delete process.env["PO_ROOT"];
|
|
28
|
+
} else {
|
|
29
|
+
process.env["PO_ROOT"] = ORIGINAL_PO_ROOT;
|
|
30
|
+
}
|
|
31
|
+
resetPATHS();
|
|
32
|
+
await Promise.all(tempRoots.splice(0).map((root) => Bun.$`rm -rf ${root}`));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("router root isolation (AC-5)", () => {
|
|
36
|
+
it("handleTaskFile reads from the threaded root, ignoring PO_ROOT=/B", async () => {
|
|
37
|
+
const rootA = await makeTempRoot("router-iso-A");
|
|
38
|
+
const rootB = await makeTempRoot("router-iso-B");
|
|
39
|
+
process.env["PO_ROOT"] = rootB;
|
|
40
|
+
|
|
41
|
+
const jobDir = path.join(rootA, "pipeline-data", "current", "job-1");
|
|
42
|
+
await mkdir(path.join(jobDir, "files", "artifacts"), { recursive: true });
|
|
43
|
+
await writeFile(path.join(jobDir, "files", "artifacts", "out.json"), '{"source":"A"}\n');
|
|
44
|
+
await writeFile(
|
|
45
|
+
path.join(jobDir, "tasks-status.json"),
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
id: "job-1",
|
|
48
|
+
tasks: {
|
|
49
|
+
research: {
|
|
50
|
+
files: { artifacts: ["out.json"], logs: [], tmp: [] },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const router = createRouter({ dataDir: rootA });
|
|
57
|
+
const response = await router.handle(
|
|
58
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/file?type=artifacts&filename=out.json", {
|
|
59
|
+
headers: { Host: "localhost" },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(response.status).toBe(200);
|
|
64
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
65
|
+
ok: true,
|
|
66
|
+
data: '{"source":"A"}\n',
|
|
67
|
+
mime: "application/json",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("handleTaskFileList reads from the threaded root, ignoring PO_ROOT=/B", async () => {
|
|
72
|
+
const rootA = await makeTempRoot("router-iso-A");
|
|
73
|
+
const rootB = await makeTempRoot("router-iso-B");
|
|
74
|
+
process.env["PO_ROOT"] = rootB;
|
|
75
|
+
|
|
76
|
+
const jobDir = path.join(rootA, "pipeline-data", "current", "job-1");
|
|
77
|
+
await mkdir(jobDir, { recursive: true });
|
|
78
|
+
await writeFile(
|
|
79
|
+
path.join(jobDir, "tasks-status.json"),
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
id: "job-1",
|
|
82
|
+
tasks: {
|
|
83
|
+
research: {
|
|
84
|
+
files: { artifacts: ["listed.json"], logs: [], tmp: [] },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const router = createRouter({ dataDir: rootA });
|
|
91
|
+
const response = await router.handle(
|
|
92
|
+
new Request("http://localhost/api/jobs/job-1/tasks/research/files?type=artifacts", {
|
|
93
|
+
headers: { Host: "localhost" },
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(response.status).toBe(200);
|
|
98
|
+
await expect(response.json()).resolves.toEqual({ ok: true, data: ["listed.json"] });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("handleSchemaFile reads from the threaded root, ignoring PO_ROOT=/B", async () => {
|
|
102
|
+
const rootA = await makeTempRoot("router-iso-A");
|
|
103
|
+
const rootB = await makeTempRoot("router-iso-B");
|
|
104
|
+
process.env["PO_ROOT"] = rootB;
|
|
105
|
+
|
|
106
|
+
await mkdir(path.join(rootA, "pipeline-config", "slug-1", "schemas"), { recursive: true });
|
|
107
|
+
await writeFile(path.join(rootA, "pipeline-config", "slug-1", "schemas", "task.schema.json"), '{"$schema":"http://example"}\n');
|
|
108
|
+
|
|
109
|
+
const router = createRouter({ dataDir: rootA });
|
|
110
|
+
const response = await router.handle(
|
|
111
|
+
new Request("http://localhost/api/pipelines/slug-1/schemas/task.schema.json", {
|
|
112
|
+
headers: { Host: "localhost" },
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(response.status).toBe(200);
|
|
117
|
+
await expect(response.json()).resolves.toEqual({ ok: true, data: '{"$schema":"http://example"}\n' });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("handleTaskAnalysis reads from the threaded root, ignoring PO_ROOT=/B", async () => {
|
|
121
|
+
const rootA = await makeTempRoot("router-iso-A");
|
|
122
|
+
const rootB = await makeTempRoot("router-iso-B");
|
|
123
|
+
process.env["PO_ROOT"] = rootB;
|
|
124
|
+
|
|
125
|
+
await mkdir(path.join(rootA, "pipeline-config", "slug-1", "tasks"), { recursive: true });
|
|
126
|
+
await writeFile(
|
|
127
|
+
path.join(rootA, "pipeline-config", "slug-1", "tasks", "research.analysis.json"),
|
|
128
|
+
JSON.stringify({ taskId: "research", steps: 3 }),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const router = createRouter({ dataDir: rootA });
|
|
132
|
+
const response = await router.handle(
|
|
133
|
+
new Request("http://localhost/api/pipelines/slug-1/tasks/research/analysis", {
|
|
134
|
+
headers: { Host: "localhost" },
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(response.status).toBe(200);
|
|
139
|
+
await expect(response.json()).resolves.toEqual({ ok: true, data: { taskId: "research", steps: 3 } });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("handleCreatePipeline writes under the threaded root, ignoring PO_ROOT=/B", async () => {
|
|
143
|
+
const rootA = await makeTempRoot("router-iso-A");
|
|
144
|
+
const rootB = await makeTempRoot("router-iso-B");
|
|
145
|
+
process.env["PO_ROOT"] = rootB;
|
|
146
|
+
|
|
147
|
+
const router = createRouter({ dataDir: rootA });
|
|
148
|
+
const response = await router.handle(
|
|
149
|
+
new Request("http://localhost/api/pipelines", {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "Content-Type": "application/json", Host: "localhost" },
|
|
152
|
+
body: JSON.stringify({ name: "Isolation Test Pipeline", description: "x" }),
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(response.status).toBe(201);
|
|
157
|
+
const body = (await response.json()) as { ok: boolean; data: { slug: string; name: string } };
|
|
158
|
+
expect(body.ok).toBe(true);
|
|
159
|
+
const slug = body.data.slug;
|
|
160
|
+
expect(slug.length).toBeGreaterThan(0);
|
|
161
|
+
|
|
162
|
+
const pipelinePath = path.join(rootA, "pipeline-config", slug, "pipeline.json");
|
|
163
|
+
const pipelineText = await Bun.file(pipelinePath).text();
|
|
164
|
+
const pipeline = JSON.parse(pipelineText) as { name: string; slug: string };
|
|
165
|
+
expect(pipeline).toMatchObject({ name: "Isolation Test Pipeline", slug });
|
|
166
|
+
|
|
167
|
+
const registryPath = path.join(rootA, "pipeline-config", "registry.json");
|
|
168
|
+
const registry = JSON.parse(await Bun.file(registryPath).text()) as {
|
|
169
|
+
pipelines: Record<string, { name: string; description: string; configDir: string; tasksDir: string }>;
|
|
170
|
+
};
|
|
171
|
+
expect(registry.pipelines[slug]?.configDir).toBe(path.join("pipeline-config", slug));
|
|
172
|
+
expect(registry.pipelines[slug]?.tasksDir).toBe(path.join("pipeline-config", slug, "tasks"));
|
|
173
|
+
expect(registry.pipelines[slug]?.name).toBe("Isolation Test Pipeline");
|
|
174
|
+
expect(registry.pipelines[slug]?.description).toBe("x");
|
|
175
|
+
|
|
176
|
+
const bRootExists = await Bun.file(path.join(rootB, "pipeline-config", "registry.json")).exists();
|
|
177
|
+
expect(bRootExists).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("handleTaskSave writes under the threaded root, ignoring PO_ROOT=/B", async () => {
|
|
181
|
+
const rootA = await makeTempRoot("router-iso-A");
|
|
182
|
+
const rootB = await makeTempRoot("router-iso-B");
|
|
183
|
+
process.env["PO_ROOT"] = rootB;
|
|
184
|
+
|
|
185
|
+
const router = createRouter({ dataDir: rootA });
|
|
186
|
+
const response = await router.handle(
|
|
187
|
+
new Request("http://localhost/api/tasks/create", {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { "Content-Type": "application/json", Host: "localhost" },
|
|
190
|
+
body: JSON.stringify({ slug: "saved-slug", taskId: "research", content: "# research notes\n" }),
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(response.status).toBe(201);
|
|
195
|
+
await expect(response.json()).resolves.toEqual({ ok: true, data: { slug: "saved-slug", taskId: "research" } });
|
|
196
|
+
|
|
197
|
+
const savedPath = path.join(rootA, "pipeline-config", "saved-slug", "tasks", "research.md");
|
|
198
|
+
const savedText = await Bun.file(savedPath).text();
|
|
199
|
+
expect(savedText).toBe("# research notes\n");
|
|
200
|
+
|
|
201
|
+
const bRootExists = await Bun.file(path.join(rootB, "pipeline-config", "saved-slug", "tasks", "research.md")).exists();
|
|
202
|
+
expect(bRootExists).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ─── Capture-args table: populated by mocked handlers so each test can assert
|
|
4
|
+
// the last argument the router passed.
|
|
5
|
+
const capturedArgs: Record<string, unknown[] | undefined> = {};
|
|
6
|
+
|
|
7
|
+
mock.module("../endpoints/file-endpoints", () => ({
|
|
8
|
+
handleTaskFile: mock((...args: unknown[]) => {
|
|
9
|
+
capturedArgs.handleTaskFile = args;
|
|
10
|
+
return Promise.resolve(new Response("{}", { headers: { "Content-Type": "application/json" } }));
|
|
11
|
+
}),
|
|
12
|
+
handleTaskFileList: mock((...args: unknown[]) => {
|
|
13
|
+
capturedArgs.handleTaskFileList = args;
|
|
14
|
+
return Promise.resolve(new Response("{}", { headers: { "Content-Type": "application/json" } }));
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module("../endpoints/schema-file-endpoint", () => ({
|
|
19
|
+
handleSchemaFile: mock((...args: unknown[]) => {
|
|
20
|
+
capturedArgs.handleSchemaFile = args;
|
|
21
|
+
return Promise.resolve(new Response("{}", { headers: { "Content-Type": "application/json" } }));
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module("../endpoints/task-analysis-endpoint", () => ({
|
|
26
|
+
handleTaskAnalysis: mock((...args: unknown[]) => {
|
|
27
|
+
capturedArgs.handleTaskAnalysis = args;
|
|
28
|
+
return Promise.resolve(new Response("{}", { headers: { "Content-Type": "application/json" } }));
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
mock.module("../endpoints/pipeline-analysis-endpoint", () => ({
|
|
33
|
+
handlePipelineAnalysis: mock((...args: unknown[]) => {
|
|
34
|
+
capturedArgs.handlePipelineAnalysis = args;
|
|
35
|
+
return Promise.resolve(new Response("{}", { headers: { "Content-Type": "application/json" } }));
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module("../endpoints/create-pipeline-endpoint", () => ({
|
|
40
|
+
handleCreatePipeline: mock((...args: unknown[]) => {
|
|
41
|
+
capturedArgs.handleCreatePipeline = args;
|
|
42
|
+
return Promise.resolve(new Response("{}", { headers: { "Content-Type": "application/json" } }));
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
mock.module("../endpoints/task-save-endpoint", () => ({
|
|
47
|
+
handleTaskSave: mock((...args: unknown[]) => {
|
|
48
|
+
capturedArgs.handleTaskSave = args;
|
|
49
|
+
return Promise.resolve(new Response("{}", { headers: { "Content-Type": "application/json" } }));
|
|
50
|
+
}),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// ─── Import the module under test (after mock.module calls) ───────────────────
|
|
54
|
+
import { createRouter } from "../router";
|
|
55
|
+
|
|
56
|
+
// ─── Env capture/restore per ~/.agents/rules/unit-testing.md §6 ───────────────
|
|
57
|
+
const ORIGINAL_PO_ROOT = process.env["PO_ROOT"];
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
process.env["PO_ROOT"] = "/B";
|
|
61
|
+
for (const key of Object.keys(capturedArgs)) delete capturedArgs[key];
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
if (ORIGINAL_PO_ROOT === undefined) {
|
|
66
|
+
delete process.env["PO_ROOT"];
|
|
67
|
+
} else {
|
|
68
|
+
process.env["PO_ROOT"] = ORIGINAL_PO_ROOT;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("router threads resolved root to six handlers (AC-5)", () => {
|
|
73
|
+
it("passes options.dataDir as the last argument to handleTaskFile", async () => {
|
|
74
|
+
const router = createRouter({ dataDir: "/A" });
|
|
75
|
+
await router.handle(
|
|
76
|
+
new Request("http://localhost/api/jobs/job-1/tasks/task-1/file?type=artifacts&filename=out.txt", {
|
|
77
|
+
headers: { Host: "localhost" },
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
expect(capturedArgs.handleTaskFile?.at(-1)).toBe("/A");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("passes options.dataDir as the last argument to handleTaskFileList", async () => {
|
|
84
|
+
const router = createRouter({ dataDir: "/A" });
|
|
85
|
+
await router.handle(
|
|
86
|
+
new Request("http://localhost/api/jobs/job-1/tasks/task-1/files?type=artifacts", {
|
|
87
|
+
headers: { Host: "localhost" },
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
expect(capturedArgs.handleTaskFileList?.at(-1)).toBe("/A");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("passes options.dataDir as the last argument to handleSchemaFile", async () => {
|
|
94
|
+
const router = createRouter({ dataDir: "/A" });
|
|
95
|
+
await router.handle(
|
|
96
|
+
new Request("http://localhost/api/pipelines/slug-1/schemas/task.schema.json", {
|
|
97
|
+
headers: { Host: "localhost" },
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
expect(capturedArgs.handleSchemaFile?.at(-1)).toBe("/A");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("passes options.dataDir as the last argument to handleTaskAnalysis", async () => {
|
|
104
|
+
const router = createRouter({ dataDir: "/A" });
|
|
105
|
+
await router.handle(
|
|
106
|
+
new Request("http://localhost/api/pipelines/slug-1/tasks/task-1/analysis", {
|
|
107
|
+
headers: { Host: "localhost" },
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
expect(capturedArgs.handleTaskAnalysis?.at(-1)).toBe("/A");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("passes options.dataDir as the last argument to handlePipelineAnalysis", async () => {
|
|
114
|
+
const router = createRouter({ dataDir: "/A" });
|
|
115
|
+
await router.handle(
|
|
116
|
+
new Request("http://localhost/api/pipelines/slug-1/analyze", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json", Host: "localhost" },
|
|
119
|
+
body: "{}",
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
expect(capturedArgs.handlePipelineAnalysis?.at(-1)).toBe("/A");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("passes options.dataDir as the last argument to handleCreatePipeline", async () => {
|
|
126
|
+
const router = createRouter({ dataDir: "/A" });
|
|
127
|
+
await router.handle(
|
|
128
|
+
new Request("http://localhost/api/pipelines", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json", Host: "localhost" },
|
|
131
|
+
body: "{}",
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
expect(capturedArgs.handleCreatePipeline?.at(-1)).toBe("/A");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("passes options.dataDir as the last argument to handleTaskSave", async () => {
|
|
138
|
+
const router = createRouter({ dataDir: "/A" });
|
|
139
|
+
await router.handle(
|
|
140
|
+
new Request("http://localhost/api/tasks/create", {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "Content-Type": "application/json", Host: "localhost" },
|
|
143
|
+
body: "{}",
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
expect(capturedArgs.handleTaskSave?.at(-1)).toBe("/A");
|
|
147
|
+
});
|
|
148
|
+
});
|