@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/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
/>
|
|
12
12
|
<title>Prompt Pipeline Dashboard</title>
|
|
13
13
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/style-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DN3-zvtP.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/style-CtZBnjlR.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -13,6 +13,20 @@ import { statusBadge } from "../../utils/ui";
|
|
|
13
13
|
import { formatCurrency4, formatTokensCompact } from "../../utils/formatters";
|
|
14
14
|
import type { JobDetail as JobDetailType } from "../components/types";
|
|
15
15
|
import { HintBanner } from "../components/onboarding";
|
|
16
|
+
import type { ApiError } from "../client/types";
|
|
17
|
+
|
|
18
|
+
function humanErrorMessage(error: ApiError): { title: string; detail: string } {
|
|
19
|
+
if (error.code === "malformed_response") {
|
|
20
|
+
return {
|
|
21
|
+
title: "Couldn't load job",
|
|
22
|
+
detail: "Server returned an unexpected response. Try again in a moment.",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
title: "Couldn't load job",
|
|
27
|
+
detail: error.message,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
export default function PipelineDetail() {
|
|
18
32
|
const { jobId } = useParams<{ jobId: string }>();
|
|
@@ -24,15 +38,19 @@ export default function PipelineDetail() {
|
|
|
24
38
|
return <Layout pageTitle="Pipeline Details"><div className="rounded-sm border-l-[3px] border-l-red-600 bg-red-100 p-4 text-red-700">No job ID provided</div></Layout>;
|
|
25
39
|
}
|
|
26
40
|
|
|
27
|
-
const { data: job, loading, error } = useJobDetailWithUpdates(jobId);
|
|
41
|
+
const { data: job, loading, error, refetch } = useJobDetailWithUpdates(jobId);
|
|
28
42
|
const breadcrumbs = [
|
|
29
43
|
{ label: "Home", href: "/" },
|
|
30
44
|
{ label: job?.pipelineLabel ?? "Pipeline Details" },
|
|
31
45
|
...(job?.name ? [{ label: job.name }] : []),
|
|
32
46
|
];
|
|
33
47
|
|
|
34
|
-
const totalCost =
|
|
48
|
+
const totalCost =
|
|
49
|
+
(job?.totalCost ?? job?.costsSummary?.totalCost ?? 0) +
|
|
50
|
+
(job?.costsSummary?.estimatedCost ?? 0) +
|
|
51
|
+
(job?.costsSummary?.unavailableCost ?? 0);
|
|
35
52
|
const totalTokens = job?.totalTokens ?? job?.costsSummary?.totalTokens ?? 0;
|
|
53
|
+
const hasEstimated = job?.costsSummary?.hasEstimated === true;
|
|
36
54
|
|
|
37
55
|
return (
|
|
38
56
|
<Layout
|
|
@@ -46,7 +64,8 @@ export default function PipelineDetail() {
|
|
|
46
64
|
<Tooltip.Root>
|
|
47
65
|
<Tooltip.Trigger asChild>
|
|
48
66
|
<button type="button" className="cursor-help border-b border-dotted border-gray-400 text-sm text-gray-500">
|
|
49
|
-
Cost: {
|
|
67
|
+
Cost: {hasEstimated ? <abbr title="Estimated cost" className="mr-0.5">≈</abbr> : null}
|
|
68
|
+
{formatCurrency4(totalCost)}
|
|
50
69
|
</button>
|
|
51
70
|
</Tooltip.Trigger>
|
|
52
71
|
<Tooltip.Portal>
|
|
@@ -73,7 +92,44 @@ export default function PipelineDetail() {
|
|
|
73
92
|
>
|
|
74
93
|
<HintBanner storageKey="pipeline-detail-hint" title="Steps run left to right." variant="info">Each step waits for its upstream dependencies to finish. Click any step card to see its output.</HintBanner>
|
|
75
94
|
{loading && !job ? <div className="rounded-md border border-gray-200 bg-white p-10 text-center text-gray-500">Loading job details...</div> : null}
|
|
76
|
-
{error
|
|
95
|
+
{error && !job ? (
|
|
96
|
+
<section
|
|
97
|
+
role="alert"
|
|
98
|
+
aria-labelledby="pipeline-detail-error-title"
|
|
99
|
+
data-testid="pipeline-detail-error"
|
|
100
|
+
className="mb-4 rounded-sm border-l-[3px] border-l-red-600 bg-red-50 p-4 text-sm text-red-800"
|
|
101
|
+
>
|
|
102
|
+
<h2 id="pipeline-detail-error-title" className="mb-1 font-semibold text-red-900">
|
|
103
|
+
{humanErrorMessage(error).title}
|
|
104
|
+
</h2>
|
|
105
|
+
<p className="mb-3 text-red-800">{humanErrorMessage(error).detail}</p>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => refetch()}
|
|
109
|
+
aria-label="Retry loading job"
|
|
110
|
+
className="inline-flex items-center rounded-md border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-600 focus-visible:ring-offset-2"
|
|
111
|
+
>
|
|
112
|
+
Retry loading job
|
|
113
|
+
</button>
|
|
114
|
+
</section>
|
|
115
|
+
) : null}
|
|
116
|
+
{error && job ? (
|
|
117
|
+
<div
|
|
118
|
+
role="status"
|
|
119
|
+
data-testid="pipeline-detail-stale"
|
|
120
|
+
className="mb-4 flex flex-wrap items-center gap-3 rounded-sm border-l-[3px] border-l-yellow-600 bg-yellow-50 p-3 text-sm text-yellow-800"
|
|
121
|
+
>
|
|
122
|
+
<span>Showing last known job state. {humanErrorMessage(error).detail}</span>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={() => refetch()}
|
|
126
|
+
aria-label="Retry loading job"
|
|
127
|
+
className="inline-flex items-center rounded-md border border-yellow-300 bg-white px-3 py-1.5 text-sm font-medium text-yellow-700 hover:bg-yellow-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-600 focus-visible:ring-offset-2"
|
|
128
|
+
>
|
|
129
|
+
Retry loading job
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
) : null}
|
|
77
133
|
{!loading && !error && !job ? <div className="rounded-xl border border-gray-200 bg-white p-4 text-gray-700">Job not found</div> : null}
|
|
78
134
|
{job ? <JobDetail job={job as unknown as JobDetailType} pipeline={{ name: job.pipelineLabel ?? job.pipeline ?? "", slug: job.pipeline ?? "", description: "", tasks: (
|
|
79
135
|
Array.isArray(job.pipelineConfig?.tasks)
|
|
@@ -8,7 +8,7 @@ import Layout from "../components/Layout";
|
|
|
8
8
|
import type { JobSummary } from "../components/types";
|
|
9
9
|
import { Progress } from "../components/ui/Progress";
|
|
10
10
|
import { HintBanner } from "../components/onboarding";
|
|
11
|
-
import type { JobConcurrencyApiStatus } from "../client/types";
|
|
11
|
+
import type { ApiError, JobConcurrencyApiStatus } from "../client/types";
|
|
12
12
|
|
|
13
13
|
type TabKey = "current" | "errors" | "complete" | "concurrency";
|
|
14
14
|
|
|
@@ -17,6 +17,7 @@ const STALE_REASON_LABELS: Record<JobConcurrencyApiStatus["staleSlots"][number][
|
|
|
17
17
|
missing_pid: "Lease missing PID past timeout",
|
|
18
18
|
dead_pid: "Process no longer running",
|
|
19
19
|
invalid_json: "Lease file is not valid JSON",
|
|
20
|
+
stale_runner: "Live PID no longer matches runner file",
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
function formatTimestamp(value: string | null | undefined): string {
|
|
@@ -26,6 +27,19 @@ function formatTimestamp(value: string | null | undefined): string {
|
|
|
26
27
|
return new Date(ms).toLocaleString();
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function humanErrorMessage(error: ApiError): { title: string; detail: string } {
|
|
31
|
+
if (error.code === "malformed_response") {
|
|
32
|
+
return {
|
|
33
|
+
title: "Couldn't load jobs",
|
|
34
|
+
detail: "Server returned an unexpected response. Try again in a moment.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
title: "Couldn't load jobs",
|
|
39
|
+
detail: error.message,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
function CapacityMetrics({ status }: { status: JobConcurrencyApiStatus }) {
|
|
30
44
|
const cells: Array<{ label: string; value: number }> = [
|
|
31
45
|
{ label: "Limit", value: status.limit },
|
|
@@ -178,11 +192,10 @@ export default function PromptPipelineDashboard() {
|
|
|
178
192
|
const { data: apiJobs, error } = hookResult;
|
|
179
193
|
const [activeTab, setActiveTab] = useState<TabKey>("current");
|
|
180
194
|
|
|
181
|
-
const jobs = useMemo<JobSummary[]>(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}, [apiJobs, error]);
|
|
195
|
+
const jobs = useMemo<JobSummary[]>(
|
|
196
|
+
() => (Array.isArray(apiJobs) ? (apiJobs as unknown as JobSummary[]) : []),
|
|
197
|
+
[apiJobs],
|
|
198
|
+
);
|
|
186
199
|
|
|
187
200
|
const grouped = useMemo(
|
|
188
201
|
() => ({
|
|
@@ -197,6 +210,8 @@ export default function PromptPipelineDashboard() {
|
|
|
197
210
|
const aggregateProgress =
|
|
198
211
|
runningJobs.length === 0 ? 0 : Math.round(runningJobs.reduce((sum, job) => sum + job.progress, 0) / runningJobs.length);
|
|
199
212
|
|
|
213
|
+
const errorMessage = error ? humanErrorMessage(error) : null;
|
|
214
|
+
|
|
200
215
|
const tabLabels: Record<TabKey, string> = {
|
|
201
216
|
current: "Current",
|
|
202
217
|
errors: "Errors",
|
|
@@ -228,7 +243,44 @@ export default function PromptPipelineDashboard() {
|
|
|
228
243
|
) : null
|
|
229
244
|
}
|
|
230
245
|
>
|
|
231
|
-
{
|
|
246
|
+
{errorMessage && apiJobs === null ? (
|
|
247
|
+
<section
|
|
248
|
+
role="alert"
|
|
249
|
+
aria-labelledby="dashboard-error-title"
|
|
250
|
+
data-testid="dashboard-error"
|
|
251
|
+
className="mb-4 rounded-sm border-l-[3px] border-l-red-600 bg-red-50 p-4 text-sm text-red-800"
|
|
252
|
+
>
|
|
253
|
+
<h2 id="dashboard-error-title" className="mb-1 font-semibold text-red-900">
|
|
254
|
+
{errorMessage.title}
|
|
255
|
+
</h2>
|
|
256
|
+
<p className="mb-3 text-red-800">{errorMessage.detail}</p>
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
onClick={() => hookResult.refetch()}
|
|
260
|
+
aria-label="Retry loading jobs"
|
|
261
|
+
className="inline-flex items-center rounded-md border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-600 focus-visible:ring-offset-2"
|
|
262
|
+
>
|
|
263
|
+
Retry loading jobs
|
|
264
|
+
</button>
|
|
265
|
+
</section>
|
|
266
|
+
) : null}
|
|
267
|
+
{errorMessage && apiJobs !== null ? (
|
|
268
|
+
<div
|
|
269
|
+
role="status"
|
|
270
|
+
data-testid="dashboard-stale"
|
|
271
|
+
className="mb-4 flex flex-wrap items-center gap-3 rounded-sm border-l-[3px] border-l-yellow-600 bg-yellow-50 p-3 text-sm text-yellow-800"
|
|
272
|
+
>
|
|
273
|
+
<span>Showing last known jobs. {errorMessage.detail}</span>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
onClick={() => hookResult.refetch()}
|
|
277
|
+
aria-label="Retry loading jobs"
|
|
278
|
+
className="inline-flex items-center rounded-md border border-yellow-300 bg-white px-3 py-1.5 text-sm font-medium text-yellow-700 hover:bg-yellow-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-600 focus-visible:ring-offset-2"
|
|
279
|
+
>
|
|
280
|
+
Retry loading jobs
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
) : null}
|
|
232
284
|
<HintBanner storageKey="dashboard-hint" title="Upload a seed file to get started." variant="action">Your first pipeline takes about 2 minutes to complete.</HintBanner>
|
|
233
285
|
<div className="mb-4 flex flex-wrap gap-2">
|
|
234
286
|
{(["current", "errors", "complete", "concurrency"] as const).map((tab) => (
|
|
@@ -4,43 +4,64 @@ import { act, fireEvent, render, waitFor } from "@testing-library/react";
|
|
|
4
4
|
import { MemoryRouter } from "react-router-dom";
|
|
5
5
|
import { afterEach, beforeEach, expect, mock, test } from "bun:test";
|
|
6
6
|
|
|
7
|
-
import type { JobConcurrencyApiStatus } from "../../client/types";
|
|
7
|
+
import type { ApiError, JobConcurrencyApiStatus } from "../../client/types";
|
|
8
8
|
|
|
9
9
|
const originalLocalStorage = globalThis.localStorage;
|
|
10
10
|
const originalFetch = globalThis.fetch;
|
|
11
11
|
const originalEventSource = globalThis.EventSource;
|
|
12
12
|
|
|
13
|
+
type HookError = ApiError;
|
|
14
|
+
|
|
15
|
+
type HookState = {
|
|
16
|
+
data: unknown[] | null;
|
|
17
|
+
error: HookError | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const defaultJobs: unknown[] = [
|
|
21
|
+
{
|
|
22
|
+
id: "job-1",
|
|
23
|
+
jobId: "job-1",
|
|
24
|
+
name: "Job One",
|
|
25
|
+
status: "running",
|
|
26
|
+
progress: 50,
|
|
27
|
+
taskCount: 2,
|
|
28
|
+
doneCount: 1,
|
|
29
|
+
location: "current",
|
|
30
|
+
tasks: { task1: { name: "task1", state: "running", files: { artifacts: [], logs: [], tmp: [] } } },
|
|
31
|
+
current: "task1",
|
|
32
|
+
displayCategory: "current",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "job-2",
|
|
36
|
+
jobId: "job-2",
|
|
37
|
+
name: "Job Two",
|
|
38
|
+
status: "failed",
|
|
39
|
+
progress: 100,
|
|
40
|
+
taskCount: 1,
|
|
41
|
+
doneCount: 1,
|
|
42
|
+
location: "current",
|
|
43
|
+
tasks: {},
|
|
44
|
+
current: null,
|
|
45
|
+
displayCategory: "errors",
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
let hookState: HookState = { data: defaultJobs, error: null };
|
|
50
|
+
let refetchCalls = 0;
|
|
51
|
+
|
|
52
|
+
function setHookState(next: HookState): void {
|
|
53
|
+
hookState = next;
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
mock.module("../../client/hooks/useJobListWithUpdates", () => ({
|
|
14
57
|
useJobListWithUpdates: () => ({
|
|
15
|
-
data:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
taskCount: 2,
|
|
23
|
-
doneCount: 1,
|
|
24
|
-
location: "current",
|
|
25
|
-
tasks: { task1: { name: "task1", state: "running", files: { artifacts: [], logs: [], tmp: [] } } },
|
|
26
|
-
current: "task1",
|
|
27
|
-
displayCategory: "current",
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: "job-2",
|
|
31
|
-
jobId: "job-2",
|
|
32
|
-
name: "Job Two",
|
|
33
|
-
status: "failed",
|
|
34
|
-
progress: 100,
|
|
35
|
-
taskCount: 1,
|
|
36
|
-
doneCount: 1,
|
|
37
|
-
location: "current",
|
|
38
|
-
tasks: {},
|
|
39
|
-
current: null,
|
|
40
|
-
displayCategory: "errors",
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
error: null,
|
|
58
|
+
data: hookState.data,
|
|
59
|
+
error: hookState.error,
|
|
60
|
+
loading: false,
|
|
61
|
+
refetch: () => {
|
|
62
|
+
refetchCalls += 1;
|
|
63
|
+
},
|
|
64
|
+
connectionStatus: "connected",
|
|
44
65
|
}),
|
|
45
66
|
}));
|
|
46
67
|
|
|
@@ -98,7 +119,7 @@ function installConcurrencyFetch(): void {
|
|
|
98
119
|
}
|
|
99
120
|
|
|
100
121
|
beforeEach(() => {
|
|
101
|
-
const store: Record<string, string> = {};
|
|
122
|
+
const store: Record<string, string> = { hasSeenWelcome: "1" };
|
|
102
123
|
globalThis.localStorage = {
|
|
103
124
|
getItem: (key: string) => store[key] ?? null,
|
|
104
125
|
setItem: (key: string, value: string) => { store[key] = value; },
|
|
@@ -107,6 +128,8 @@ beforeEach(() => {
|
|
|
107
128
|
get length() { return Object.keys(store).length; },
|
|
108
129
|
key: (i: number) => Object.keys(store)[i] ?? null,
|
|
109
130
|
} as Storage;
|
|
131
|
+
setHookState({ data: defaultJobs, error: null });
|
|
132
|
+
refetchCalls = 0;
|
|
110
133
|
setConcurrency(emptyStatus());
|
|
111
134
|
MockEventSource.reset();
|
|
112
135
|
installConcurrencyFetch();
|
|
@@ -249,6 +272,7 @@ test("Concurrency tab renders stale slot warnings when present", async () => {
|
|
|
249
272
|
staleSlots: [
|
|
250
273
|
{ jobId: "stale-1", reason: "dead_pid" },
|
|
251
274
|
{ jobId: "stale-2", reason: "invalid_json" },
|
|
275
|
+
{ jobId: "stale-3", reason: "stale_runner" },
|
|
252
276
|
],
|
|
253
277
|
});
|
|
254
278
|
const { default: PromptPipelineDashboard } = await import("../PromptPipelineDashboard");
|
|
@@ -260,11 +284,13 @@ test("Concurrency tab renders stale slot warnings when present", async () => {
|
|
|
260
284
|
|
|
261
285
|
fireEvent.click(view.getByText("Concurrency"));
|
|
262
286
|
|
|
263
|
-
await waitFor(() => expect(view.getByText(/Stale slots \(
|
|
287
|
+
await waitFor(() => expect(view.getByText(/Stale slots \(3\)/)).toBeTruthy());
|
|
264
288
|
expect(view.getByText("stale-1")).toBeTruthy();
|
|
265
289
|
expect(view.getByText("Process no longer running")).toBeTruthy();
|
|
266
290
|
expect(view.getByText("stale-2")).toBeTruthy();
|
|
267
291
|
expect(view.getByText("Lease file is not valid JSON")).toBeTruthy();
|
|
292
|
+
expect(view.getByText("stale-3")).toBeTruthy();
|
|
293
|
+
expect(view.getByText("Live PID no longer matches runner file")).toBeTruthy();
|
|
268
294
|
});
|
|
269
295
|
|
|
270
296
|
test("Concurrency tab updates after an SSE refresh without changing tabs", async () => {
|
|
@@ -340,3 +366,59 @@ test("Concurrency tab shows stale data when live updates disconnect", async () =
|
|
|
340
366
|
await waitFor(() => expect(view.getByText(/Showing last known concurrency status/)).toBeTruthy());
|
|
341
367
|
expect(view.getByText("active-before-error")).toBeTruthy();
|
|
342
368
|
});
|
|
369
|
+
|
|
370
|
+
test("PromptPipelineDashboard renders an announced error state with a working retry when the list read fails", async () => {
|
|
371
|
+
setHookState({
|
|
372
|
+
data: null,
|
|
373
|
+
error: { code: "unknown_error", message: "The server failed to process the request", status: 500 },
|
|
374
|
+
});
|
|
375
|
+
const { default: PromptPipelineDashboard } = await import("../PromptPipelineDashboard");
|
|
376
|
+
const view = render(
|
|
377
|
+
<MemoryRouter>
|
|
378
|
+
<PromptPipelineDashboard />
|
|
379
|
+
</MemoryRouter>,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const alert = view.getByRole("alert");
|
|
383
|
+
expect(alert).toBeTruthy();
|
|
384
|
+
expect(view.getByText(/The server failed to process the request/)).toBeTruthy();
|
|
385
|
+
|
|
386
|
+
const retry = view.getByRole("button", { name: "Retry loading jobs" });
|
|
387
|
+
expect(refetchCalls).toBe(0);
|
|
388
|
+
fireEvent.click(retry);
|
|
389
|
+
expect(refetchCalls).toBe(1);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("PromptPipelineDashboard renders a distinct first-use empty state when the read returns no jobs", async () => {
|
|
393
|
+
setHookState({ data: [], error: null });
|
|
394
|
+
const { default: PromptPipelineDashboard } = await import("../PromptPipelineDashboard");
|
|
395
|
+
const view = render(
|
|
396
|
+
<MemoryRouter>
|
|
397
|
+
<PromptPipelineDashboard />
|
|
398
|
+
</MemoryRouter>,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(view.queryByRole("alert")).toBeNull();
|
|
402
|
+
expect(view.getByText(/No pipelines yet/)).toBeTruthy();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("PromptPipelineDashboard shows a human message for malformed_response and never the raw code", async () => {
|
|
406
|
+
setHookState({
|
|
407
|
+
data: null,
|
|
408
|
+
error: { code: "malformed_response", message: "Malformed response", status: 200 },
|
|
409
|
+
});
|
|
410
|
+
const { default: PromptPipelineDashboard } = await import("../PromptPipelineDashboard");
|
|
411
|
+
const view = render(
|
|
412
|
+
<MemoryRouter>
|
|
413
|
+
<PromptPipelineDashboard />
|
|
414
|
+
</MemoryRouter>,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const alert = view.getByRole("alert");
|
|
418
|
+
expect(alert).toBeTruthy();
|
|
419
|
+
expect(view.getByText(/Server returned an unexpected response/)).toBeTruthy();
|
|
420
|
+
|
|
421
|
+
const alertText = alert.textContent ?? "";
|
|
422
|
+
expect(alertText.includes("malformed_response")).toBe(false);
|
|
423
|
+
expect(view.queryByText(/malformed_response/)).toBeNull();
|
|
424
|
+
});
|