@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
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deriveJobStatusFromTasks,
|
|
3
|
-
normalizeJobStatus,
|
|
4
|
-
normalizeTaskState,
|
|
5
|
-
} from "../../../config/statuses";
|
|
1
|
+
import { normalizeJobView } from "../../../core/job-view";
|
|
6
2
|
import type {
|
|
7
3
|
CanonicalJob,
|
|
8
|
-
CanonicalJobStatus,
|
|
9
4
|
CanonicalTask,
|
|
10
5
|
ComputedStatus,
|
|
11
|
-
GateInfo,
|
|
12
6
|
JobReadResult,
|
|
13
7
|
TransformationStats,
|
|
14
8
|
} from "../types";
|
|
@@ -18,141 +12,13 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
18
12
|
return value as Record<string, unknown>;
|
|
19
13
|
}
|
|
20
14
|
|
|
21
|
-
function asTaskFiles(value: unknown): CanonicalTask["files"] {
|
|
22
|
-
const record = asRecord(value);
|
|
23
|
-
return {
|
|
24
|
-
artifacts: Array.isArray(record?.["artifacts"]) ? (record["artifacts"] as string[]) : [],
|
|
25
|
-
logs: Array.isArray(record?.["logs"]) ? (record["logs"] as string[]) : [],
|
|
26
|
-
tmp: Array.isArray(record?.["tmp"]) ? (record["tmp"] as string[]) : [],
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function toGateInfo(value: unknown): GateInfo | null | undefined {
|
|
31
|
-
if (value === null) return null;
|
|
32
|
-
const record = asRecord(value);
|
|
33
|
-
if (!record) return undefined;
|
|
34
|
-
|
|
35
|
-
const artifacts = Array.isArray(record["artifacts"])
|
|
36
|
-
? record["artifacts"].filter((artifact): artifact is string => typeof artifact === "string")
|
|
37
|
-
: undefined;
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
afterTask: typeof record["afterTask"] === "string" ? record["afterTask"] : "",
|
|
41
|
-
message: typeof record["message"] === "string" ? record["message"] : "",
|
|
42
|
-
requestedAt: typeof record["requestedAt"] === "string" ? record["requestedAt"] : "",
|
|
43
|
-
...(artifacts && artifacts.length > 0 ? { artifacts } : {}),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function toTask(name: string, value: unknown): CanonicalTask {
|
|
48
|
-
const task = asRecord(value) ?? {};
|
|
49
|
-
return {
|
|
50
|
-
name,
|
|
51
|
-
state: normalizeTaskState(task["state"]),
|
|
52
|
-
files: asTaskFiles(task["files"]),
|
|
53
|
-
startedAt: typeof task["startedAt"] === "string" ? task["startedAt"] : null,
|
|
54
|
-
endedAt: typeof task["endedAt"] === "string" ? task["endedAt"] : null,
|
|
55
|
-
attempts: typeof task["attempts"] === "number" ? task["attempts"] : undefined,
|
|
56
|
-
restartCount: typeof task["restartCount"] === "number" ? task["restartCount"] : undefined,
|
|
57
|
-
retrying: typeof task["retrying"] === "boolean" ? task["retrying"] : undefined,
|
|
58
|
-
nextRetryAt: typeof task["nextRetryAt"] === "string" ? task["nextRetryAt"] : undefined,
|
|
59
|
-
lastRetryError: Object.hasOwn(task, "lastRetryError") ? task["lastRetryError"] as import("../types").RetryError | null : undefined,
|
|
60
|
-
executionTimeMs:
|
|
61
|
-
typeof task["executionTimeMs"] === "number" ? task["executionTimeMs"] : undefined,
|
|
62
|
-
refinementAttempts:
|
|
63
|
-
typeof task["refinementAttempts"] === "number"
|
|
64
|
-
? task["refinementAttempts"]
|
|
65
|
-
: undefined,
|
|
66
|
-
stageLogPath: typeof task["stageLogPath"] === "string" ? task["stageLogPath"] : undefined,
|
|
67
|
-
errorContext: task["errorContext"],
|
|
68
|
-
currentStage: typeof task["currentStage"] === "string" ? task["currentStage"] : undefined,
|
|
69
|
-
failedStage: typeof task["failedStage"] === "string" ? task["failedStage"] : undefined,
|
|
70
|
-
artifacts: task["artifacts"],
|
|
71
|
-
error: asRecord(task["error"]) as CanonicalTask["error"],
|
|
72
|
-
skipReason: typeof task["skipReason"] === "string" ? task["skipReason"] : undefined,
|
|
73
|
-
skippedBy: typeof task["skippedBy"] === "string" ? task["skippedBy"] : undefined,
|
|
74
|
-
controlApplied: typeof task["controlApplied"] === "boolean" ? task["controlApplied"] : undefined,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getStatusValue(status: ReturnType<typeof deriveJobStatusFromTasks>): CanonicalJobStatus {
|
|
79
|
-
return status === "failed" ? "error" : status;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getProgress(tasks: CanonicalTask[]): number {
|
|
83
|
-
if (tasks.length === 0) return 0;
|
|
84
|
-
|
|
85
|
-
const completed = tasks.filter((task) => task.state === "done" || task.state === "skipped").length;
|
|
86
|
-
return Math.floor((completed / tasks.length) * 100);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getTitle(raw: Record<string, unknown>, jobId: string): string {
|
|
90
|
-
const title = raw["title"] ?? raw["name"];
|
|
91
|
-
return typeof title === "string" && title.trim() !== "" ? title : jobId;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getCosts(raw: Record<string, unknown>): Record<string, unknown> {
|
|
95
|
-
const explicit = asRecord(raw["costs"]);
|
|
96
|
-
if (explicit) return explicit;
|
|
97
|
-
|
|
98
|
-
// Aggregate cost and token totals from individual task entries
|
|
99
|
-
const tasks = asRecord(raw["tasks"]);
|
|
100
|
-
if (!tasks) return {};
|
|
101
|
-
|
|
102
|
-
let totalCost = 0;
|
|
103
|
-
let totalInputTokens = 0;
|
|
104
|
-
let totalOutputTokens = 0;
|
|
105
|
-
|
|
106
|
-
for (const value of Object.values(tasks)) {
|
|
107
|
-
const task = asRecord(value);
|
|
108
|
-
if (!task) continue;
|
|
109
|
-
|
|
110
|
-
// Sum from [model, input, output, cost?] tuples
|
|
111
|
-
const usage = task["tokenUsage"];
|
|
112
|
-
if (Array.isArray(usage)) {
|
|
113
|
-
for (const entry of usage) {
|
|
114
|
-
if (Array.isArray(entry) && entry.length >= 3) {
|
|
115
|
-
if (typeof entry[1] === "number") totalInputTokens += entry[1];
|
|
116
|
-
if (typeof entry[2] === "number") totalOutputTokens += entry[2];
|
|
117
|
-
if (typeof entry[3] === "number") totalCost += entry[3];
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
124
|
-
if (totalCost === 0 && totalTokens === 0) return {};
|
|
125
|
-
|
|
126
|
-
return { totalCost, totalTokens, totalInputTokens, totalOutputTokens };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
15
|
export function computeJobStatus(tasksInput: unknown): ComputedStatus {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
return { status: "pending", progress: 0 };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
status: getStatusValue(deriveJobStatusFromTasks(tasks)),
|
|
137
|
-
progress: getProgress(tasks),
|
|
138
|
-
};
|
|
16
|
+
const view = normalizeJobView({ tasks: tasksInput }, "", "");
|
|
17
|
+
return { status: view.status, progress: view.progress };
|
|
139
18
|
}
|
|
140
19
|
|
|
141
20
|
export function transformTasks(rawTasks: unknown): Record<string, CanonicalTask> {
|
|
142
|
-
|
|
143
|
-
return Object.fromEntries(
|
|
144
|
-
rawTasks.map((task, index) => {
|
|
145
|
-
const record = asRecord(task);
|
|
146
|
-
const name = typeof record?.["name"] === "string" ? (record["name"] as string) : `task-${index + 1}`;
|
|
147
|
-
return [name, toTask(name, task)];
|
|
148
|
-
}),
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const record = asRecord(rawTasks);
|
|
153
|
-
if (!record) return {};
|
|
154
|
-
|
|
155
|
-
return Object.fromEntries(Object.entries(record).map(([name, task]) => [name, toTask(name, task)]));
|
|
21
|
+
return normalizeJobView({ tasks: rawTasks }, "", "").tasks;
|
|
156
22
|
}
|
|
157
23
|
|
|
158
24
|
export function transformJobStatus(raw: unknown, jobId: string, location: string): CanonicalJob | null {
|
|
@@ -164,42 +30,41 @@ export function transformJobStatus(raw: unknown, jobId: string, location: string
|
|
|
164
30
|
console.warn(`job id mismatch: expected "${jobId}" but received "${rawJobId}"`);
|
|
165
31
|
}
|
|
166
32
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const snapshotStatus = normalizeJobStatus(record["status"] ?? record["state"]);
|
|
170
|
-
const gate = toGateInfo(record["gate"]);
|
|
171
|
-
const title = getTitle(record, jobId);
|
|
33
|
+
return normalizeJobView(raw, jobId, location);
|
|
34
|
+
}
|
|
172
35
|
|
|
36
|
+
export function buildUnreadableJob(result: JobReadResult): CanonicalJob {
|
|
37
|
+
const code = result.code ?? "UNKNOWN_READ_ERROR";
|
|
38
|
+
const detail = result.message ? ` Reader error: ${result.message}` : "";
|
|
39
|
+
const message =
|
|
40
|
+
`Status file is unreadable (${code}). ` +
|
|
41
|
+
`Repair or delete ${result.path ?? "tasks-status.json"} for job "${result.jobId}" to recover.` +
|
|
42
|
+
detail;
|
|
173
43
|
return {
|
|
174
|
-
id: jobId,
|
|
175
|
-
jobId,
|
|
176
|
-
name:
|
|
177
|
-
title,
|
|
178
|
-
status:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
pipeline: typeof record["pipeline"] === "string" ? record["pipeline"] : undefined,
|
|
189
|
-
pipelineLabel:
|
|
190
|
-
typeof record["pipelineLabel"] === "string" ? record["pipelineLabel"] : undefined,
|
|
191
|
-
pipelineConfig: asRecord(record["pipelineConfig"]) ?? undefined,
|
|
192
|
-
current: record["current"],
|
|
193
|
-
currentStage: record["currentStage"],
|
|
194
|
-
gate,
|
|
195
|
-
warnings: Array.isArray(record["warnings"]) ? (record["warnings"] as string[]) : undefined,
|
|
44
|
+
id: result.jobId,
|
|
45
|
+
jobId: result.jobId,
|
|
46
|
+
name: result.jobId,
|
|
47
|
+
title: result.jobId,
|
|
48
|
+
status: "failed",
|
|
49
|
+
progress: 0,
|
|
50
|
+
createdAt: null,
|
|
51
|
+
updatedAt: null,
|
|
52
|
+
location: result.location ?? null,
|
|
53
|
+
tasks: {},
|
|
54
|
+
files: {},
|
|
55
|
+
costs: {},
|
|
56
|
+
readable: false,
|
|
57
|
+
error: { code, message, path: result.path },
|
|
196
58
|
};
|
|
197
59
|
}
|
|
198
60
|
|
|
199
61
|
export function transformMultipleJobs(jobReadResults: JobReadResult[]): CanonicalJob[] {
|
|
200
62
|
return jobReadResults
|
|
201
|
-
.
|
|
202
|
-
|
|
63
|
+
.map((result) =>
|
|
64
|
+
result.ok
|
|
65
|
+
? transformJobStatus(result.data, result.jobId, result.location)
|
|
66
|
+
: result.code === "INVALID_JSON" ? buildUnreadableJob(result) : null,
|
|
67
|
+
)
|
|
203
68
|
.filter((job): job is CanonicalJob => job !== null);
|
|
204
69
|
}
|
|
205
70
|
|
|
@@ -208,6 +73,7 @@ export function getTransformationStats(
|
|
|
208
73
|
transformedJobs: CanonicalJob[],
|
|
209
74
|
): TransformationStats {
|
|
210
75
|
const successfulReads = readResults.filter((result) => result.ok).length;
|
|
76
|
+
const successfulTransforms = transformedJobs.filter((job) => job.readable !== false).length;
|
|
211
77
|
const statusDistribution = transformedJobs.reduce<Record<string, number>>((acc, job) => {
|
|
212
78
|
acc[job.status] = (acc[job.status] ?? 0) + 1;
|
|
213
79
|
return acc;
|
|
@@ -216,9 +82,9 @@ export function getTransformationStats(
|
|
|
216
82
|
return {
|
|
217
83
|
totalRead: readResults.length,
|
|
218
84
|
successfulReads,
|
|
219
|
-
successfulTransforms
|
|
220
|
-
failedTransforms: successfulReads -
|
|
221
|
-
transformationRate: successfulReads === 0 ? 0 :
|
|
85
|
+
successfulTransforms,
|
|
86
|
+
failedTransforms: successfulReads - successfulTransforms,
|
|
87
|
+
transformationRate: successfulReads === 0 ? 0 : successfulTransforms / successfulReads,
|
|
222
88
|
statusDistribution,
|
|
223
89
|
};
|
|
224
90
|
}
|
package/src/ui/state/types.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { JobStatusValue, TaskStateValue } from "../../config/statuses";
|
|
2
|
+
import type { JobView, JobViewTask } from "../../core/job-view";
|
|
2
3
|
|
|
3
4
|
export type ChangeType = "created" | "modified" | "deleted";
|
|
4
|
-
export type CanonicalJobStatus = JobStatusValue
|
|
5
|
+
export type CanonicalJobStatus = JobStatusValue;
|
|
5
6
|
|
|
6
7
|
export interface ChangeEntry {
|
|
7
8
|
path: string;
|
|
@@ -56,7 +57,7 @@ export interface ComposeSnapshotOptions {
|
|
|
56
57
|
|
|
57
58
|
export interface SnapshotDeps {
|
|
58
59
|
listAllJobs?: () => { current: string[]; complete: string[] } | Promise<{ current: string[]; complete: string[] }>;
|
|
59
|
-
readJob?: (id: string
|
|
60
|
+
readJob?: (id: string) => Promise<JobReadResult>;
|
|
60
61
|
transformMultipleJobs?: (results: JobReadResult[]) => CanonicalJob[];
|
|
61
62
|
now?: () => Date;
|
|
62
63
|
paths?: Record<string, string>;
|
|
@@ -126,53 +127,12 @@ export interface GateInfo {
|
|
|
126
127
|
message: string;
|
|
127
128
|
artifacts?: string[];
|
|
128
129
|
requestedAt: string;
|
|
130
|
+
kind?: "approval" | "control_invalid";
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
export
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
files: { artifacts: string[]; logs: string[]; tmp: string[] };
|
|
135
|
-
startedAt?: string | null;
|
|
136
|
-
endedAt?: string | null;
|
|
137
|
-
attempts?: number;
|
|
138
|
-
restartCount?: number;
|
|
139
|
-
retrying?: boolean;
|
|
140
|
-
nextRetryAt?: string;
|
|
141
|
-
lastRetryError?: RetryError | null;
|
|
142
|
-
executionTimeMs?: number;
|
|
143
|
-
refinementAttempts?: number;
|
|
144
|
-
stageLogPath?: string;
|
|
145
|
-
errorContext?: unknown;
|
|
146
|
-
currentStage?: string;
|
|
147
|
-
failedStage?: string;
|
|
148
|
-
artifacts?: unknown;
|
|
149
|
-
error?: { message: string; [key: string]: unknown } | null;
|
|
150
|
-
skipReason?: string;
|
|
151
|
-
skippedBy?: string;
|
|
152
|
-
controlApplied?: boolean;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface CanonicalJob {
|
|
156
|
-
id: string;
|
|
157
|
-
jobId: string;
|
|
158
|
-
name: string;
|
|
159
|
-
title: string;
|
|
160
|
-
status: CanonicalJobStatus;
|
|
161
|
-
progress: number;
|
|
162
|
-
createdAt: string | null;
|
|
163
|
-
updatedAt: string | null;
|
|
164
|
-
location: string | null;
|
|
165
|
-
tasks: Record<string, CanonicalTask>;
|
|
166
|
-
files: Record<string, unknown>;
|
|
167
|
-
costs: Record<string, unknown>;
|
|
168
|
-
pipeline?: string;
|
|
169
|
-
pipelineLabel?: string;
|
|
170
|
-
pipelineConfig?: Record<string, unknown>;
|
|
171
|
-
current?: unknown;
|
|
172
|
-
currentStage?: unknown;
|
|
173
|
-
gate?: GateInfo | null;
|
|
174
|
-
warnings?: string[];
|
|
175
|
-
}
|
|
133
|
+
export type CanonicalTask = JobViewTask;
|
|
134
|
+
|
|
135
|
+
export type CanonicalJob = JobView;
|
|
176
136
|
|
|
177
137
|
export interface JobReadResult {
|
|
178
138
|
ok: boolean;
|
|
@@ -181,6 +141,7 @@ export interface JobReadResult {
|
|
|
181
141
|
location: string;
|
|
182
142
|
code?: string;
|
|
183
143
|
message?: string;
|
|
144
|
+
path?: string;
|
|
184
145
|
}
|
|
185
146
|
|
|
186
147
|
export interface TransformationStats {
|
|
@@ -202,7 +163,7 @@ export interface JobListStats {
|
|
|
202
163
|
export interface GroupedJobs {
|
|
203
164
|
running: CanonicalJob[];
|
|
204
165
|
waiting: CanonicalJob[];
|
|
205
|
-
|
|
166
|
+
failed: CanonicalJob[];
|
|
206
167
|
pending: CanonicalJob[];
|
|
207
168
|
complete: CanonicalJob[];
|
|
208
169
|
}
|
|
@@ -214,6 +175,9 @@ export interface CostsSummary {
|
|
|
214
175
|
totalCost: number;
|
|
215
176
|
totalInputCost: number;
|
|
216
177
|
totalOutputCost: number;
|
|
178
|
+
estimatedCost: number;
|
|
179
|
+
hasEstimated: boolean;
|
|
180
|
+
unavailableCost: number;
|
|
217
181
|
}
|
|
218
182
|
|
|
219
183
|
export interface APIJob {
|
|
@@ -230,10 +194,13 @@ export interface APIJob {
|
|
|
230
194
|
currentStage?: unknown;
|
|
231
195
|
gate?: GateInfo | null;
|
|
232
196
|
costsSummary: CostsSummary;
|
|
197
|
+
costs?: Record<string, unknown>;
|
|
233
198
|
pipelineSlug?: string;
|
|
234
199
|
pipeline?: string;
|
|
235
200
|
pipelineLabel?: string;
|
|
236
201
|
pipelineConfig?: Record<string, unknown>;
|
|
202
|
+
readable?: boolean;
|
|
203
|
+
error?: { code: string; message: string; path?: string };
|
|
237
204
|
}
|
|
238
205
|
|
|
239
206
|
export interface AggregationStats {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { resolveUnderRoot, resolveWithin } from "../path-containment";
|
|
5
|
+
|
|
6
|
+
describe("resolveWithin", () => {
|
|
7
|
+
const rootDir = "/app/staging";
|
|
8
|
+
|
|
9
|
+
it("rejects relative path with parent dir escape", () => {
|
|
10
|
+
expect(resolveWithin(rootDir, "../x")).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("rejects deeply nested parent dir escape", () => {
|
|
14
|
+
expect(resolveWithin(rootDir, "../../../../tmp/PWNED.txt")).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("rejects normalized parent dir escape", () => {
|
|
18
|
+
expect(resolveWithin(rootDir, "a/../../b")).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects absolute path", () => {
|
|
22
|
+
expect(resolveWithin(rootDir, "/etc/passwd")).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects empty string", () => {
|
|
26
|
+
expect(resolveWithin(rootDir, "")).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rejects non-string input", () => {
|
|
30
|
+
expect(resolveWithin(rootDir, 42)).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("rejects string containing NUL byte", () => {
|
|
34
|
+
expect(resolveWithin(rootDir, "safe\0/../escape")).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects current-directory root target", () => {
|
|
38
|
+
expect(resolveWithin(rootDir, ".")).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects nested path that normalizes to root", () => {
|
|
42
|
+
expect(resolveWithin(rootDir, "dir/..")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("resolves a plain filename within root", () => {
|
|
46
|
+
const result = resolveWithin(rootDir, "out.json");
|
|
47
|
+
expect(result).toBe(path.resolve(rootDir, "out.json"));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("resolves a nested relative path within root", () => {
|
|
51
|
+
const result = resolveWithin(rootDir, "data/foo.json");
|
|
52
|
+
expect(result).toBe(path.resolve(rootDir, "data/foo.json"));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("resolveUnderRoot", () => {
|
|
57
|
+
const rootDir = "/app/staging";
|
|
58
|
+
|
|
59
|
+
it("rejects parent dir segment", () => {
|
|
60
|
+
expect(resolveUnderRoot(rootDir, "..")).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects encoded slash escape that decodes to traversal", () => {
|
|
64
|
+
expect(resolveUnderRoot(rootDir, "..%2f..%2fetc")).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rejects decoded null byte", () => {
|
|
68
|
+
expect(resolveUnderRoot(rootDir, "foo%00bar")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects malformed percent-encoding", () => {
|
|
72
|
+
expect(resolveUnderRoot(rootDir, "%zz")).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rejects truncated percent-encoding", () => {
|
|
76
|
+
expect(resolveUnderRoot(rootDir, "%2")).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects non-string segment", () => {
|
|
80
|
+
expect(resolveUnderRoot(rootDir, "a", 42 as unknown as string)).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("resolves a plain encoded filename", () => {
|
|
84
|
+
expect(resolveUnderRoot(rootDir, "out.json")).toBe(path.resolve(rootDir, "out.json"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("resolves multiple segments joined under root", () => {
|
|
88
|
+
expect(resolveUnderRoot(rootDir, "data", "foo.json")).toBe(
|
|
89
|
+
path.resolve(rootDir, "data/foo.json"),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("decodes a legitimately encoded space once (AC-9)", () => {
|
|
94
|
+
expect(resolveUnderRoot(rootDir, "my%20file.json")).toBe(
|
|
95
|
+
path.resolve(rootDir, "my file.json"),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("rejects encoded dot-dot (AC-3)", () => {
|
|
100
|
+
expect(resolveUnderRoot(rootDir, "%2e%2e")).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("rejects mixed-case encoded dot-dot (AC-3)", () => {
|
|
104
|
+
expect(resolveUnderRoot(rootDir, "%2E%2E")).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("rejects encoded traversal nested under a name (AC-3)", () => {
|
|
108
|
+
expect(resolveUnderRoot(rootDir, "a%2f..%2f..%2fetc")).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("contains a doubly-encoded dot-dot that decodes to a literal (AC-3)", () => {
|
|
112
|
+
expect(resolveUnderRoot(rootDir, "%252e%252e")).toBe(path.resolve(rootDir, "%2e%2e"));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("URL parsing characterization (AC-2)", () => {
|
|
117
|
+
it("normalizes an encoded dot-dot path segment before capture", () => {
|
|
118
|
+
expect(new URL("https://h/a/%2e%2e/b").pathname).toBe("/b");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("drops a standalone encoded dot-dot segment", () => {
|
|
122
|
+
expect(new URL("https://h/%2e%2e").pathname).toBe("/");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("normalizes a mixed-case encoded dot-dot segment", () => {
|
|
126
|
+
expect(new URL("https://h/%2E%2E").pathname).toBe("/");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps an encoded slash encoded within a segment", () => {
|
|
130
|
+
expect(new URL("https://h/x%2f").pathname).toBe("/x%2f");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("keeps a doubly-encoded dot encoded", () => {
|
|
134
|
+
expect(new URL("https://h/x%252e").pathname).toBe("/x%252e");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("keeps an encoded space encoded within a segment", () => {
|
|
138
|
+
expect(new URL("https://h/x%20").pathname).toBe("/x%20");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("URLPattern captures keep an encoded slash encoded", () => {
|
|
142
|
+
const result = new URLPattern({ pathname: "/:seg" }).exec(new URL("https://h/x%2f"));
|
|
143
|
+
expect(result?.pathname?.groups?.seg).toBe("x%2f");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("URLPattern captures keep a doubly-encoded dot encoded", () => {
|
|
147
|
+
const result = new URLPattern({ pathname: "/:seg" }).exec(new URL("https://h/x%252e"));
|
|
148
|
+
expect(result?.pathname?.groups?.seg).toBe("x%252e");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("URLPattern captures keep an encoded space encoded", () => {
|
|
152
|
+
const result = new URLPattern({ pathname: "/:seg" }).exec(new URL("https://h/x%20"));
|
|
153
|
+
expect(result?.pathname?.groups?.seg).toBe("x%20");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("URLSearchParams.get() decodes query values exactly once", () => {
|
|
157
|
+
const a = new URL("https://h/x?a=%20b%2520c").searchParams.get("a");
|
|
158
|
+
expect(a).toBe(" b%20c");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -16,3 +16,24 @@ export function resolveWithin(rootDir: string, relativePath: unknown): string |
|
|
|
16
16
|
if (!resolved.startsWith(root + path.sep)) return null;
|
|
17
17
|
return resolved;
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decode each encoded URL path segment/pathname exactly once, then delegate to
|
|
22
|
+
* `resolveWithin` for lexical containment. For URLPattern path captures and
|
|
23
|
+
* static pathnames only — do NOT pass `URLSearchParams.get()` values here
|
|
24
|
+
* (they are already decoded once and would double-decode `%25`).
|
|
25
|
+
*/
|
|
26
|
+
export function resolveUnderRoot(rootDir: string, ...segments: string[]): string | null {
|
|
27
|
+
const decoded: string[] = [];
|
|
28
|
+
for (const seg of segments) {
|
|
29
|
+
if (typeof seg !== "string") return null;
|
|
30
|
+
let d: string;
|
|
31
|
+
try {
|
|
32
|
+
d = decodeURIComponent(seg);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
decoded.push(d);
|
|
37
|
+
}
|
|
38
|
+
return resolveWithin(rootDir, decoded.join("/"));
|
|
39
|
+
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { writeAnalysisFile } from "../enrichers/analysis-writer.ts";
|
|
7
|
-
import type { TaskAnalysis } from "../types.ts";
|
|
8
|
-
|
|
9
|
-
const VALID_ANALYSIS: TaskAnalysis = {
|
|
10
|
-
taskFilePath: "/pipeline/tasks/research.js",
|
|
11
|
-
stages: [{ name: "fetchData", order: 0, isAsync: true }],
|
|
12
|
-
artifacts: {
|
|
13
|
-
reads: [{ fileName: "input.json", stage: "fetchData", required: true }],
|
|
14
|
-
writes: [{ fileName: "output.json", stage: "fetchData" }],
|
|
15
|
-
unresolvedReads: [],
|
|
16
|
-
unresolvedWrites: [],
|
|
17
|
-
},
|
|
18
|
-
models: [{ provider: "openai", method: "chat", stage: "fetchData" }],
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
let tmpDir: string;
|
|
22
|
-
|
|
23
|
-
beforeAll(async () => {
|
|
24
|
-
tmpDir = await mkdtemp(path.join(os.tmpdir(), "analysis-writer-test-"));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterAll(async () => {
|
|
28
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("writeAnalysisFile", () => {
|
|
32
|
-
it("writes analysis file with analyzedAt and all fields for valid input", async () => {
|
|
33
|
-
const pipelinePath = path.join(tmpDir, "pipeline-1");
|
|
34
|
-
const before = new Date();
|
|
35
|
-
|
|
36
|
-
await writeAnalysisFile(pipelinePath, "research", VALID_ANALYSIS);
|
|
37
|
-
|
|
38
|
-
const after = new Date();
|
|
39
|
-
const filePath = path.join(pipelinePath, "analysis", "research.analysis.json");
|
|
40
|
-
expect(existsSync(filePath)).toBe(true);
|
|
41
|
-
|
|
42
|
-
const content = JSON.parse(await Bun.file(filePath).text()) as Record<string, unknown>;
|
|
43
|
-
|
|
44
|
-
expect(typeof content.analyzedAt).toBe("string");
|
|
45
|
-
const analyzedAt = new Date(content.analyzedAt as string);
|
|
46
|
-
expect(analyzedAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
47
|
-
expect(analyzedAt.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
48
|
-
|
|
49
|
-
expect(content.taskFilePath).toBe(VALID_ANALYSIS.taskFilePath);
|
|
50
|
-
expect(content.stages).toEqual(VALID_ANALYSIS.stages);
|
|
51
|
-
expect(content.artifacts).toEqual(VALID_ANALYSIS.artifacts);
|
|
52
|
-
expect(content.models).toEqual(VALID_ANALYSIS.models);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("throws before creating any file when taskFilePath is null", async () => {
|
|
56
|
-
const pipelinePath = path.join(tmpDir, "pipeline-2");
|
|
57
|
-
const bad = { ...VALID_ANALYSIS, taskFilePath: null } as unknown as TaskAnalysis;
|
|
58
|
-
|
|
59
|
-
await expect(
|
|
60
|
-
writeAnalysisFile(pipelinePath, "research", bad),
|
|
61
|
-
).rejects.toThrow(/taskFilePath/);
|
|
62
|
-
|
|
63
|
-
expect(existsSync(path.join(pipelinePath, "analysis"))).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("throws when stages is not an array", async () => {
|
|
67
|
-
const pipelinePath = path.join(tmpDir, "pipeline-3");
|
|
68
|
-
const bad = { ...VALID_ANALYSIS, stages: "not-an-array" } as unknown as TaskAnalysis;
|
|
69
|
-
|
|
70
|
-
await expect(
|
|
71
|
-
writeAnalysisFile(pipelinePath, "research", bad),
|
|
72
|
-
).rejects.toThrow(/stages/);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("throws when artifacts is missing reads", async () => {
|
|
76
|
-
const pipelinePath = path.join(tmpDir, "pipeline-4");
|
|
77
|
-
const bad = {
|
|
78
|
-
...VALID_ANALYSIS,
|
|
79
|
-
artifacts: { writes: [], unresolvedReads: [], unresolvedWrites: [] },
|
|
80
|
-
} as unknown as TaskAnalysis;
|
|
81
|
-
|
|
82
|
-
await expect(
|
|
83
|
-
writeAnalysisFile(pipelinePath, "research", bad),
|
|
84
|
-
).rejects.toThrow(/artifacts/);
|
|
85
|
-
});
|
|
86
|
-
});
|