@ryanfw/prompt-orchestration-pipeline 1.3.3 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/http-api.md +14 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +144 -41
- package/src/api/index.ts +15 -83
- package/src/cli/__tests__/analyze-task.test.ts +40 -7
- package/src/cli/__tests__/index.test.ts +337 -17
- package/src/cli/__tests__/types.test.ts +0 -18
- package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
- package/src/cli/analyze-task.ts +3 -14
- package/src/cli/index.ts +73 -61
- package/src/cli/types.ts +0 -13
- package/src/cli/update-pipeline-json.ts +3 -14
- package/src/config/__tests__/paths.test.ts +46 -1
- package/src/config/__tests__/pipeline-registry.test.ts +767 -0
- package/src/config/__tests__/sse-events.test.ts +470 -0
- package/src/config/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +517 -107
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +200 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +485 -3
- package/src/core/__tests__/json-file.test.ts +111 -0
- package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
- package/src/core/__tests__/logger.test.ts +22 -0
- package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
- package/src/core/__tests__/orchestrator.test.ts +373 -2
- package/src/core/__tests__/pipeline-runner.test.ts +946 -9
- package/src/core/__tests__/redact.test.ts +153 -0
- package/src/core/__tests__/runner-liveness.test.ts +45 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +1 -1
- package/src/core/__tests__/task-runner.test.ts +594 -3
- package/src/core/__tests__/task-telemetry.test.ts +241 -0
- package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
- package/src/core/agent-step.ts +4 -1
- package/src/core/agent-types.ts +5 -4
- package/src/core/config.ts +127 -234
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +50 -19
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +162 -18
- package/src/core/json-file.ts +45 -0
- package/src/core/lifecycle-policy.ts +23 -8
- package/src/core/logger.ts +0 -29
- package/src/core/orchestrator.ts +124 -70
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +8 -4
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +3 -28
- package/src/core/task-runner.ts +356 -319
- package/src/core/task-telemetry.ts +107 -0
- package/src/harness/__tests__/subprocess.test.ts +112 -1
- package/src/harness/subprocess.ts +55 -16
- package/src/llm/__tests__/index.test.ts +684 -33
- package/src/llm/index.ts +310 -67
- package/src/providers/__tests__/alibaba.test.ts +67 -6
- package/src/providers/__tests__/anthropic.test.ts +35 -14
- package/src/providers/__tests__/base.test.ts +62 -0
- package/src/providers/__tests__/claude-code.test.ts +99 -14
- package/src/providers/__tests__/deepseek.test.ts +16 -6
- package/src/providers/__tests__/gemini.test.ts +47 -25
- package/src/providers/__tests__/moonshot.test.ts +27 -0
- package/src/providers/__tests__/openai.test.ts +262 -74
- package/src/providers/__tests__/opencode.test.ts +77 -0
- package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
- package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
- package/src/providers/__tests__/types.test.ts +85 -0
- package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
- package/src/providers/__tests__/zhipu.test.ts +27 -14
- package/src/providers/alibaba.ts +20 -39
- package/src/providers/anthropic.ts +23 -11
- package/src/providers/base.ts +19 -0
- package/src/providers/claude-code.ts +27 -18
- package/src/providers/deepseek.ts +9 -28
- package/src/providers/gemini.ts +20 -58
- package/src/providers/moonshot.ts +15 -11
- package/src/providers/openai.ts +79 -61
- package/src/providers/opencode.ts +16 -33
- package/src/providers/stream-accumulator.ts +27 -21
- package/src/providers/types.ts +29 -4
- package/src/providers/zhipu.ts +15 -44
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
- package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
- package/src/task-analysis/__tests__/repository.test.ts +65 -0
- package/src/task-analysis/__tests__/types.test.ts +63 -130
- package/src/task-analysis/analyzer.ts +91 -0
- package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
- package/src/task-analysis/index.ts +2 -36
- package/src/task-analysis/repository.ts +45 -0
- package/src/task-analysis/types.ts +42 -58
- package/src/ui/client/__tests__/api.test.ts +143 -1
- package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
- package/src/ui/client/__tests__/job-adapter.test.ts +125 -2
- package/src/ui/client/__tests__/load-state.test.ts +70 -0
- package/src/ui/client/__tests__/types.test.ts +66 -3
- package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
- package/src/ui/client/__tests__/useJobList.test.ts +198 -23
- package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
- package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
- package/src/ui/client/adapters/job-adapter.ts +31 -16
- package/src/ui/client/api.ts +38 -15
- package/src/ui/client/bootstrap.ts +19 -14
- package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
- package/src/ui/client/hooks/useJobList.ts +26 -31
- package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
- package/src/ui/client/load-state.ts +20 -0
- package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
- package/src/ui/client/reducers/job-events.ts +137 -0
- package/src/ui/client/types.ts +14 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +13 -2
- package/src/ui/components/StageTimeline.tsx +8 -20
- package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
- package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
- package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
- package/src/ui/components/__tests__/JobTable.test.tsx +83 -0
- package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
- package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
- package/src/ui/components/types.ts +33 -15
- package/src/ui/dist/assets/{index-L6cvsCAx.js → index-DN3-zvtP.js} +4299 -251
- package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
- package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/pages/PipelineDetail.tsx +60 -4
- package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
- package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
- package/src/ui/pages/__tests__/pages.test.tsx +236 -42
- package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
- package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
- package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
- package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
- package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
- package/src/ui/server/__tests__/index.test.ts +63 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +455 -1
- package/src/ui/server/__tests__/job-endpoints.test.ts +43 -1
- package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
- package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
- package/src/ui/server/__tests__/router-threading.test.ts +148 -0
- package/src/ui/server/__tests__/router.test.ts +104 -0
- package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
- package/src/ui/server/__tests__/sse-enhancer.test.ts +39 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- package/src/ui/server/embedded-assets-imports.d.ts +44 -0
- package/src/ui/server/embedded-assets.ts +13 -2
- package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
- package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
- package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
- package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
- package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
- package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
- package/src/ui/server/endpoints/file-endpoints.ts +15 -10
- package/src/ui/server/endpoints/job-control-endpoints.ts +122 -73
- package/src/ui/server/endpoints/job-endpoints.ts +1 -0
- package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
- package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
- package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
- package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
- package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
- package/src/ui/server/index.ts +19 -14
- package/src/ui/server/job-reader.ts +14 -2
- package/src/ui/server/router.ts +33 -10
- package/src/ui/server/sse-broadcast.ts +14 -3
- package/src/ui/server/sse-enhancer.ts +12 -2
- package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
- package/src/ui/state/__tests__/snapshot.test.ts +70 -15
- package/src/ui/state/__tests__/types.test.ts +6 -0
- package/src/ui/state/snapshot.ts +1 -1
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +42 -0
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +45 -3
- package/src/ui/state/transformers/list-transformer.ts +6 -0
- package/src/ui/state/types.ts +6 -1
- package/src/utils/__tests__/path-containment.test.ts +160 -0
- package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
- package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
- package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
- package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
- package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
- package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
- package/src/task-analysis/__tests__/index.test.ts +0 -143
- package/src/task-analysis/__tests__/parser.test.ts +0 -41
- package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
- package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
- package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
- package/src/task-analysis/extractors/artifacts.ts +0 -143
- package/src/task-analysis/extractors/llm-calls.ts +0 -117
- package/src/task-analysis/extractors/stages.ts +0 -45
- package/src/task-analysis/parser.ts +0 -20
- package/src/task-analysis/utils/ast.ts +0 -45
- package/src/ui/dist/assets/index-L6cvsCAx.js.map +0 -1
- package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
- package/src/ui/embedded-assets.js +0 -12
- package/src/ui/server/__tests__/path-containment.test.ts +0 -54
package/src/ui/client/types.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { JobStatusValue, TaskStateValue } from "../../config/statuses";
|
|
2
|
+
import type { SseEventType } from "../../config/sse-events.ts";
|
|
3
|
+
import type { UsageSource } from "../../providers/types";
|
|
4
|
+
|
|
5
|
+
export type { SseEventType };
|
|
2
6
|
|
|
3
7
|
export type ApiErrorCode =
|
|
4
8
|
| "job_running"
|
|
@@ -43,19 +47,6 @@ export interface RestartJobOptions {
|
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
export type SseEventType =
|
|
47
|
-
| "state"
|
|
48
|
-
| "job:updated"
|
|
49
|
-
| "job:created"
|
|
50
|
-
| "job:removed"
|
|
51
|
-
| "heartbeat"
|
|
52
|
-
| "message"
|
|
53
|
-
| "status:changed"
|
|
54
|
-
| "seed:uploaded"
|
|
55
|
-
| "state:change"
|
|
56
|
-
| "state:summary"
|
|
57
|
-
| "task:updated";
|
|
58
|
-
|
|
59
50
|
export interface BootstrapOptions {
|
|
60
51
|
stateUrl?: string;
|
|
61
52
|
sseUrl?: string;
|
|
@@ -87,6 +78,9 @@ export interface CostsSummary {
|
|
|
87
78
|
totalCost: number;
|
|
88
79
|
totalInputCost: number;
|
|
89
80
|
totalOutputCost: number;
|
|
81
|
+
estimatedCost: number;
|
|
82
|
+
hasEstimated: boolean;
|
|
83
|
+
unavailableCost: number;
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
export type RetryError = { message: string; name?: string; stack?: string } | string;
|
|
@@ -96,6 +90,7 @@ export interface GateInfo {
|
|
|
96
90
|
message: string;
|
|
97
91
|
artifacts?: string[];
|
|
98
92
|
requestedAt: string;
|
|
93
|
+
kind?: "approval" | "control_invalid";
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
export interface TaskFiles {
|
|
@@ -165,6 +160,9 @@ export interface TaskCostBreakdown {
|
|
|
165
160
|
inputCost: number;
|
|
166
161
|
outputCost: number;
|
|
167
162
|
totalCost: number;
|
|
163
|
+
source?: UsageSource;
|
|
164
|
+
failed?: boolean;
|
|
165
|
+
costEstimated?: boolean;
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
export interface NormalizedJobDetail extends NormalizedJobSummary {
|
|
@@ -185,11 +183,12 @@ export interface UseJobListWithUpdatesResult extends UseJobListResult {
|
|
|
185
183
|
export interface UseJobDetailWithUpdatesResult {
|
|
186
184
|
data: NormalizedJobDetail | null;
|
|
187
185
|
loading: boolean;
|
|
188
|
-
error:
|
|
186
|
+
error: ApiError | null;
|
|
189
187
|
connectionStatus: ConnectionStatus;
|
|
190
188
|
isRefreshing: boolean;
|
|
191
189
|
isTransitioning: boolean;
|
|
192
190
|
isHydrated: boolean;
|
|
191
|
+
refetch: () => void;
|
|
193
192
|
}
|
|
194
193
|
|
|
195
194
|
export type AnalysisStatus = "idle" | "connecting" | "running" | "complete" | "error";
|
|
@@ -235,7 +234,7 @@ export interface JobConcurrencyApiStatus {
|
|
|
235
234
|
}>;
|
|
236
235
|
staleSlots: Array<{
|
|
237
236
|
jobId: string;
|
|
238
|
-
reason: "missing_current_job" | "missing_pid" | "dead_pid" | "invalid_json";
|
|
237
|
+
reason: "missing_current_job" | "missing_pid" | "dead_pid" | "invalid_json" | "stale_runner";
|
|
239
238
|
}>;
|
|
240
239
|
}
|
|
241
240
|
|
|
@@ -246,11 +245,6 @@ export interface UseConcurrencyStatusResult {
|
|
|
246
245
|
refetch: () => void;
|
|
247
246
|
}
|
|
248
247
|
|
|
249
|
-
export interface SseJobEvent {
|
|
250
|
-
type: SseEventType;
|
|
251
|
-
data: Record<string, unknown>;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
248
|
export type AnalysisSseEventType =
|
|
255
249
|
| "started"
|
|
256
250
|
| "task:start"
|
|
@@ -346,7 +346,12 @@ export default function DAGGrid({
|
|
|
346
346
|
</div>
|
|
347
347
|
</div>
|
|
348
348
|
<div className="p-4">
|
|
349
|
-
{items[index]?.subtitle ?
|
|
349
|
+
{items[index]?.subtitle ? (
|
|
350
|
+
<div className="text-sm text-gray-600">
|
|
351
|
+
{items[index]?.costEstimated ? <abbr title="Estimated cost" className="mr-0.5">≈</abbr> : null}
|
|
352
|
+
{items[index]?.subtitle}
|
|
353
|
+
</div>
|
|
354
|
+
) : null}
|
|
350
355
|
{items[index]?.body ? <div className="mt-2 text-sm text-gray-700">{items[index]?.body}</div> : null}
|
|
351
356
|
<div className="mt-3 flex gap-2 border-t border-gray-100 pt-3">
|
|
352
357
|
{itemStatus === "pending" ? (
|
|
@@ -33,13 +33,20 @@ export default function JobDetail({
|
|
|
33
33
|
typeof task?.config?.["model"] === "string" ? String(task.config.model) : null,
|
|
34
34
|
typeof task?.refinementAttempts === "number" ? `${task.refinementAttempts} refinements` : null,
|
|
35
35
|
];
|
|
36
|
-
const breakdown =
|
|
37
|
-
|
|
38
|
-
if (
|
|
36
|
+
const breakdown = job.costs?.[item.id];
|
|
37
|
+
const breakdownTokens = breakdown ? breakdown.inputTokens + breakdown.outputTokens : 0;
|
|
38
|
+
if (breakdownTokens > 0) parts.push(formatTokensCompact(breakdownTokens));
|
|
39
|
+
if (typeof breakdown?.totalCost === "number" && breakdown.totalCost > 0) parts.push(formatCurrency4(breakdown.totalCost));
|
|
40
|
+
const source = breakdown?.source;
|
|
41
|
+
const costEstimated =
|
|
42
|
+
breakdown?.costEstimated === true ||
|
|
43
|
+
source === "estimated" ||
|
|
44
|
+
source === "unavailable";
|
|
39
45
|
return {
|
|
40
46
|
...item,
|
|
41
47
|
subtitle: parts.filter(Boolean).join(" · ") || null,
|
|
42
48
|
body: item.body ?? task?.error?.message ?? null,
|
|
49
|
+
...(costEstimated ? { costEstimated: true } : {}),
|
|
43
50
|
};
|
|
44
51
|
});
|
|
45
52
|
|
|
@@ -54,7 +61,8 @@ export default function JobDetail({
|
|
|
54
61
|
prior.title === item.title &&
|
|
55
62
|
prior.subtitle === item.subtitle &&
|
|
56
63
|
prior.body === item.body &&
|
|
57
|
-
prior.restartCount === item.restartCount
|
|
64
|
+
prior.restartCount === item.restartCount &&
|
|
65
|
+
prior.costEstimated === item.costEstimated
|
|
58
66
|
) {
|
|
59
67
|
return prior;
|
|
60
68
|
}
|
|
@@ -71,8 +71,12 @@ export default function JobTable({
|
|
|
71
71
|
const currentTaskId =
|
|
72
72
|
typeof job.current === "string" ? job.current : job.current?.taskName;
|
|
73
73
|
const currentTask = currentTaskId ? taskMap[currentTaskId] : undefined;
|
|
74
|
-
const totalCost =
|
|
74
|
+
const totalCost =
|
|
75
|
+
(job.totalCost ?? job.costsSummary?.totalCost ?? 0) +
|
|
76
|
+
(job.costsSummary?.estimatedCost ?? 0) +
|
|
77
|
+
(job.costsSummary?.unavailableCost ?? 0);
|
|
75
78
|
const totalTokens = job.totalTokens ?? job.costsSummary?.totalTokens ?? 0;
|
|
79
|
+
const hasEstimated = job.costsSummary?.hasEstimated === true;
|
|
76
80
|
const startMs = Object.values(taskMap).map((task) => toMs(task.startedAt)).filter((value): value is number => value !== null).sort((a, b) => a - b)[0] ?? null;
|
|
77
81
|
const endMs = Object.values(taskMap).map((task) => toMs(task.endedAt)).filter((value): value is number => value !== null).sort((a, b) => b - a)[0] ?? null;
|
|
78
82
|
|
|
@@ -114,7 +118,14 @@ export default function JobTable({
|
|
|
114
118
|
<td className="px-4 py-4">
|
|
115
119
|
{readable ? (
|
|
116
120
|
<>
|
|
117
|
-
<div className="text-sm text-gray-700">
|
|
121
|
+
<div className="text-sm text-gray-700">
|
|
122
|
+
{totalCost > 0 ? (
|
|
123
|
+
<>
|
|
124
|
+
{hasEstimated ? <abbr title="Estimated cost" className="mr-0.5">≈</abbr> : null}
|
|
125
|
+
{formatCurrency4(totalCost)}
|
|
126
|
+
</>
|
|
127
|
+
) : "—"}
|
|
128
|
+
</div>
|
|
118
129
|
{totalTokens > 0 ? <div className="text-xs text-gray-500">{formatTokensCompact(totalTokens)}</div> : null}
|
|
119
130
|
</>
|
|
120
131
|
) : (
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import { Badge } from "./ui/Badge";
|
|
3
|
+
import type { AnalysisStage } from "./types";
|
|
5
4
|
|
|
6
|
-
export const StageTimeline = memo(function StageTimeline({ stages }: { stages:
|
|
7
|
-
const
|
|
8
|
-
.filter((stage) => stage.name)
|
|
9
|
-
.slice()
|
|
10
|
-
.sort((a, b) => {
|
|
11
|
-
const orderA = typeof a.order === "number" ? a.order : Number.MAX_SAFE_INTEGER;
|
|
12
|
-
const orderB = typeof b.order === "number" ? b.order : Number.MAX_SAFE_INTEGER;
|
|
13
|
-
return orderA - orderB;
|
|
14
|
-
});
|
|
5
|
+
export const StageTimeline = memo(function StageTimeline({ stages }: { stages: AnalysisStage[] }) {
|
|
6
|
+
const namedStages = stages.filter((stage) => stage.name);
|
|
15
7
|
|
|
16
8
|
return (
|
|
17
9
|
<ol role="list" aria-label="Task execution stages" className="relative ml-1 space-y-4 border-l border-gray-300">
|
|
18
|
-
{
|
|
19
|
-
<li key={stage.name} className="relative flex
|
|
20
|
-
<div className="absolute left-[-5px] h-2 w-2 rounded-full bg-[#6d28d9]" />
|
|
21
|
-
<span className="text-sm">{stage.name}</span>
|
|
22
|
-
{stage.
|
|
23
|
-
<Badge intent="amber" className="ml-auto">
|
|
24
|
-
async
|
|
25
|
-
</Badge>
|
|
26
|
-
) : null}
|
|
10
|
+
{namedStages.map((stage) => (
|
|
11
|
+
<li key={stage.name} className="relative flex flex-col gap-1 pl-4">
|
|
12
|
+
<div className="absolute left-[-5px] top-1.5 h-2 w-2 rounded-full bg-[#6d28d9]" />
|
|
13
|
+
<span className="text-sm font-medium">{stage.name}</span>
|
|
14
|
+
{stage.purpose ? <span className="text-sm text-gray-600">{stage.purpose}</span> : null}
|
|
27
15
|
</li>
|
|
28
16
|
))}
|
|
29
17
|
</ol>
|
|
@@ -45,11 +45,47 @@ export const TaskAnalysisDisplay = memo(function TaskAnalysisDisplay({
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
if (loading) return <div className="p-4 text-sm text-gray-500">Loading analysis…</div>;
|
|
48
|
-
if (error) return <div className="p-4 text-sm text-red-700">{error}</div>;
|
|
48
|
+
if (error) return <div className="p-4 text-sm text-red-700" role="alert">{error}</div>;
|
|
49
49
|
if (analysis === null) return <div className="p-4 text-sm text-gray-500">No analysis available.</div>;
|
|
50
50
|
|
|
51
|
+
if (analysis.analysisStatus === "unconfigured") {
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-3 p-6 text-sm" role="status">
|
|
54
|
+
<h3 className="text-base font-semibold">Analysis not configured</h3>
|
|
55
|
+
<p className="text-gray-600">
|
|
56
|
+
No analysis model is configured. Set an internal analysis model to generate a reference for this
|
|
57
|
+
task.
|
|
58
|
+
</p>
|
|
59
|
+
{analysis.analysisError ? (
|
|
60
|
+
<p className="text-xs text-gray-500">{analysis.analysisError}</p>
|
|
61
|
+
) : null}
|
|
62
|
+
<Button size="sm" variant="outline">
|
|
63
|
+
Configure model
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (analysis.analysisStatus === "error") {
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-2 p-6 text-sm" role="alert">
|
|
72
|
+
<h3 className="text-base font-semibold text-red-700">Analysis failed</h3>
|
|
73
|
+
<p className="text-gray-600">The analysis could not be generated. Re-run analysis to try again.</p>
|
|
74
|
+
{analysis.analysisError ? (
|
|
75
|
+
<p className="text-xs text-gray-500">{analysis.analysisError}</p>
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
51
81
|
return (
|
|
52
82
|
<div className="space-y-6 p-6">
|
|
83
|
+
{analysis.summary ? (
|
|
84
|
+
<section className="space-y-2">
|
|
85
|
+
<h3 className="text-base font-semibold">Summary</h3>
|
|
86
|
+
<p className="text-sm text-gray-700">{analysis.summary}</p>
|
|
87
|
+
</section>
|
|
88
|
+
) : null}
|
|
53
89
|
<section className="space-y-3">
|
|
54
90
|
<h3 className="text-base font-semibold">Artifacts</h3>
|
|
55
91
|
<div className="grid gap-4 md:grid-cols-2">
|
|
@@ -57,8 +93,11 @@ export const TaskAnalysisDisplay = memo(function TaskAnalysisDisplay({
|
|
|
57
93
|
<p className="mb-2 text-sm font-medium">Reads</p>
|
|
58
94
|
<ul className="space-y-2 text-sm">
|
|
59
95
|
{analysis.artifacts.reads.map((artifact) => (
|
|
60
|
-
<li key={`read-${artifact.fileName}`} className="flex items-
|
|
61
|
-
<span>
|
|
96
|
+
<li key={`read-${artifact.fileName}`} className="flex items-start justify-between gap-3 rounded border p-2">
|
|
97
|
+
<span className="flex flex-col">
|
|
98
|
+
<span className="font-medium">{artifact.fileName}</span>
|
|
99
|
+
{artifact.role ? <span className="text-xs text-gray-600">{artifact.role}</span> : null}
|
|
100
|
+
</span>
|
|
62
101
|
<Button size="sm" variant="outline" onClick={() => void openPreview(artifact.fileName, "read")}>
|
|
63
102
|
Preview
|
|
64
103
|
</Button>
|
|
@@ -70,8 +109,11 @@ export const TaskAnalysisDisplay = memo(function TaskAnalysisDisplay({
|
|
|
70
109
|
<p className="mb-2 text-sm font-medium">Writes</p>
|
|
71
110
|
<ul className="space-y-2 text-sm">
|
|
72
111
|
{analysis.artifacts.writes.map((artifact) => (
|
|
73
|
-
<li key={`write-${artifact.fileName}`} className="flex items-
|
|
74
|
-
<span>
|
|
112
|
+
<li key={`write-${artifact.fileName}`} className="flex items-start justify-between gap-3 rounded border p-2">
|
|
113
|
+
<span className="flex flex-col">
|
|
114
|
+
<span className="font-medium">{artifact.fileName}</span>
|
|
115
|
+
{artifact.role ? <span className="text-xs text-gray-600">{artifact.role}</span> : null}
|
|
116
|
+
</span>
|
|
75
117
|
<Button size="sm" variant="outline" onClick={() => void openPreview(artifact.fileName, "write")}>
|
|
76
118
|
Preview
|
|
77
119
|
</Button>
|
|
@@ -95,7 +137,7 @@ export const TaskAnalysisDisplay = memo(function TaskAnalysisDisplay({
|
|
|
95
137
|
))}
|
|
96
138
|
</ul>
|
|
97
139
|
</section>
|
|
98
|
-
<p className="text-xs text-gray-500">Analyzed at {analysis.analyzedAt}</p>
|
|
140
|
+
<p className="text-xs text-gray-500">AI-generated, approximate · Analyzed at {analysis.analyzedAt}</p>
|
|
99
141
|
{preview !== null ? (
|
|
100
142
|
<SchemaPreviewPanel
|
|
101
143
|
fileName={preview.fileName}
|
|
@@ -246,3 +246,39 @@ test("DAGGrid renders gate-adjacent waiting nodes with a visible waiting label",
|
|
|
246
246
|
expect(view.getByText("Waiting")).toBeTruthy();
|
|
247
247
|
expect(view.container.querySelector('[data-status="waiting"]')).not.toBeNull();
|
|
248
248
|
});
|
|
249
|
+
|
|
250
|
+
test("DAGGrid renders estimated marker when costEstimated is true", () => {
|
|
251
|
+
installMatchMedia();
|
|
252
|
+
|
|
253
|
+
const view = render(
|
|
254
|
+
<DAGGrid
|
|
255
|
+
items={[makeItem({ id: "build", title: "Build", status: "done", subtitle: "gpt-4 · 150 tok · $0.0012", costEstimated: true })]}
|
|
256
|
+
jobId="job-1"
|
|
257
|
+
taskById={{ build: makeTaskState({ name: "build", state: "done" }) }}
|
|
258
|
+
pipelineTasks={["build"]}
|
|
259
|
+
filesByTypeForItem={() => emptyFiles}
|
|
260
|
+
geometryAdapter={geometryAdapter}
|
|
261
|
+
/>,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const marker = view.container.querySelector('abbr[title="Estimated cost"]');
|
|
265
|
+
expect(marker).not.toBeNull();
|
|
266
|
+
expect(marker?.textContent).toBe("≈");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("DAGGrid omits estimated marker when costEstimated is absent", () => {
|
|
270
|
+
installMatchMedia();
|
|
271
|
+
|
|
272
|
+
const view = render(
|
|
273
|
+
<DAGGrid
|
|
274
|
+
items={[makeItem({ id: "build", title: "Build", status: "done", subtitle: "gpt-4 · 150 tok · $0.0012" })]}
|
|
275
|
+
jobId="job-1"
|
|
276
|
+
taskById={{ build: makeTaskState({ name: "build", state: "done" }) }}
|
|
277
|
+
pipelineTasks={["build"]}
|
|
278
|
+
filesByTypeForItem={() => emptyFiles}
|
|
279
|
+
geometryAdapter={geometryAdapter}
|
|
280
|
+
/>,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(view.container.querySelector('abbr[title="Estimated cost"]')).toBeNull();
|
|
284
|
+
});
|
|
@@ -137,3 +137,115 @@ test("JobDetail cancels gate rejection when the prompt is cancelled", () => {
|
|
|
137
137
|
window.prompt = originalPrompt;
|
|
138
138
|
}
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
test("JobDetail flags estimated task cost on the DAG item", () => {
|
|
142
|
+
const job: JobDetailType = {
|
|
143
|
+
...makeJob(0),
|
|
144
|
+
costs: {
|
|
145
|
+
build: {
|
|
146
|
+
inputTokens: 100,
|
|
147
|
+
outputTokens: 50,
|
|
148
|
+
inputCost: 0.0006,
|
|
149
|
+
outputCost: 0.0006,
|
|
150
|
+
totalCost: 0.0012,
|
|
151
|
+
source: "estimated",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
render(<JobDetail job={job} pipeline={pipeline} />);
|
|
157
|
+
|
|
158
|
+
const lastItems = capturedItems[capturedItems.length - 1] as DagItem[];
|
|
159
|
+
const buildItem = lastItems.find((item) => item.id === "build");
|
|
160
|
+
expect(buildItem?.costEstimated).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("JobDetail flags unavailable task cost on the DAG item", () => {
|
|
164
|
+
const job: JobDetailType = {
|
|
165
|
+
...makeJob(0),
|
|
166
|
+
costs: {
|
|
167
|
+
build: {
|
|
168
|
+
inputTokens: 100,
|
|
169
|
+
outputTokens: 50,
|
|
170
|
+
inputCost: 0.0006,
|
|
171
|
+
outputCost: 0.0006,
|
|
172
|
+
totalCost: 0.0012,
|
|
173
|
+
source: "unavailable",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
render(<JobDetail job={job} pipeline={pipeline} />);
|
|
179
|
+
|
|
180
|
+
const lastItems = capturedItems[capturedItems.length - 1] as DagItem[];
|
|
181
|
+
const buildItem = lastItems.find((item) => item.id === "build");
|
|
182
|
+
expect(buildItem?.costEstimated).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("JobDetail does not flag reported failed task cost as estimated", () => {
|
|
186
|
+
const job: JobDetailType = {
|
|
187
|
+
...makeJob(0),
|
|
188
|
+
costs: {
|
|
189
|
+
build: {
|
|
190
|
+
inputTokens: 100,
|
|
191
|
+
outputTokens: 0,
|
|
192
|
+
inputCost: 0.0006,
|
|
193
|
+
outputCost: 0,
|
|
194
|
+
totalCost: 0.0006,
|
|
195
|
+
source: "reported",
|
|
196
|
+
failed: true,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
render(<JobDetail job={job} pipeline={pipeline} />);
|
|
202
|
+
|
|
203
|
+
const lastItems = capturedItems[capturedItems.length - 1] as DagItem[];
|
|
204
|
+
const buildItem = lastItems.find((item) => item.id === "build");
|
|
205
|
+
expect(buildItem?.costEstimated).toBeFalsy();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("JobDetail flags reported task cost when costEstimated is explicit", () => {
|
|
209
|
+
const job: JobDetailType = {
|
|
210
|
+
...makeJob(0),
|
|
211
|
+
costs: {
|
|
212
|
+
build: {
|
|
213
|
+
inputTokens: 100,
|
|
214
|
+
outputTokens: 50,
|
|
215
|
+
inputCost: 0,
|
|
216
|
+
outputCost: 0,
|
|
217
|
+
totalCost: 0,
|
|
218
|
+
source: "reported",
|
|
219
|
+
costEstimated: true,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
render(<JobDetail job={job} pipeline={pipeline} />);
|
|
225
|
+
|
|
226
|
+
const lastItems = capturedItems[capturedItems.length - 1] as DagItem[];
|
|
227
|
+
const buildItem = lastItems.find((item) => item.id === "build");
|
|
228
|
+
expect(buildItem?.costEstimated).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("JobDetail leaves costEstimated unset for reported task cost", () => {
|
|
232
|
+
const job: JobDetailType = {
|
|
233
|
+
...makeJob(0),
|
|
234
|
+
costs: {
|
|
235
|
+
build: {
|
|
236
|
+
inputTokens: 100,
|
|
237
|
+
outputTokens: 50,
|
|
238
|
+
inputCost: 0.0006,
|
|
239
|
+
outputCost: 0.0006,
|
|
240
|
+
totalCost: 0.0012,
|
|
241
|
+
source: "reported",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
render(<JobDetail job={job} pipeline={pipeline} />);
|
|
247
|
+
|
|
248
|
+
const lastItems = capturedItems[capturedItems.length - 1] as DagItem[];
|
|
249
|
+
const buildItem = lastItems.find((item) => item.id === "build");
|
|
250
|
+
expect(buildItem?.costEstimated).toBeFalsy();
|
|
251
|
+
});
|
|
@@ -106,3 +106,86 @@ test("JobTable renders a normal job without readable or error as a clickable row
|
|
|
106
106
|
expect(onOpenJob).toHaveBeenCalledTimes(1);
|
|
107
107
|
expect(onOpenJob).toHaveBeenCalledWith("job-1");
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
test("JobTable marks estimated cost when costsSummary.hasEstimated is true", () => {
|
|
111
|
+
const view = render(
|
|
112
|
+
<MemoryRouter>
|
|
113
|
+
<JobTable
|
|
114
|
+
jobs={[
|
|
115
|
+
makeJob({
|
|
116
|
+
costsSummary: {
|
|
117
|
+
totalTokens: 10,
|
|
118
|
+
totalInputTokens: 5,
|
|
119
|
+
totalOutputTokens: 5,
|
|
120
|
+
totalCost: 0.0012,
|
|
121
|
+
totalInputCost: 0.0006,
|
|
122
|
+
totalOutputCost: 0.0006,
|
|
123
|
+
estimatedCost: 0.0012,
|
|
124
|
+
hasEstimated: true,
|
|
125
|
+
unavailableCost: 0,
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
]}
|
|
129
|
+
onOpenJob={() => undefined}
|
|
130
|
+
/>
|
|
131
|
+
</MemoryRouter>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const marker = view.container.querySelector('tbody abbr[title="Estimated cost"]');
|
|
135
|
+
expect(marker).not.toBeNull();
|
|
136
|
+
expect(marker?.textContent).toBe("≈");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("JobTable displays reported, estimated, and unavailable cost buckets together", () => {
|
|
140
|
+
const view = render(
|
|
141
|
+
<MemoryRouter>
|
|
142
|
+
<JobTable
|
|
143
|
+
jobs={[
|
|
144
|
+
makeJob({
|
|
145
|
+
costsSummary: {
|
|
146
|
+
totalTokens: 10,
|
|
147
|
+
totalInputTokens: 5,
|
|
148
|
+
totalOutputTokens: 5,
|
|
149
|
+
totalCost: 0.001,
|
|
150
|
+
totalInputCost: 0.001,
|
|
151
|
+
totalOutputCost: 0,
|
|
152
|
+
estimatedCost: 0.002,
|
|
153
|
+
hasEstimated: true,
|
|
154
|
+
unavailableCost: 0.003,
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
]}
|
|
158
|
+
onOpenJob={() => undefined}
|
|
159
|
+
/>
|
|
160
|
+
</MemoryRouter>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(view.getByText("$0.006")).toBeTruthy();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("JobTable omits estimated marker for a fully reported job", () => {
|
|
167
|
+
const view = render(
|
|
168
|
+
<MemoryRouter>
|
|
169
|
+
<JobTable
|
|
170
|
+
jobs={[
|
|
171
|
+
makeJob({
|
|
172
|
+
costsSummary: {
|
|
173
|
+
totalTokens: 10,
|
|
174
|
+
totalInputTokens: 5,
|
|
175
|
+
totalOutputTokens: 5,
|
|
176
|
+
totalCost: 0.0012,
|
|
177
|
+
totalInputCost: 0.0006,
|
|
178
|
+
totalOutputCost: 0.0006,
|
|
179
|
+
estimatedCost: 0,
|
|
180
|
+
hasEstimated: false,
|
|
181
|
+
unavailableCost: 0,
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
]}
|
|
185
|
+
onOpenJob={() => undefined}
|
|
186
|
+
/>
|
|
187
|
+
</MemoryRouter>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(view.container.querySelector('tbody abbr[title="Estimated cost"]')).toBeNull();
|
|
191
|
+
});
|
|
@@ -11,20 +11,19 @@ afterEach(() => {
|
|
|
11
11
|
document.body.innerHTML = "";
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
test("StageTimeline
|
|
14
|
+
test("StageTimeline renders each stage name and purpose in order", () => {
|
|
15
15
|
const view = render(
|
|
16
16
|
<StageTimeline
|
|
17
17
|
stages={[
|
|
18
|
-
{ name: "
|
|
19
|
-
{ name: "
|
|
20
|
-
{ name: "C" },
|
|
18
|
+
{ name: "ingestion", purpose: "Load raw input" },
|
|
19
|
+
{ name: "inference", purpose: "Run the model" },
|
|
21
20
|
]}
|
|
22
21
|
/>,
|
|
23
22
|
);
|
|
24
23
|
|
|
25
24
|
const items = Array.from(view.container.querySelectorAll("li")).map((node) => node.textContent?.trim());
|
|
26
|
-
expect(items).toEqual(["
|
|
27
|
-
expect(view.getByText("
|
|
25
|
+
expect(items).toEqual(["ingestionLoad raw input", "inferenceRun the model"]);
|
|
26
|
+
expect(view.getByText("Load raw input")).toBeTruthy();
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
test("PageSubheader renders breadcrumbs", () => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import "./test-dom";
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { afterEach, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import { TaskAnalysisDisplay } from "../TaskAnalysisDisplay";
|
|
7
|
+
import type { TaskAnalysis } from "../types";
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
document.body.innerHTML = "";
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function makeAnalysis(overrides: Partial<TaskAnalysis> = {}): TaskAnalysis {
|
|
14
|
+
return {
|
|
15
|
+
summary: "Summarizes the input document.",
|
|
16
|
+
stages: [{ name: "ingestion", purpose: "Load raw input" }],
|
|
17
|
+
artifacts: {
|
|
18
|
+
reads: [{ fileName: "input.json", role: "source data" }],
|
|
19
|
+
writes: [{ fileName: "output.json", role: "result data" }],
|
|
20
|
+
},
|
|
21
|
+
models: [{ provider: "deepseek", method: "chat", stage: "ingestion" }],
|
|
22
|
+
analyzedAt: "2026-06-22T00:00:00.000Z",
|
|
23
|
+
analysisStatus: "ok",
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("ok renders summary, stage purpose, artifacts, models, and the AI-generated label", () => {
|
|
29
|
+
const view = render(
|
|
30
|
+
<TaskAnalysisDisplay analysis={makeAnalysis()} loading={false} error={null} pipelineSlug="demo" />,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(view.getByText("Summarizes the input document.")).toBeTruthy();
|
|
34
|
+
expect(view.getByText("Load raw input")).toBeTruthy();
|
|
35
|
+
expect(view.getByText("input.json")).toBeTruthy();
|
|
36
|
+
expect(view.getByText("source data")).toBeTruthy();
|
|
37
|
+
expect(view.getByText("output.json")).toBeTruthy();
|
|
38
|
+
expect(view.getByText("result data")).toBeTruthy();
|
|
39
|
+
expect(view.getByText("deepseek · chat · ingestion")).toBeTruthy();
|
|
40
|
+
expect(view.container.textContent).toContain("AI-generated, approximate");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("unconfigured renders the configure message and CTA", () => {
|
|
44
|
+
const analysis = makeAnalysis({ analysisStatus: "unconfigured" });
|
|
45
|
+
const view = render(
|
|
46
|
+
<TaskAnalysisDisplay analysis={analysis} loading={false} error={null} pipelineSlug="demo" />,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(view.container.textContent).toContain("No analysis model is configured");
|
|
50
|
+
expect(view.getByRole("button", { name: "Configure model" })).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("error renders an error note with the analysis error detail", () => {
|
|
54
|
+
const analysis = makeAnalysis({
|
|
55
|
+
analysisStatus: "error",
|
|
56
|
+
analysisError: "validation failed for field summary",
|
|
57
|
+
});
|
|
58
|
+
const view = render(
|
|
59
|
+
<TaskAnalysisDisplay analysis={analysis} loading={false} error={null} pipelineSlug="demo" />,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(view.getByText("Analysis failed")).toBeTruthy();
|
|
63
|
+
expect(view.getByText("validation failed for field summary")).toBeTruthy();
|
|
64
|
+
});
|