@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
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 {
|
|
@@ -155,6 +150,8 @@ export interface NormalizedJobSummary {
|
|
|
155
150
|
totalTokens?: number;
|
|
156
151
|
displayCategory: string;
|
|
157
152
|
__warnings?: string[];
|
|
153
|
+
readable?: boolean;
|
|
154
|
+
error?: { code: string; message: string; path?: string };
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
export interface TaskCostBreakdown {
|
|
@@ -163,6 +160,9 @@ export interface TaskCostBreakdown {
|
|
|
163
160
|
inputCost: number;
|
|
164
161
|
outputCost: number;
|
|
165
162
|
totalCost: number;
|
|
163
|
+
source?: UsageSource;
|
|
164
|
+
failed?: boolean;
|
|
165
|
+
costEstimated?: boolean;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
export interface NormalizedJobDetail extends NormalizedJobSummary {
|
|
@@ -183,11 +183,12 @@ export interface UseJobListWithUpdatesResult extends UseJobListResult {
|
|
|
183
183
|
export interface UseJobDetailWithUpdatesResult {
|
|
184
184
|
data: NormalizedJobDetail | null;
|
|
185
185
|
loading: boolean;
|
|
186
|
-
error:
|
|
186
|
+
error: ApiError | null;
|
|
187
187
|
connectionStatus: ConnectionStatus;
|
|
188
188
|
isRefreshing: boolean;
|
|
189
189
|
isTransitioning: boolean;
|
|
190
190
|
isHydrated: boolean;
|
|
191
|
+
refetch: () => void;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
export type AnalysisStatus = "idle" | "connecting" | "running" | "complete" | "error";
|
|
@@ -233,7 +234,7 @@ export interface JobConcurrencyApiStatus {
|
|
|
233
234
|
}>;
|
|
234
235
|
staleSlots: Array<{
|
|
235
236
|
jobId: string;
|
|
236
|
-
reason: "missing_current_job" | "missing_pid" | "dead_pid" | "invalid_json";
|
|
237
|
+
reason: "missing_current_job" | "missing_pid" | "dead_pid" | "invalid_json" | "stale_runner";
|
|
237
238
|
}>;
|
|
238
239
|
}
|
|
239
240
|
|
|
@@ -244,11 +245,6 @@ export interface UseConcurrencyStatusResult {
|
|
|
244
245
|
refetch: () => void;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
export interface SseJobEvent {
|
|
248
|
-
type: SseEventType;
|
|
249
|
-
data: Record<string, unknown>;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
248
|
export type AnalysisSseEventType =
|
|
253
249
|
| "started"
|
|
254
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
|
}
|
|
@@ -65,47 +65,75 @@ export default function JobTable({
|
|
|
65
65
|
<tbody className="divide-y divide-gray-200">
|
|
66
66
|
{jobs.map((job) => {
|
|
67
67
|
const validId = Boolean(job.id);
|
|
68
|
+
const readable = job.readable !== false;
|
|
69
|
+
const clickable = validId && readable;
|
|
68
70
|
const taskMap = Array.isArray(job.tasks) ? Object.fromEntries(job.tasks.map((task) => [task.name, task] as const)) : job.tasks;
|
|
69
71
|
const currentTaskId =
|
|
70
72
|
typeof job.current === "string" ? job.current : job.current?.taskName;
|
|
71
73
|
const currentTask = currentTaskId ? taskMap[currentTaskId] : undefined;
|
|
72
|
-
const totalCost =
|
|
74
|
+
const totalCost =
|
|
75
|
+
(job.totalCost ?? job.costsSummary?.totalCost ?? 0) +
|
|
76
|
+
(job.costsSummary?.estimatedCost ?? 0) +
|
|
77
|
+
(job.costsSummary?.unavailableCost ?? 0);
|
|
73
78
|
const totalTokens = job.totalTokens ?? job.costsSummary?.totalTokens ?? 0;
|
|
79
|
+
const hasEstimated = job.costsSummary?.hasEstimated === true;
|
|
74
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;
|
|
75
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;
|
|
76
82
|
|
|
77
83
|
return (
|
|
78
84
|
<tr
|
|
79
85
|
key={job.jobId}
|
|
80
|
-
className={
|
|
81
|
-
onClick={() =>
|
|
82
|
-
tabIndex={
|
|
86
|
+
className={clickable ? "cursor-pointer transition hover:bg-gray-50" : "cursor-not-allowed opacity-60"}
|
|
87
|
+
onClick={() => clickable && onOpenJob(job.id)}
|
|
88
|
+
tabIndex={clickable ? 0 : -1}
|
|
83
89
|
onKeyDown={(event) => {
|
|
84
|
-
if (
|
|
90
|
+
if (clickable && (event.key === "Enter" || event.key === " ")) onOpenJob(job.id);
|
|
85
91
|
}}
|
|
86
92
|
>
|
|
87
93
|
<td className="px-4 py-4">
|
|
88
94
|
<div className="font-medium text-[#6d28d9]">{job.name}</div>
|
|
89
95
|
<div className="text-xs text-gray-500">{job.id}</div>
|
|
96
|
+
{!readable && job.error ? <div className="text-xs text-red-600">{job.error.message}</div> : null}
|
|
90
97
|
</td>
|
|
91
98
|
<td className="px-4 py-4 text-sm text-gray-700">{job.pipelineLabel ?? job.pipeline ?? pipeline?.name ?? "—"}</td>
|
|
92
99
|
<td className="px-4 py-4">
|
|
93
|
-
|
|
100
|
+
{readable ? (
|
|
101
|
+
<Badge intent={getIntent(job.status)}>{job.status}</Badge>
|
|
102
|
+
) : (
|
|
103
|
+
<Badge intent="red">Unreadable</Badge>
|
|
104
|
+
)}
|
|
94
105
|
</td>
|
|
95
106
|
<td className="px-4 py-4 text-sm text-gray-700">{currentTask?.name ?? currentTaskId ?? "—"}</td>
|
|
96
107
|
<td className="px-4 py-4">
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
{readable ? (
|
|
109
|
+
<div className="flex min-w-[160px] items-center gap-3">
|
|
110
|
+
<Progress value={job.progress} variant={job.status === "failed" ? "error" : job.status === "complete" ? "completed" : job.status === "running" ? "running" : "pending"} className="flex-1" />
|
|
111
|
+
<span className="text-sm text-gray-600">{job.progress}%</span>
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
"—"
|
|
115
|
+
)}
|
|
101
116
|
</td>
|
|
102
117
|
<td className="px-4 py-4 text-sm text-gray-700">{countCompleted(job)} of {job.taskCount}</td>
|
|
103
118
|
<td className="px-4 py-4">
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
{readable ? (
|
|
120
|
+
<>
|
|
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>
|
|
129
|
+
{totalTokens > 0 ? <div className="text-xs text-gray-500">{formatTokensCompact(totalTokens)}</div> : null}
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
"—"
|
|
133
|
+
)}
|
|
106
134
|
</td>
|
|
107
135
|
<td className="px-4 py-4 text-sm text-gray-700">
|
|
108
|
-
{startMs ? <TimerText startMs={startMs} endMs={endMs} granularity="second" /> : "—"}
|
|
136
|
+
{readable ? (startMs ? <TimerText startMs={startMs} endMs={endMs} granularity="second" /> : "—") : "—"}
|
|
109
137
|
</td>
|
|
110
138
|
</tr>
|
|
111
139
|
);
|
|
@@ -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
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./test-dom";
|
|
2
2
|
|
|
3
|
-
import { render } from "@testing-library/react";
|
|
3
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
4
4
|
import { MemoryRouter } from "react-router-dom";
|
|
5
5
|
import { afterEach, expect, mock, test } from "bun:test";
|
|
6
6
|
|
|
@@ -53,3 +53,139 @@ test("JobTable renders waiting status badge", () => {
|
|
|
53
53
|
|
|
54
54
|
expect(view.getByText("waiting")).toBeTruthy();
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
test("JobTable renders an unreadable job as a non-clickable row with badge and recovery text", () => {
|
|
58
|
+
const recoveryMessage =
|
|
59
|
+
'Status file is unreadable (INVALID_JSON). Repair or delete tasks-status.json for job "job-bad" to recover.';
|
|
60
|
+
const onOpenJob = mock((_jobId: string) => {});
|
|
61
|
+
const view = render(
|
|
62
|
+
<MemoryRouter>
|
|
63
|
+
<JobTable
|
|
64
|
+
jobs={[
|
|
65
|
+
makeJob({
|
|
66
|
+
id: "job-bad",
|
|
67
|
+
jobId: "job-bad",
|
|
68
|
+
name: "Bad Job",
|
|
69
|
+
status: "error",
|
|
70
|
+
readable: false,
|
|
71
|
+
error: { code: "INVALID_JSON", message: recoveryMessage },
|
|
72
|
+
}),
|
|
73
|
+
]}
|
|
74
|
+
onOpenJob={onOpenJob}
|
|
75
|
+
/>
|
|
76
|
+
</MemoryRouter>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const row = view.container.querySelector("tbody tr");
|
|
80
|
+
expect(row).toBeTruthy();
|
|
81
|
+
expect(row?.getAttribute("tabindex")).toBe("-1");
|
|
82
|
+
expect(row?.className).toContain("cursor-not-allowed");
|
|
83
|
+
|
|
84
|
+
expect(view.getByText("Unreadable")).toBeTruthy();
|
|
85
|
+
expect(view.getByText(recoveryMessage)).toBeTruthy();
|
|
86
|
+
|
|
87
|
+
fireEvent.click(row!);
|
|
88
|
+
fireEvent.keyDown(row!, { key: "Enter" });
|
|
89
|
+
expect(onOpenJob).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("JobTable renders a normal job without readable or error as a clickable row", () => {
|
|
93
|
+
const onOpenJob = mock((_jobId: string) => {});
|
|
94
|
+
const view = render(
|
|
95
|
+
<MemoryRouter>
|
|
96
|
+
<JobTable jobs={[makeJob()]} onOpenJob={onOpenJob} />
|
|
97
|
+
</MemoryRouter>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const row = view.container.querySelector("tbody tr");
|
|
101
|
+
expect(row).toBeTruthy();
|
|
102
|
+
expect(row?.getAttribute("tabindex")).toBe("0");
|
|
103
|
+
expect(row?.className).toContain("cursor-pointer");
|
|
104
|
+
|
|
105
|
+
fireEvent.click(row!);
|
|
106
|
+
expect(onOpenJob).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(onOpenJob).toHaveBeenCalledWith("job-1");
|
|
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
|
+
});
|