@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,9 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
+
import type { JobView } from "../../../core/job-view";
|
|
3
4
|
import type {
|
|
4
5
|
APIJob,
|
|
5
6
|
AcquireResult,
|
|
6
7
|
CanonicalJob,
|
|
8
|
+
CanonicalJobStatus,
|
|
7
9
|
ChangeTrackerState,
|
|
8
10
|
ComposeSnapshotOptions,
|
|
9
11
|
FilterOptions,
|
|
@@ -44,7 +46,7 @@ describe("ui/state types", () => {
|
|
|
44
46
|
jobId: "job-1",
|
|
45
47
|
name: "Job 1",
|
|
46
48
|
title: "Job 1",
|
|
47
|
-
status: "
|
|
49
|
+
status: "failed",
|
|
48
50
|
progress: 0,
|
|
49
51
|
createdAt: null,
|
|
50
52
|
updatedAt: null,
|
|
@@ -56,15 +58,15 @@ describe("ui/state types", () => {
|
|
|
56
58
|
const grouped: GroupedJobs = {
|
|
57
59
|
running: [],
|
|
58
60
|
waiting: [],
|
|
59
|
-
|
|
60
|
-
pending: [
|
|
61
|
+
failed: [],
|
|
62
|
+
pending: [],
|
|
61
63
|
complete: [],
|
|
62
64
|
};
|
|
63
65
|
const filter: FilterOptions = { status: "pending", location: "current" };
|
|
64
66
|
const apiJob: APIJob = {
|
|
65
67
|
jobId: "job-1",
|
|
66
68
|
title: "Job 1",
|
|
67
|
-
status: "
|
|
69
|
+
status: "failed",
|
|
68
70
|
progress: 0,
|
|
69
71
|
createdAt: null,
|
|
70
72
|
updatedAt: null,
|
|
@@ -77,6 +79,9 @@ describe("ui/state types", () => {
|
|
|
77
79
|
totalCost: 0,
|
|
78
80
|
totalInputCost: 0,
|
|
79
81
|
totalOutputCost: 0,
|
|
82
|
+
estimatedCost: 0,
|
|
83
|
+
hasEstimated: false,
|
|
84
|
+
unavailableCost: 0,
|
|
80
85
|
},
|
|
81
86
|
};
|
|
82
87
|
const stats: TransformationStats = {
|
|
@@ -96,9 +101,103 @@ describe("ui/state types", () => {
|
|
|
96
101
|
expect(snapshotDeps.readJob).toBeUndefined();
|
|
97
102
|
expect(schema.fileName).toBe("seed.json");
|
|
98
103
|
expect(result.response).toBeInstanceOf(Response);
|
|
99
|
-
expect(
|
|
104
|
+
expect(job.status).toBe("failed");
|
|
105
|
+
expect(grouped.failed).toEqual([]);
|
|
100
106
|
expect(filter.location).toBe("current");
|
|
101
107
|
expect(apiJob.costsSummary.totalCost).toBe(0);
|
|
108
|
+
expect(apiJob.status).toBe("failed");
|
|
102
109
|
expect(stats.transformationRate).toBe(0);
|
|
103
110
|
});
|
|
111
|
+
|
|
112
|
+
it("rejects status:\"error\" as a CanonicalJobStatus at the type level", () => {
|
|
113
|
+
// @ts-expect-error — "error" is not assignable to CanonicalJobStatus
|
|
114
|
+
const rejected: CanonicalJobStatus = "error";
|
|
115
|
+
expect(rejected).toBe("error");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("rejects status:\"error\" on a CanonicalJob literal", () => {
|
|
119
|
+
const job: CanonicalJob = {
|
|
120
|
+
id: "job-1",
|
|
121
|
+
jobId: "job-1",
|
|
122
|
+
name: "Job 1",
|
|
123
|
+
title: "Job 1",
|
|
124
|
+
// @ts-expect-error — "error" is not assignable to CanonicalJobStatus
|
|
125
|
+
status: "error",
|
|
126
|
+
progress: 0,
|
|
127
|
+
createdAt: null,
|
|
128
|
+
updatedAt: null,
|
|
129
|
+
location: "current",
|
|
130
|
+
tasks: {},
|
|
131
|
+
files: {},
|
|
132
|
+
costs: {},
|
|
133
|
+
};
|
|
134
|
+
expect(job.jobId).toBe("job-1");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("rejects status:\"error\" on an APIJob literal", () => {
|
|
138
|
+
const apiJob: APIJob = {
|
|
139
|
+
jobId: "job-1",
|
|
140
|
+
title: "Job 1",
|
|
141
|
+
// @ts-expect-error — "error" is not assignable to CanonicalJobStatus
|
|
142
|
+
status: "error",
|
|
143
|
+
progress: 0,
|
|
144
|
+
createdAt: null,
|
|
145
|
+
updatedAt: null,
|
|
146
|
+
location: "current",
|
|
147
|
+
tasks: {},
|
|
148
|
+
costsSummary: {
|
|
149
|
+
totalTokens: 0,
|
|
150
|
+
totalInputTokens: 0,
|
|
151
|
+
totalOutputTokens: 0,
|
|
152
|
+
totalCost: 0,
|
|
153
|
+
totalInputCost: 0,
|
|
154
|
+
totalOutputCost: 0,
|
|
155
|
+
estimatedCost: 0,
|
|
156
|
+
hasEstimated: false,
|
|
157
|
+
unavailableCost: 0,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
expect(apiJob.jobId).toBe("job-1");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("preserves readable:false and error: { code, message } on a CanonicalJob", () => {
|
|
164
|
+
const job: CanonicalJob = {
|
|
165
|
+
id: "job-1",
|
|
166
|
+
jobId: "job-1",
|
|
167
|
+
name: "Job 1",
|
|
168
|
+
title: "Job 1",
|
|
169
|
+
status: "failed",
|
|
170
|
+
progress: 0,
|
|
171
|
+
createdAt: null,
|
|
172
|
+
updatedAt: null,
|
|
173
|
+
location: "current",
|
|
174
|
+
tasks: {},
|
|
175
|
+
files: {},
|
|
176
|
+
costs: {},
|
|
177
|
+
readable: false,
|
|
178
|
+
error: { code: "E_FOO", message: "broken", path: "/tmp/job-1" },
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
expect(job.readable).toBe(false);
|
|
182
|
+
expect(job.error).toEqual({ code: "E_FOO", message: "broken", path: "/tmp/job-1" });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("CanonicalJob is structurally identical to JobView", () => {
|
|
186
|
+
const job: CanonicalJob = {
|
|
187
|
+
id: "job-1",
|
|
188
|
+
jobId: "job-1",
|
|
189
|
+
name: "Job 1",
|
|
190
|
+
title: "Job 1",
|
|
191
|
+
status: "failed",
|
|
192
|
+
progress: 0,
|
|
193
|
+
createdAt: null,
|
|
194
|
+
updatedAt: null,
|
|
195
|
+
location: "current",
|
|
196
|
+
tasks: {},
|
|
197
|
+
files: {},
|
|
198
|
+
costs: {},
|
|
199
|
+
};
|
|
200
|
+
const view: JobView = job;
|
|
201
|
+
expect(view.id).toBe("job-1");
|
|
202
|
+
});
|
|
104
203
|
});
|
package/src/ui/state/snapshot.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
StateSnapshot,
|
|
11
11
|
} from "./types";
|
|
12
12
|
|
|
13
|
-
const SNAPSHOT_STATUS_ORDER = ["
|
|
13
|
+
const SNAPSHOT_STATUS_ORDER = ["failed", "running", "complete", "pending"] as const;
|
|
14
14
|
|
|
15
15
|
function nowIso(now: (() => Date) | undefined): string {
|
|
16
16
|
return (now ?? (() => new Date()))().toISOString();
|
|
@@ -137,7 +137,7 @@ export async function buildSnapshotFromFilesystem(deps: SnapshotDeps = {}): Prom
|
|
|
137
137
|
const readResults = await Promise.all(
|
|
138
138
|
readRequests.map(async ({ jobId, location }) => {
|
|
139
139
|
try {
|
|
140
|
-
return await resolved.readJob(jobId
|
|
140
|
+
return await resolved.readJob(jobId);
|
|
141
141
|
} catch (error) {
|
|
142
142
|
console.warn(`failed to read job "${jobId}" from "${location}"`, error);
|
|
143
143
|
return { ok: false, jobId, location, message: String(error) } satisfies JobReadResult;
|
|
@@ -34,6 +34,7 @@ describe("list-transformer", () => {
|
|
|
34
34
|
it("returns status priorities and sorts valid jobs only", () => {
|
|
35
35
|
expect(getStatusPriority("running")).toBe(4);
|
|
36
36
|
expect(getStatusPriority("waiting")).toBe(4);
|
|
37
|
+
expect(getStatusPriority("failed")).toBe(3);
|
|
37
38
|
expect(getStatusPriority("unknown")).toBe(0);
|
|
38
39
|
|
|
39
40
|
const jobs = sortJobs([
|
|
@@ -57,17 +58,22 @@ describe("list-transformer", () => {
|
|
|
57
58
|
const jobs = [
|
|
58
59
|
makeJob({ id: "job-1", title: "Alpha", status: "running", progress: 50 }),
|
|
59
60
|
makeJob({ id: "job-2", jobId: "job-2", title: "Beta", status: "complete", progress: 100, location: "complete" }),
|
|
61
|
+
makeJob({ id: "job-3", jobId: "job-3", title: "Gamma", status: "failed", progress: 25 }),
|
|
60
62
|
];
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
const grouped = groupJobsByStatus(jobs);
|
|
65
|
+
expect(grouped).toMatchObject({
|
|
63
66
|
running: [jobs[0]],
|
|
64
67
|
complete: [jobs[1]],
|
|
68
|
+
failed: [jobs[2]],
|
|
65
69
|
waiting: [],
|
|
66
70
|
});
|
|
67
|
-
expect(
|
|
71
|
+
expect(grouped).not.toHaveProperty("error");
|
|
72
|
+
expect(getJobListStats(jobs).averageProgress).toBe(58);
|
|
68
73
|
expect(filterJobs(jobs, "alp")).toEqual([jobs[0]]);
|
|
69
74
|
expect(filterJobs(jobs, "JOB-2")).toEqual([jobs[1]]);
|
|
70
75
|
expect(filterJobs(jobs, "", { status: "complete", location: "complete" })).toEqual([jobs[1]]);
|
|
76
|
+
expect(filterJobs(jobs, "", { status: "failed" })).toEqual([jobs[2]]);
|
|
71
77
|
});
|
|
72
78
|
|
|
73
79
|
it("always includes zeroed costs in API output and computes aggregation stats", () => {
|
|
@@ -80,6 +86,9 @@ describe("list-transformer", () => {
|
|
|
80
86
|
totalCost: 0,
|
|
81
87
|
totalInputCost: 0,
|
|
82
88
|
totalOutputCost: 0,
|
|
89
|
+
estimatedCost: 0,
|
|
90
|
+
hasEstimated: false,
|
|
91
|
+
unavailableCost: 0,
|
|
83
92
|
});
|
|
84
93
|
expect(getAggregationStats(jobs, jobs, jobs)).toEqual({
|
|
85
94
|
totalInput: 2,
|
|
@@ -91,6 +100,45 @@ describe("list-transformer", () => {
|
|
|
91
100
|
});
|
|
92
101
|
});
|
|
93
102
|
|
|
103
|
+
it("preserves estimated cost fields in API output", () => {
|
|
104
|
+
const job = makeJob({
|
|
105
|
+
costs: {
|
|
106
|
+
totalTokens: 10,
|
|
107
|
+
totalInputTokens: 6,
|
|
108
|
+
totalOutputTokens: 4,
|
|
109
|
+
totalCost: 1,
|
|
110
|
+
totalInputCost: 0.6,
|
|
111
|
+
totalOutputCost: 0.4,
|
|
112
|
+
estimatedCost: 2,
|
|
113
|
+
unavailableCost: 3,
|
|
114
|
+
hasEstimated: true,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(transformJobListForAPI([job])[0]?.costsSummary).toEqual({
|
|
119
|
+
totalTokens: 10,
|
|
120
|
+
totalInputTokens: 6,
|
|
121
|
+
totalOutputTokens: 4,
|
|
122
|
+
totalCost: 1,
|
|
123
|
+
totalInputCost: 0.6,
|
|
124
|
+
totalOutputCost: 0.4,
|
|
125
|
+
estimatedCost: 2,
|
|
126
|
+
unavailableCost: 3,
|
|
127
|
+
hasEstimated: true,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("defaults missing estimated cost fields to 0/false", () => {
|
|
132
|
+
const job = makeJob({ costs: { totalCost: 5 } });
|
|
133
|
+
|
|
134
|
+
expect(transformJobListForAPI([job])[0]?.costsSummary).toMatchObject({
|
|
135
|
+
totalCost: 5,
|
|
136
|
+
estimatedCost: 0,
|
|
137
|
+
hasEstimated: false,
|
|
138
|
+
unavailableCost: 0,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
94
142
|
it("passes gate metadata through API list output", () => {
|
|
95
143
|
const gate = {
|
|
96
144
|
afterTask: "review",
|
|
@@ -103,4 +151,39 @@ describe("list-transformer", () => {
|
|
|
103
151
|
gate,
|
|
104
152
|
});
|
|
105
153
|
});
|
|
154
|
+
|
|
155
|
+
it("keeps an unreadable placeholder with null createdAt through aggregate and transform", () => {
|
|
156
|
+
const error = {
|
|
157
|
+
code: "INVALID_JSON",
|
|
158
|
+
message: "Repair or delete /path/to/tasks-status.json for job \"job-x\" to recover.",
|
|
159
|
+
path: "/path/to/tasks-status.json",
|
|
160
|
+
};
|
|
161
|
+
const placeholder = makeJob({
|
|
162
|
+
id: "job-x",
|
|
163
|
+
jobId: "job-x",
|
|
164
|
+
status: "failed",
|
|
165
|
+
createdAt: null,
|
|
166
|
+
readable: false,
|
|
167
|
+
error,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const aggregated = aggregateAndSortJobs([placeholder], []);
|
|
171
|
+
expect(aggregated).toHaveLength(1);
|
|
172
|
+
expect(aggregated[0]?.id).toBe("job-x");
|
|
173
|
+
|
|
174
|
+
const apiJob = transformJobListForAPI(aggregated)[0];
|
|
175
|
+
expect(apiJob?.jobId).toBe("job-x");
|
|
176
|
+
expect(apiJob?.readable).toBe(false);
|
|
177
|
+
expect(apiJob?.error).toEqual(error);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("does not add readable or error to a normal job in API output", () => {
|
|
181
|
+
const normal = makeJob();
|
|
182
|
+
|
|
183
|
+
expect(aggregateAndSortJobs([normal], [])).toHaveLength(1);
|
|
184
|
+
|
|
185
|
+
const apiJob = transformJobListForAPI([normal])[0];
|
|
186
|
+
expect(apiJob).not.toHaveProperty("readable");
|
|
187
|
+
expect(apiJob).not.toHaveProperty("error");
|
|
188
|
+
});
|
|
106
189
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
import { normalizeJobView } from "../../../../core/job-view";
|
|
3
4
|
import {
|
|
5
|
+
buildUnreadableJob,
|
|
4
6
|
computeJobStatus,
|
|
5
7
|
getTransformationStats,
|
|
6
8
|
transformJobStatus,
|
|
@@ -48,11 +50,11 @@ describe("status-transformer", () => {
|
|
|
48
50
|
expect(job?.id).toBe("job-1");
|
|
49
51
|
expect(job?.jobId).toBe("job-1");
|
|
50
52
|
expect(job?.name).toBe("Title");
|
|
51
|
-
expect(warn).
|
|
53
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("job id mismatch"));
|
|
52
54
|
warn.mockRestore();
|
|
53
55
|
});
|
|
54
56
|
|
|
55
|
-
it("
|
|
57
|
+
it("computeJobStatus returns running when half the tasks are done", () => {
|
|
56
58
|
const result = computeJobStatus({
|
|
57
59
|
a: { state: "done" },
|
|
58
60
|
b: { state: "done" },
|
|
@@ -63,7 +65,7 @@ describe("status-transformer", () => {
|
|
|
63
65
|
expect(result).toEqual({ status: "running", progress: 50 });
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
it("
|
|
68
|
+
it("computeJobStatus returns pending when some tasks are pending and none running", () => {
|
|
67
69
|
const result = computeJobStatus({
|
|
68
70
|
a: { state: "done" },
|
|
69
71
|
b: { state: "skipped" },
|
|
@@ -77,8 +79,8 @@ describe("status-transformer", () => {
|
|
|
77
79
|
expect(computeJobStatus([])).toEqual({ status: "pending", progress: 0 });
|
|
78
80
|
});
|
|
79
81
|
|
|
80
|
-
it("transformJobStatus derives progress from tasks
|
|
81
|
-
const
|
|
82
|
+
it("transformJobStatus derives progress from tasks via deriveProgress (ignores persisted progress)", () => {
|
|
83
|
+
const withProgress = transformJobStatus(
|
|
82
84
|
{
|
|
83
85
|
title: "Test",
|
|
84
86
|
progress: 100,
|
|
@@ -91,8 +93,44 @@ describe("status-transformer", () => {
|
|
|
91
93
|
"current",
|
|
92
94
|
);
|
|
93
95
|
|
|
94
|
-
expect(
|
|
95
|
-
expect(
|
|
96
|
+
expect(withProgress?.progress).toBe(50);
|
|
97
|
+
expect(withProgress?.status).toBe("running");
|
|
98
|
+
|
|
99
|
+
const withoutProgress = transformJobStatus(
|
|
100
|
+
{
|
|
101
|
+
title: "Test",
|
|
102
|
+
tasks: {
|
|
103
|
+
a: { state: "done" },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
"job-2",
|
|
107
|
+
"current",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(withoutProgress?.progress).toBe(100);
|
|
111
|
+
expect(withoutProgress?.status).toBe("complete");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("transformJobStatus aggregates task tokenUsage costs through the canonical view", () => {
|
|
115
|
+
const job = transformJobStatus(
|
|
116
|
+
{
|
|
117
|
+
title: "Costed",
|
|
118
|
+
state: "done",
|
|
119
|
+
tasks: {
|
|
120
|
+
a: { state: "done", tokenUsage: [["openai:gpt-4o", 12, 6, 0.25]] },
|
|
121
|
+
b: { state: "done", tokenUsage: [["anthropic:claude", 3, 9, 0.5]] },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
"job-cost",
|
|
125
|
+
"complete",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(job?.costs).toMatchObject({
|
|
129
|
+
totalCost: 0.75,
|
|
130
|
+
totalTokens: 30,
|
|
131
|
+
totalInputTokens: 15,
|
|
132
|
+
totalOutputTokens: 15,
|
|
133
|
+
});
|
|
96
134
|
});
|
|
97
135
|
|
|
98
136
|
it("surfaces waiting snapshots and gate metadata", () => {
|
|
@@ -138,7 +176,7 @@ describe("status-transformer", () => {
|
|
|
138
176
|
"current",
|
|
139
177
|
);
|
|
140
178
|
|
|
141
|
-
expect(job?.status).toBe("
|
|
179
|
+
expect(job?.status).toBe("failed");
|
|
142
180
|
});
|
|
143
181
|
|
|
144
182
|
it("maps numeric restartCount on tasks", () => {
|
|
@@ -183,23 +221,15 @@ describe("status-transformer", () => {
|
|
|
183
221
|
expect(tasks.alpha?.lastRetryError).toBeNull();
|
|
184
222
|
});
|
|
185
223
|
|
|
186
|
-
it("
|
|
187
|
-
const
|
|
224
|
+
it("computes transformation stats for valid reads", () => {
|
|
225
|
+
const readResults = [
|
|
188
226
|
{ ok: true, data: { tasks: { a: { state: "done" } } }, jobId: "job-1", location: "current" },
|
|
189
|
-
|
|
190
|
-
|
|
227
|
+
];
|
|
228
|
+
const transformed = transformMultipleJobs(readResults);
|
|
191
229
|
|
|
192
230
|
expect(transformed).toHaveLength(1);
|
|
193
|
-
expect(
|
|
194
|
-
|
|
195
|
-
[
|
|
196
|
-
{ ok: true, data: { tasks: { a: { state: "done" } } }, jobId: "job-1", location: "current" },
|
|
197
|
-
{ ok: false, jobId: "job-2", location: "current" },
|
|
198
|
-
],
|
|
199
|
-
transformed,
|
|
200
|
-
),
|
|
201
|
-
).toEqual({
|
|
202
|
-
totalRead: 2,
|
|
231
|
+
expect(getTransformationStats(readResults, transformed)).toEqual({
|
|
232
|
+
totalRead: 1,
|
|
203
233
|
successfulReads: 1,
|
|
204
234
|
successfulTransforms: 1,
|
|
205
235
|
failedTransforms: 0,
|
|
@@ -207,4 +237,202 @@ describe("status-transformer", () => {
|
|
|
207
237
|
statusDistribution: { complete: 1 },
|
|
208
238
|
});
|
|
209
239
|
});
|
|
240
|
+
|
|
241
|
+
it("computes transformation stats without counting unreadable placeholders as successful transforms", () => {
|
|
242
|
+
const readResults = [
|
|
243
|
+
{ ok: true, data: { tasks: { a: { state: "done" } } }, jobId: "job-1", location: "current" },
|
|
244
|
+
{ ok: false, code: "INVALID_JSON", jobId: "job-2", location: "current" },
|
|
245
|
+
{ ok: false, code: "JOB_NOT_FOUND", jobId: "job-3", location: "current" },
|
|
246
|
+
];
|
|
247
|
+
const transformed = transformMultipleJobs(readResults);
|
|
248
|
+
|
|
249
|
+
expect(transformed).toHaveLength(2);
|
|
250
|
+
expect(getTransformationStats(readResults, transformed)).toEqual({
|
|
251
|
+
totalRead: 3,
|
|
252
|
+
successfulReads: 1,
|
|
253
|
+
successfulTransforms: 1,
|
|
254
|
+
failedTransforms: 0,
|
|
255
|
+
transformationRate: 1,
|
|
256
|
+
statusDistribution: { complete: 1, failed: 1 },
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("buildUnreadableJob maps corrupt reads to an error placeholder with recovery guidance", () => {
|
|
261
|
+
const placeholder = buildUnreadableJob({
|
|
262
|
+
ok: false,
|
|
263
|
+
code: "INVALID_JSON",
|
|
264
|
+
path: "/jobs/job-2/tasks-status.json",
|
|
265
|
+
jobId: "job-2",
|
|
266
|
+
location: "current",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(placeholder.readable).toBe(false);
|
|
270
|
+
expect(placeholder.status).toBe("failed");
|
|
271
|
+
expect(placeholder.error?.code).toBe("INVALID_JSON");
|
|
272
|
+
expect(placeholder.error?.path).toBe("/jobs/job-2/tasks-status.json");
|
|
273
|
+
const message = placeholder.error?.message.toLowerCase() ?? "";
|
|
274
|
+
expect(message).toContain("repair");
|
|
275
|
+
expect(message).toContain("delete");
|
|
276
|
+
expect(message).toContain("job-2");
|
|
277
|
+
expect(message).toContain("/jobs/job-2/tasks-status.json");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("buildUnreadableJob uses the same fallback code in the message and structured error", () => {
|
|
281
|
+
const placeholder = buildUnreadableJob({
|
|
282
|
+
ok: false,
|
|
283
|
+
message: "EACCES",
|
|
284
|
+
jobId: "job-2",
|
|
285
|
+
location: "current",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(placeholder.error?.code).toBe("UNKNOWN_READ_ERROR");
|
|
289
|
+
expect(placeholder.error?.message).toContain("(UNKNOWN_READ_ERROR)");
|
|
290
|
+
expect(placeholder.error?.message).toContain("EACCES");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("transformMultipleJobs maps corrupt reads to placeholders, skips missing reads, and leaves valid results readable", () => {
|
|
294
|
+
const transformed = transformMultipleJobs([
|
|
295
|
+
{
|
|
296
|
+
ok: true,
|
|
297
|
+
data: { tasks: { a: { state: "done" } } },
|
|
298
|
+
jobId: "job-1",
|
|
299
|
+
location: "current",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
ok: false,
|
|
303
|
+
code: "INVALID_JSON",
|
|
304
|
+
path: "/jobs/job-2/tasks-status.json",
|
|
305
|
+
jobId: "job-2",
|
|
306
|
+
location: "current",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
ok: false,
|
|
310
|
+
code: "JOB_NOT_FOUND",
|
|
311
|
+
jobId: "job-3",
|
|
312
|
+
location: "current",
|
|
313
|
+
},
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
expect(transformed).toHaveLength(2);
|
|
317
|
+
const [valid, unreadable] = transformed;
|
|
318
|
+
|
|
319
|
+
expect(valid?.status).toBe("complete");
|
|
320
|
+
expect(valid?.readable).toBeUndefined();
|
|
321
|
+
expect(valid?.error).toBeUndefined();
|
|
322
|
+
|
|
323
|
+
expect(unreadable?.readable).toBe(false);
|
|
324
|
+
expect(unreadable?.status).toBe("failed");
|
|
325
|
+
expect(unreadable?.error?.code).toBe("INVALID_JSON");
|
|
326
|
+
expect(unreadable?.error?.path).toBe("/jobs/job-2/tasks-status.json");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("coerces parseable-but-tasks-missing snapshots to readable jobs", () => {
|
|
330
|
+
const transformed = transformMultipleJobs([
|
|
331
|
+
{ ok: true, data: { jobId: "job-3" }, jobId: "job-3", location: "current" },
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
expect(transformed).toHaveLength(1);
|
|
335
|
+
const job = transformed[0];
|
|
336
|
+
expect(job?.readable).not.toBe(false);
|
|
337
|
+
expect(job?.error).toBeUndefined();
|
|
338
|
+
expect(job?.status).toBe("pending");
|
|
339
|
+
expect(job?.tasks).toEqual({});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("transformJobStatus output equals normalizeJobView for object inputs", () => {
|
|
343
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
344
|
+
const raw = {
|
|
345
|
+
jobId: "job-1",
|
|
346
|
+
title: "Title",
|
|
347
|
+
state: "failed",
|
|
348
|
+
tasks: { alpha: { state: "done" } },
|
|
349
|
+
gate: null,
|
|
350
|
+
error: { name: "GateRejected", message: "gate rejected" },
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const wrapped = transformJobStatus(raw, "job-1", "current");
|
|
354
|
+
const direct = normalizeJobView(raw, "job-1", "current");
|
|
355
|
+
|
|
356
|
+
expect(wrapped).not.toBeNull();
|
|
357
|
+
expect(wrapped).toEqual(direct);
|
|
358
|
+
warn.mockRestore();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("transformJobStatus returns null for non-object raw input", () => {
|
|
362
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
363
|
+
expect(transformJobStatus(null, "job-1", "current")).toBeNull();
|
|
364
|
+
expect(transformJobStatus("not an object", "job-1", "current")).toBeNull();
|
|
365
|
+
expect(transformJobStatus([1, 2, 3], "job-1", "current")).toBeNull();
|
|
366
|
+
expect(warn).not.toHaveBeenCalled();
|
|
367
|
+
warn.mockRestore();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("buildUnreadableJob returns status:'failed' and survives isValidJob", () => {
|
|
371
|
+
const placeholder = buildUnreadableJob({
|
|
372
|
+
ok: false,
|
|
373
|
+
code: "INVALID_JSON",
|
|
374
|
+
path: "/jobs/job-2/tasks-status.json",
|
|
375
|
+
jobId: "job-2",
|
|
376
|
+
location: "current",
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(placeholder.status).toBe("failed");
|
|
380
|
+
expect(placeholder.readable).toBe(false);
|
|
381
|
+
expect(placeholder.id).toBe("job-2");
|
|
382
|
+
expect(placeholder.jobId).toBe("job-2");
|
|
383
|
+
expect(placeholder.error?.code).toBe("INVALID_JSON");
|
|
384
|
+
expect(placeholder.error?.path).toBe("/jobs/job-2/tasks-status.json");
|
|
385
|
+
|
|
386
|
+
expect(Boolean(placeholder.id && placeholder.status)).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("transformTasks uses zero-based fallback ids for unnamed array entries", () => {
|
|
390
|
+
const unnamed = transformTasks([{}, {}]);
|
|
391
|
+
expect(Object.keys(unnamed)).toEqual(["task-0", "task-1"]);
|
|
392
|
+
|
|
393
|
+
const mixed = transformTasks([{ name: "alpha" }, {}]);
|
|
394
|
+
expect(Object.keys(mixed)).toEqual(["alpha", "task-1"]);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("transformJobStatus preserves control_invalid kind and omits invalid kind (AC-11)", () => {
|
|
398
|
+
const controlInvalid = transformJobStatus(
|
|
399
|
+
{
|
|
400
|
+
state: "waiting",
|
|
401
|
+
gate: {
|
|
402
|
+
afterTask: "review",
|
|
403
|
+
message: "Control file invalid for review: bad directive.",
|
|
404
|
+
requestedAt: "2026-06-19T12:00:00.000Z",
|
|
405
|
+
kind: "control_invalid",
|
|
406
|
+
},
|
|
407
|
+
tasks: {
|
|
408
|
+
review: { state: "done" },
|
|
409
|
+
deploy: { state: "pending" },
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
"job-1",
|
|
413
|
+
"current",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(controlInvalid?.gate?.kind).toBe("control_invalid");
|
|
417
|
+
|
|
418
|
+
const invalidKind = transformJobStatus(
|
|
419
|
+
{
|
|
420
|
+
state: "waiting",
|
|
421
|
+
gate: {
|
|
422
|
+
afterTask: "review",
|
|
423
|
+
message: "Control file invalid.",
|
|
424
|
+
requestedAt: "2026-06-19T12:00:00.000Z",
|
|
425
|
+
kind: "garbage",
|
|
426
|
+
},
|
|
427
|
+
tasks: {
|
|
428
|
+
review: { state: "done" },
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
"job-2",
|
|
432
|
+
"current",
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
expect(invalidKind?.gate).toBeDefined();
|
|
436
|
+
expect(Object.hasOwn(invalidKind!.gate!, "kind")).toBe(false);
|
|
437
|
+
});
|
|
210
438
|
});
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
const STATUS_PRIORITY: Record<string, number> = {
|
|
13
13
|
running: 4,
|
|
14
14
|
waiting: 4,
|
|
15
|
-
|
|
15
|
+
failed: 3,
|
|
16
16
|
pending: 2,
|
|
17
17
|
complete: 1,
|
|
18
18
|
};
|
|
@@ -24,9 +24,13 @@ const ZERO_COSTS: CostsSummary = {
|
|
|
24
24
|
totalCost: 0,
|
|
25
25
|
totalInputCost: 0,
|
|
26
26
|
totalOutputCost: 0,
|
|
27
|
+
estimatedCost: 0,
|
|
28
|
+
hasEstimated: false,
|
|
29
|
+
unavailableCost: 0,
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
function isValidJob(job: CanonicalJob): boolean {
|
|
33
|
+
if (job.readable === false) return Boolean(job.id && job.status);
|
|
30
34
|
return Boolean(job.id && job.status && job.createdAt);
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -42,6 +46,9 @@ function getCostsSummary(costs: Record<string, unknown>): CostsSummary {
|
|
|
42
46
|
totalCost: typeof costs["totalCost"] === "number" ? costs["totalCost"] : 0,
|
|
43
47
|
totalInputCost: typeof costs["totalInputCost"] === "number" ? costs["totalInputCost"] : 0,
|
|
44
48
|
totalOutputCost: typeof costs["totalOutputCost"] === "number" ? costs["totalOutputCost"] : 0,
|
|
49
|
+
estimatedCost: typeof costs["estimatedCost"] === "number" ? costs["estimatedCost"] : 0,
|
|
50
|
+
hasEstimated: typeof costs["hasEstimated"] === "boolean" ? costs["hasEstimated"] : false,
|
|
51
|
+
unavailableCost: typeof costs["unavailableCost"] === "number" ? costs["unavailableCost"] : 0,
|
|
45
52
|
};
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -56,7 +63,7 @@ export function sortJobs(jobs: CanonicalJob[]): CanonicalJob[] {
|
|
|
56
63
|
.sort((left, right) => {
|
|
57
64
|
const priority = getStatusPriority(right.status) - getStatusPriority(left.status);
|
|
58
65
|
if (priority !== 0) return priority;
|
|
59
|
-
const created = left.createdAt
|
|
66
|
+
const created = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
|
|
60
67
|
if (created !== 0) return created;
|
|
61
68
|
return left.id.localeCompare(right.id);
|
|
62
69
|
});
|
|
@@ -82,7 +89,7 @@ export function groupJobsByStatus(jobs: CanonicalJob[]): GroupedJobs {
|
|
|
82
89
|
const grouped: GroupedJobs = {
|
|
83
90
|
running: [],
|
|
84
91
|
waiting: [],
|
|
85
|
-
|
|
92
|
+
failed: [],
|
|
86
93
|
pending: [],
|
|
87
94
|
complete: [],
|
|
88
95
|
};
|
|
@@ -158,6 +165,9 @@ export function transformJobListForAPI(
|
|
|
158
165
|
apiJob.pipelineConfig = job.pipelineConfig;
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
if (job.readable !== undefined) apiJob.readable = job.readable;
|
|
169
|
+
if (job.error !== undefined) apiJob.error = job.error;
|
|
170
|
+
|
|
161
171
|
return apiJob;
|
|
162
172
|
});
|
|
163
173
|
}
|