@shipfox/client-workflows 0.2.0
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/.storybook/main.ts +1 -0
- package/.storybook/preview.css +10 -0
- package/.storybook/preview.tsx +97 -0
- package/.swcrc +42 -0
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +162 -0
- package/LICENSE +21 -0
- package/dist/components/identifier/identifier.d.ts +6 -0
- package/dist/components/identifier/identifier.d.ts.map +1 -0
- package/dist/components/identifier/identifier.js +144 -0
- package/dist/components/identifier/identifier.js.map +1 -0
- package/dist/components/identifier/index.d.ts +2 -0
- package/dist/components/identifier/index.d.ts.map +1 -0
- package/dist/components/identifier/index.js +3 -0
- package/dist/components/identifier/index.js.map +1 -0
- package/dist/components/job-graph/graph-model.d.ts +27 -0
- package/dist/components/job-graph/graph-model.d.ts.map +1 -0
- package/dist/components/job-graph/graph-model.js +107 -0
- package/dist/components/job-graph/graph-model.js.map +1 -0
- package/dist/components/job-graph/index.d.ts +3 -0
- package/dist/components/job-graph/index.d.ts.map +1 -0
- package/dist/components/job-graph/index.js +3 -0
- package/dist/components/job-graph/index.js.map +1 -0
- package/dist/components/job-graph/job-duration-format.d.ts +4 -0
- package/dist/components/job-graph/job-duration-format.d.ts.map +1 -0
- package/dist/components/job-graph/job-duration-format.js +41 -0
- package/dist/components/job-graph/job-duration-format.js.map +1 -0
- package/dist/components/job-graph/job-duration-label.d.ts +5 -0
- package/dist/components/job-graph/job-duration-label.d.ts.map +1 -0
- package/dist/components/job-graph/job-duration-label.js +33 -0
- package/dist/components/job-graph/job-duration-label.js.map +1 -0
- package/dist/components/job-graph/job-graph-content.d.ts +9 -0
- package/dist/components/job-graph/job-graph-content.d.ts.map +1 -0
- package/dist/components/job-graph/job-graph-content.js +181 -0
- package/dist/components/job-graph/job-graph-content.js.map +1 -0
- package/dist/components/job-graph/job-graph-view.d.ts +11 -0
- package/dist/components/job-graph/job-graph-view.d.ts.map +1 -0
- package/dist/components/job-graph/job-graph-view.js +24 -0
- package/dist/components/job-graph/job-graph-view.js.map +1 -0
- package/dist/components/job-graph/job-graph.d.ts +3 -0
- package/dist/components/job-graph/job-graph.d.ts.map +1 -0
- package/dist/components/job-graph/job-graph.js +21 -0
- package/dist/components/job-graph/job-graph.js.map +1 -0
- package/dist/components/job-graph/job-node.d.ts +16 -0
- package/dist/components/job-graph/job-node.d.ts.map +1 -0
- package/dist/components/job-graph/job-node.js +196 -0
- package/dist/components/job-graph/job-node.js.map +1 -0
- package/dist/components/job-graph/types.d.ts +9 -0
- package/dist/components/job-graph/types.d.ts.map +1 -0
- package/dist/components/job-graph/types.js +3 -0
- package/dist/components/job-graph/types.js.map +1 -0
- package/dist/components/step-list/index.d.ts +3 -0
- package/dist/components/step-list/index.d.ts.map +1 -0
- package/dist/components/step-list/index.js +3 -0
- package/dist/components/step-list/index.js.map +1 -0
- package/dist/components/step-list/step-list-model.d.ts +36 -0
- package/dist/components/step-list/step-list-model.d.ts.map +1 -0
- package/dist/components/step-list/step-list-model.js +135 -0
- package/dist/components/step-list/step-list-model.js.map +1 -0
- package/dist/components/step-list/step-list.d.ts +32 -0
- package/dist/components/step-list/step-list.d.ts.map +1 -0
- package/dist/components/step-list/step-list.js +409 -0
- package/dist/components/step-list/step-list.js.map +1 -0
- package/dist/components/workflow-run-duration-label.d.ts +8 -0
- package/dist/components/workflow-run-duration-label.d.ts.map +1 -0
- package/dist/components/workflow-run-duration-label.js +84 -0
- package/dist/components/workflow-run-duration-label.js.map +1 -0
- package/dist/components/workflow-run-list/index.d.ts +2 -0
- package/dist/components/workflow-run-list/index.d.ts.map +1 -0
- package/dist/components/workflow-run-list/index.js +3 -0
- package/dist/components/workflow-run-list/index.js.map +1 -0
- package/dist/components/workflow-run-list/run-display.d.ts +5 -0
- package/dist/components/workflow-run-list/run-display.d.ts.map +1 -0
- package/dist/components/workflow-run-list/run-display.js +19 -0
- package/dist/components/workflow-run-list/run-display.js.map +1 -0
- package/dist/components/workflow-run-list/types.d.ts +21 -0
- package/dist/components/workflow-run-list/types.d.ts.map +1 -0
- package/dist/components/workflow-run-list/types.js +3 -0
- package/dist/components/workflow-run-list/types.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +14 -0
- package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-content.js +37 -0
- package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +10 -0
- package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-header.js +55 -0
- package/dist/components/workflow-run-list/workflow-run-list-header.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +15 -0
- package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -0
- package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +3 -0
- package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-view.js +46 -0
- package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list.d.ts +3 -0
- package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list.js +20 -0
- package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-row.d.ts +14 -0
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-row.js +172 -0
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -0
- package/dist/components/workflow-run-summary/index.d.ts +2 -0
- package/dist/components/workflow-run-summary/index.d.ts.map +1 -0
- package/dist/components/workflow-run-summary/index.js +3 -0
- package/dist/components/workflow-run-summary/index.js.map +1 -0
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +9 -0
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -0
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +138 -0
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -0
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +20 -0
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -0
- package/dist/components/workflow-run-summary/workflow-run-summary.js +252 -0
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -0
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +7 -0
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -0
- package/dist/components/workflow-run-view/agent-config-failure-callout.js +84 -0
- package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -0
- package/dist/components/workflow-run-view/index.d.ts +3 -0
- package/dist/components/workflow-run-view/index.d.ts.map +1 -0
- package/dist/components/workflow-run-view/index.js +4 -0
- package/dist/components/workflow-run-view/index.js.map +1 -0
- package/dist/components/workflow-run-view/job-card.d.ts +17 -0
- package/dist/components/workflow-run-view/job-card.d.ts.map +1 -0
- package/dist/components/workflow-run-view/job-card.js +476 -0
- package/dist/components/workflow-run-view/job-card.js.map +1 -0
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts +10 -0
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -0
- package/dist/components/workflow-run-view/job-execution-switcher.js +171 -0
- package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -0
- package/dist/components/workflow-run-view/job-execution-time-text.d.ts +6 -0
- package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +1 -0
- package/dist/components/workflow-run-view/job-execution-time-text.js +24 -0
- package/dist/components/workflow-run-view/job-execution-time-text.js.map +1 -0
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +9 -0
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +1 -0
- package/dist/components/workflow-run-view/step-attempt-log-panel.js +152 -0
- package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-selection.d.ts +14 -0
- package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-selection.js +56 -0
- package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-states.d.ts +12 -0
- package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-states.js +91 -0
- package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +15 -0
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-view.js +285 -0
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -0
- package/dist/components/workflow-source-panel/index.d.ts +2 -0
- package/dist/components/workflow-source-panel/index.d.ts.map +1 -0
- package/dist/components/workflow-source-panel/index.js +3 -0
- package/dist/components/workflow-source-panel/index.js.map +1 -0
- package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +13 -0
- package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -0
- package/dist/components/workflow-source-panel/workflow-source-panel.js +91 -0
- package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -0
- package/dist/components/workflow-status/status-visuals.d.ts +12 -0
- package/dist/components/workflow-status/status-visuals.d.ts.map +1 -0
- package/dist/components/workflow-status/status-visuals.js +54 -0
- package/dist/components/workflow-status/status-visuals.js.map +1 -0
- package/dist/components/workflow-status/workflow-status-icon.d.ts +22 -0
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -0
- package/dist/components/workflow-status/workflow-status-icon.js +100 -0
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -0
- package/dist/core/entities/job-execution.d.ts +69 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -0
- package/dist/core/entities/job-execution.js +83 -0
- package/dist/core/entities/job-execution.js.map +1 -0
- package/dist/core/entities/job.d.ts +56 -0
- package/dist/core/entities/job.d.ts.map +1 -0
- package/dist/core/entities/job.js +71 -0
- package/dist/core/entities/job.js.map +1 -0
- package/dist/core/entities/step-attempt.d.ts +49 -0
- package/dist/core/entities/step-attempt.d.ts.map +1 -0
- package/dist/core/entities/step-attempt.js +48 -0
- package/dist/core/entities/step-attempt.js.map +1 -0
- package/dist/core/entities/step.d.ts +41 -0
- package/dist/core/entities/step.d.ts.map +1 -0
- package/dist/core/entities/step.js +51 -0
- package/dist/core/entities/step.js.map +1 -0
- package/dist/core/entities/workflow-run-attempt.d.ts +44 -0
- package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -0
- package/dist/core/entities/workflow-run-attempt.js +51 -0
- package/dist/core/entities/workflow-run-attempt.js.map +1 -0
- package/dist/core/entities/workflow-run.d.ts +61 -0
- package/dist/core/entities/workflow-run.d.ts.map +1 -0
- package/dist/core/entities/workflow-run.js +104 -0
- package/dist/core/entities/workflow-run.js.map +1 -0
- package/dist/core/workflow-run-url-state.d.ts +13 -0
- package/dist/core/workflow-run-url-state.d.ts.map +1 -0
- package/dist/core/workflow-run-url-state.js +43 -0
- package/dist/core/workflow-run-url-state.js.map +1 -0
- package/dist/core/workflow-run.d.ts +13 -0
- package/dist/core/workflow-run.d.ts.map +1 -0
- package/dist/core/workflow-run.js +8 -0
- package/dist/core/workflow-run.js.map +1 -0
- package/dist/env.d.js +2 -0
- package/dist/env.d.js.map +1 -0
- package/dist/feature.d.ts +20 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/feature.js +27 -0
- package/dist/feature.js.map +1 -0
- package/dist/hooks/api/workflow-runs.d.ts +109 -0
- package/dist/hooks/api/workflow-runs.d.ts.map +1 -0
- package/dist/hooks/api/workflow-runs.js +394 -0
- package/dist/hooks/api/workflow-runs.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/workflow-run-first-time-use.d.ts +6 -0
- package/dist/pages/workflow-run-first-time-use.d.ts.map +1 -0
- package/dist/pages/workflow-run-first-time-use.js +17 -0
- package/dist/pages/workflow-run-first-time-use.js.map +1 -0
- package/dist/pages/workflow-run-page.d.ts +8 -0
- package/dist/pages/workflow-run-page.d.ts.map +1 -0
- package/dist/pages/workflow-run-page.js +83 -0
- package/dist/pages/workflow-run-page.js.map +1 -0
- package/dist/routes/run-detail.d.ts +8 -0
- package/dist/routes/run-detail.d.ts.map +1 -0
- package/dist/routes/run-detail.js +21 -0
- package/dist/routes/run-detail.js.map +1 -0
- package/dist/routes/runs.d.ts +8 -0
- package/dist/routes/runs.d.ts.map +1 -0
- package/dist/routes/runs.js +20 -0
- package/dist/routes/runs.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +114 -0
- package/src/components/identifier/identifier.tsx +178 -0
- package/src/components/identifier/index.ts +1 -0
- package/src/components/job-graph/graph-model.test.ts +189 -0
- package/src/components/job-graph/graph-model.ts +178 -0
- package/src/components/job-graph/index.ts +2 -0
- package/src/components/job-graph/job-duration-format.ts +54 -0
- package/src/components/job-graph/job-duration-label.tsx +29 -0
- package/src/components/job-graph/job-graph-content.tsx +225 -0
- package/src/components/job-graph/job-graph-view.test.tsx +298 -0
- package/src/components/job-graph/job-graph-view.tsx +45 -0
- package/src/components/job-graph/job-graph.stories.tsx +182 -0
- package/src/components/job-graph/job-graph.tsx +24 -0
- package/src/components/job-graph/job-node.stories.tsx +364 -0
- package/src/components/job-graph/job-node.test.tsx +347 -0
- package/src/components/job-graph/job-node.tsx +202 -0
- package/src/components/job-graph/types.ts +9 -0
- package/src/components/step-list/index.ts +6 -0
- package/src/components/step-list/step-list-model.test.ts +369 -0
- package/src/components/step-list/step-list-model.ts +205 -0
- package/src/components/step-list/step-list.stories.tsx +419 -0
- package/src/components/step-list/step-list.test.tsx +566 -0
- package/src/components/step-list/step-list.tsx +494 -0
- package/src/components/workflow-run-duration-label.tsx +118 -0
- package/src/components/workflow-run-list/index.ts +1 -0
- package/src/components/workflow-run-list/run-display.test.ts +53 -0
- package/src/components/workflow-run-list/run-display.ts +22 -0
- package/src/components/workflow-run-list/types.ts +22 -0
- package/src/components/workflow-run-list/workflow-run-list-content.tsx +58 -0
- package/src/components/workflow-run-list/workflow-run-list-header.tsx +53 -0
- package/src/components/workflow-run-list/workflow-run-list-states.tsx +86 -0
- package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +178 -0
- package/src/components/workflow-run-list/workflow-run-list-view.tsx +57 -0
- package/src/components/workflow-run-list/workflow-run-list.stories.tsx +111 -0
- package/src/components/workflow-run-list/workflow-run-list.tsx +29 -0
- package/src/components/workflow-run-list/workflow-run-row.tsx +176 -0
- package/src/components/workflow-run-summary/index.ts +1 -0
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +230 -0
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +146 -0
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +414 -0
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +354 -0
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +295 -0
- package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +140 -0
- package/src/components/workflow-run-view/agent-config-failure-callout.tsx +93 -0
- package/src/components/workflow-run-view/index.ts +2 -0
- package/src/components/workflow-run-view/job-card.stories.tsx +788 -0
- package/src/components/workflow-run-view/job-card.tsx +566 -0
- package/src/components/workflow-run-view/job-execution-switcher.tsx +170 -0
- package/src/components/workflow-run-view/job-execution-time-text.tsx +34 -0
- package/src/components/workflow-run-view/step-attempt-log-panel.test.tsx +290 -0
- package/src/components/workflow-run-view/step-attempt-log-panel.tsx +152 -0
- package/src/components/workflow-run-view/workflow-run-selection.test.ts +280 -0
- package/src/components/workflow-run-view/workflow-run-selection.ts +89 -0
- package/src/components/workflow-run-view/workflow-run-states.tsx +70 -0
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +1214 -0
- package/src/components/workflow-run-view/workflow-run-view.tsx +341 -0
- package/src/components/workflow-source-panel/index.ts +1 -0
- package/src/components/workflow-source-panel/workflow-source-panel.stories.tsx +110 -0
- package/src/components/workflow-source-panel/workflow-source-panel.test.tsx +96 -0
- package/src/components/workflow-source-panel/workflow-source-panel.tsx +136 -0
- package/src/components/workflow-status/status-visuals.test.ts +38 -0
- package/src/components/workflow-status/status-visuals.ts +36 -0
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +90 -0
- package/src/components/workflow-status/workflow-status-icon.tsx +135 -0
- package/src/core/entities/job-execution.ts +139 -0
- package/src/core/entities/job.ts +149 -0
- package/src/core/entities/step-attempt.ts +89 -0
- package/src/core/entities/step.ts +105 -0
- package/src/core/entities/workflow-run-attempt.ts +87 -0
- package/src/core/entities/workflow-run.ts +183 -0
- package/src/core/workflow-run-url-state.ts +61 -0
- package/src/core/workflow-run.test.ts +640 -0
- package/src/core/workflow-run.ts +66 -0
- package/src/env.d.ts +8 -0
- package/src/feature.ts +26 -0
- package/src/hooks/api/workflow-runs.test.tsx +410 -0
- package/src/hooks/api/workflow-runs.ts +485 -0
- package/src/index.ts +45 -0
- package/src/page-harness-budget.test.ts +33 -0
- package/src/pages/workflow-run-first-time-use.tsx +17 -0
- package/src/pages/workflow-run-page.test.tsx +517 -0
- package/src/pages/workflow-run-page.tsx +89 -0
- package/src/routes/run-detail.tsx +15 -0
- package/src/routes/runs.tsx +11 -0
- package/test/fixtures/logs.ts +13 -0
- package/test/fixtures/workflow-run.ts +311 -0
- package/test/pages.tsx +79 -0
- package/test/render.tsx +40 -0
- package/test/setup.ts +3 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vercel.json +8 -0
- package/vitest.config.ts +70 -0
|
@@ -0,0 +1,1214 @@
|
|
|
1
|
+
import type {WorkflowRunDetailResponseDto} from '@shipfox/api-workflows-dto';
|
|
2
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
3
|
+
import {toast} from '@shipfox/react-ui/toast';
|
|
4
|
+
import {screen, waitFor, within} from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import {workflowRunsQueryKeys} from '#hooks/api/workflow-runs.js';
|
|
7
|
+
import {inlineLogBody, outputLine} from '#test/fixtures/logs.js';
|
|
8
|
+
import {
|
|
9
|
+
runAttemptsResponseDto,
|
|
10
|
+
workflowJobDto,
|
|
11
|
+
workflowJobExecutionDto,
|
|
12
|
+
workflowRunAttemptDto,
|
|
13
|
+
workflowRunDetailDto,
|
|
14
|
+
workflowRunDto,
|
|
15
|
+
workflowStepAttemptDto,
|
|
16
|
+
workflowStepDto,
|
|
17
|
+
} from '#test/fixtures/workflow-run.js';
|
|
18
|
+
import {jsonResponse, PROJECT_TEST_WID, renderProjectPage} from '#test/pages.js';
|
|
19
|
+
import {WorkflowRunView} from './workflow-run-view.js';
|
|
20
|
+
|
|
21
|
+
const RUN_ID = '66666666-6666-4666-8666-666666666666';
|
|
22
|
+
const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
|
|
23
|
+
const DEFINITION_ID = '55555555-5555-4555-8555-555555555555';
|
|
24
|
+
const BUILD_JOB_ID = '77777777-7777-4777-8777-777777777777';
|
|
25
|
+
const DEPLOY_JOB_ID = '88888888-8888-4888-8888-888888888888';
|
|
26
|
+
const CHECKOUT_STEP_ID = '99999999-9999-4999-8999-999999999999';
|
|
27
|
+
const CHECKOUT_ATTEMPT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
28
|
+
const RERUN_BUTTON_NAME = /^Re-run/;
|
|
29
|
+
const ATTEMPT_2_PATTERN = /Attempt 2/;
|
|
30
|
+
const COPY_RUN_BUTTON_NAME = /Copy run/;
|
|
31
|
+
const EXECUTION_ONE_MENU_ITEM_PATTERN = /#1/;
|
|
32
|
+
|
|
33
|
+
describe('WorkflowRunView', () => {
|
|
34
|
+
test('renders the run summary, jobs graph, and selected job step attempts when a run loads', async () => {
|
|
35
|
+
configureApiClient({
|
|
36
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
renderView();
|
|
40
|
+
|
|
41
|
+
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
42
|
+
|
|
43
|
+
expect(within(summary).getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
|
|
44
|
+
expect(within(summary).getAllByText('Running')).not.toHaveLength(0);
|
|
45
|
+
expect(within(summary).getByText('fire')).toBeInTheDocument();
|
|
46
|
+
expect(
|
|
47
|
+
within(summary).queryByRole('button', {name: COPY_RUN_BUTTON_NAME}),
|
|
48
|
+
).not.toBeInTheDocument();
|
|
49
|
+
expect(await screen.findByRole('region', {name: 'Workflow jobs'})).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByRole('button', {name: 'build, Succeeded'})).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByRole('button', {name: 'deploy, Running'})).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByRole('region', {name: 'build'})).toBeInTheDocument();
|
|
53
|
+
expect(
|
|
54
|
+
screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}),
|
|
55
|
+
).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('renders active step attempt logs inline when the selected job is running', async () => {
|
|
59
|
+
const stepId = '99999999-9999-4999-8999-000000000001';
|
|
60
|
+
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
61
|
+
const url = requestUrl(input);
|
|
62
|
+
if (url.pathname === `/steps/${stepId}/attempts/1/logs`) {
|
|
63
|
+
return Promise.resolve(jsonResponse(inlineLogBody(outputLine('live output\n'), 1)));
|
|
64
|
+
}
|
|
65
|
+
return Promise.resolve(
|
|
66
|
+
jsonResponse(
|
|
67
|
+
workflowRunViewDetailDto({
|
|
68
|
+
jobs: [
|
|
69
|
+
workflowJobDto({
|
|
70
|
+
id: BUILD_JOB_ID,
|
|
71
|
+
run_attempt_id: RUN_ID,
|
|
72
|
+
name: 'build',
|
|
73
|
+
status: 'running',
|
|
74
|
+
steps: [
|
|
75
|
+
workflowStepDto({
|
|
76
|
+
id: stepId,
|
|
77
|
+
key: 'test',
|
|
78
|
+
name: 'test',
|
|
79
|
+
status: 'running',
|
|
80
|
+
attempts: [
|
|
81
|
+
workflowStepAttemptDto({
|
|
82
|
+
id: 'aaaaaaaa-aaaa-4aaa-8aaa-000000000001',
|
|
83
|
+
step_id: stepId,
|
|
84
|
+
status: 'running',
|
|
85
|
+
exit_code: null,
|
|
86
|
+
finished_at: null,
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
98
|
+
|
|
99
|
+
renderView();
|
|
100
|
+
|
|
101
|
+
expect(await screen.findByText('live output')).toBeInTheDocument();
|
|
102
|
+
const logRequest = fetchImpl.mock.calls
|
|
103
|
+
.map((call) => new URL((call[0] as Request).url))
|
|
104
|
+
.find((url) => url.pathname === `/steps/${stepId}/attempts/1/logs`);
|
|
105
|
+
expect(logRequest?.searchParams.get('cursor')).toBe('0');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('switches executions from the listening job header', async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
configureApiClient({
|
|
111
|
+
fetchImpl: vi.fn(() =>
|
|
112
|
+
Promise.resolve(
|
|
113
|
+
jsonResponse(
|
|
114
|
+
workflowRunViewDetailDto({
|
|
115
|
+
jobs: [
|
|
116
|
+
workflowJobDto({
|
|
117
|
+
id: BUILD_JOB_ID,
|
|
118
|
+
run_attempt_id: RUN_ID,
|
|
119
|
+
name: 'release-gates',
|
|
120
|
+
mode: 'listening',
|
|
121
|
+
status: 'running',
|
|
122
|
+
listener_status: 'listening',
|
|
123
|
+
job_executions: [
|
|
124
|
+
workflowJobExecutionDto({
|
|
125
|
+
id: 'execution-1',
|
|
126
|
+
job_id: BUILD_JOB_ID,
|
|
127
|
+
sequence: 1,
|
|
128
|
+
status: 'succeeded',
|
|
129
|
+
trigger_events: [
|
|
130
|
+
{
|
|
131
|
+
source: 'github',
|
|
132
|
+
event: 'pull_request',
|
|
133
|
+
delivery_id: 'delivery-1',
|
|
134
|
+
received_at: '2026-05-07T01:00:00.000Z',
|
|
135
|
+
data: {number: 12},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
started_at: '2026-05-07T01:01:00.000Z',
|
|
139
|
+
finished_at: '2026-05-07T01:01:20.000Z',
|
|
140
|
+
steps: [
|
|
141
|
+
workflowStepDto({
|
|
142
|
+
key: 'first-event',
|
|
143
|
+
name: 'first-event',
|
|
144
|
+
status: 'succeeded',
|
|
145
|
+
attempts: [
|
|
146
|
+
workflowStepAttemptDto({
|
|
147
|
+
status: 'succeeded',
|
|
148
|
+
finished_at: '2026-05-07T01:01:20.000Z',
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
}),
|
|
152
|
+
],
|
|
153
|
+
}),
|
|
154
|
+
workflowJobExecutionDto({
|
|
155
|
+
id: 'execution-2',
|
|
156
|
+
job_id: BUILD_JOB_ID,
|
|
157
|
+
sequence: 2,
|
|
158
|
+
status: 'failed',
|
|
159
|
+
status_reason: 'step_failed',
|
|
160
|
+
trigger_events: [
|
|
161
|
+
{
|
|
162
|
+
source: 'github',
|
|
163
|
+
event: 'deployment_status',
|
|
164
|
+
delivery_id: 'delivery-2',
|
|
165
|
+
received_at: '2026-05-07T01:01:00.000Z',
|
|
166
|
+
data: {state: 'success'},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
source: 'github',
|
|
170
|
+
event: 'check_run',
|
|
171
|
+
delivery_id: 'delivery-3',
|
|
172
|
+
received_at: '2026-05-07T01:01:10.000Z',
|
|
173
|
+
data: {status: 'completed'},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
started_at: '2026-05-07T01:02:00.000Z',
|
|
177
|
+
finished_at: '2026-05-07T01:02:20.000Z',
|
|
178
|
+
steps: [
|
|
179
|
+
workflowStepDto({
|
|
180
|
+
key: 'second-event',
|
|
181
|
+
name: 'second-event',
|
|
182
|
+
status: 'failed',
|
|
183
|
+
attempts: [
|
|
184
|
+
workflowStepAttemptDto({
|
|
185
|
+
status: 'failed',
|
|
186
|
+
exit_code: 1,
|
|
187
|
+
finished_at: '2026-05-07T01:02:20.000Z',
|
|
188
|
+
}),
|
|
189
|
+
],
|
|
190
|
+
}),
|
|
191
|
+
],
|
|
192
|
+
}),
|
|
193
|
+
],
|
|
194
|
+
}),
|
|
195
|
+
],
|
|
196
|
+
}),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
renderView();
|
|
203
|
+
expect(await screen.findByText('second-event')).toBeInTheDocument();
|
|
204
|
+
expect(
|
|
205
|
+
await screen.findByRole('button', {name: 'github · deployment_status (2 events)'}),
|
|
206
|
+
).toBeInTheDocument();
|
|
207
|
+
expect(screen.getByText('deployment_status (2)')).toBeInTheDocument();
|
|
208
|
+
|
|
209
|
+
await user.click(
|
|
210
|
+
screen.getByRole('button', {name: 'Switch job execution, currently execution 2'}),
|
|
211
|
+
);
|
|
212
|
+
await user.click(screen.getByRole('menuitem', {name: EXECUTION_ONE_MENU_ITEM_PATTERN}));
|
|
213
|
+
|
|
214
|
+
expect(await screen.findByText('first-event')).toBeInTheDocument();
|
|
215
|
+
expect(screen.queryByText('second-event')).not.toBeInTheDocument();
|
|
216
|
+
expect(await screen.findByRole('button', {name: 'github · pull_request'})).toBeInTheDocument();
|
|
217
|
+
expect(screen.getByText('pull_request')).toBeInTheDocument();
|
|
218
|
+
expect(screen.queryByText('deployment_status (2)')).not.toBeInTheDocument();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('shows generic provider setup guidance for agent config failures', async () => {
|
|
222
|
+
const user = userEvent.setup();
|
|
223
|
+
const stepId = '99999999-9999-4999-8999-000000000004';
|
|
224
|
+
const attemptId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000004';
|
|
225
|
+
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
226
|
+
const url = requestUrl(input);
|
|
227
|
+
if (url.pathname === `/steps/${stepId}/attempts/1/logs`) {
|
|
228
|
+
return Promise.resolve(
|
|
229
|
+
jsonResponse(inlineLogBody(outputLine('configuration failed\n'), 1)),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return Promise.resolve(
|
|
233
|
+
jsonResponse(
|
|
234
|
+
workflowRunViewDetailDto({
|
|
235
|
+
jobs: [
|
|
236
|
+
workflowJobDto({
|
|
237
|
+
id: BUILD_JOB_ID,
|
|
238
|
+
run_attempt_id: RUN_ID,
|
|
239
|
+
name: 'build',
|
|
240
|
+
status: 'failed',
|
|
241
|
+
steps: [
|
|
242
|
+
workflowStepDto({
|
|
243
|
+
id: stepId,
|
|
244
|
+
key: 'implement',
|
|
245
|
+
name: 'Fix the failing tests.',
|
|
246
|
+
type: 'agent',
|
|
247
|
+
status: 'failed',
|
|
248
|
+
config: {
|
|
249
|
+
provider: 'anthropic',
|
|
250
|
+
model: 'claude-opus-4-8',
|
|
251
|
+
thinking: 'high',
|
|
252
|
+
prompt: 'Fix the failing tests.',
|
|
253
|
+
},
|
|
254
|
+
error: {
|
|
255
|
+
message: 'Model provider request failed',
|
|
256
|
+
reason: 'agent_invocation_failed',
|
|
257
|
+
category: 'user',
|
|
258
|
+
},
|
|
259
|
+
attempts: [
|
|
260
|
+
workflowStepAttemptDto({
|
|
261
|
+
id: attemptId,
|
|
262
|
+
step_id: stepId,
|
|
263
|
+
status: 'failed',
|
|
264
|
+
exit_code: 1,
|
|
265
|
+
error: {
|
|
266
|
+
message: 'Model provider credentials are not configured',
|
|
267
|
+
reason: 'agent_config_invalid',
|
|
268
|
+
agentConfigIssue: 'provider_not_configured',
|
|
269
|
+
},
|
|
270
|
+
finished_at: '2026-05-07T01:01:20.000Z',
|
|
271
|
+
}),
|
|
272
|
+
],
|
|
273
|
+
}),
|
|
274
|
+
],
|
|
275
|
+
}),
|
|
276
|
+
],
|
|
277
|
+
}),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
282
|
+
|
|
283
|
+
renderView();
|
|
284
|
+
await user.click(
|
|
285
|
+
await screen.findByRole('button', {
|
|
286
|
+
name: 'Fix the failing tests., Failed, attempt 1, User',
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(
|
|
291
|
+
screen.queryByRole('region', {name: 'Resolved agent configuration'}),
|
|
292
|
+
).not.toBeInTheDocument();
|
|
293
|
+
expect(screen.queryByText('anthropic')).not.toBeInTheDocument();
|
|
294
|
+
expect(screen.queryByText('claude-opus-4-8')).not.toBeInTheDocument();
|
|
295
|
+
expect(screen.queryByText('high')).not.toBeInTheDocument();
|
|
296
|
+
expect(screen.getByText('Configure credentials for the selected provider')).toBeInTheDocument();
|
|
297
|
+
expect(
|
|
298
|
+
screen.getByText(
|
|
299
|
+
'This step uses the selected provider, but no workspace credentials are configured for that model provider. Configure the selected provider in Agents, then re-run the workflow.',
|
|
300
|
+
),
|
|
301
|
+
).toBeInTheDocument();
|
|
302
|
+
expect(screen.getByRole('link', {name: 'Configure Agents'})).toHaveAttribute(
|
|
303
|
+
'href',
|
|
304
|
+
`/workspaces/${PROJECT_TEST_WID}/settings/agents`,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('falls back to the step error when an agent attempt has no typed error', async () => {
|
|
309
|
+
const user = userEvent.setup();
|
|
310
|
+
const stepId = '99999999-9999-4999-8999-000000000005';
|
|
311
|
+
const attemptId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000005';
|
|
312
|
+
configureApiClient({
|
|
313
|
+
fetchImpl: vi.fn((input: RequestInfo | URL) => {
|
|
314
|
+
const url = requestUrl(input);
|
|
315
|
+
if (url.pathname === `/steps/${stepId}/attempts/1/logs`) {
|
|
316
|
+
return Promise.resolve(
|
|
317
|
+
jsonResponse(inlineLogBody(outputLine('configuration failed\n'), 1)),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return Promise.resolve(
|
|
321
|
+
jsonResponse(
|
|
322
|
+
workflowRunViewDetailDto({
|
|
323
|
+
jobs: [
|
|
324
|
+
workflowJobDto({
|
|
325
|
+
id: BUILD_JOB_ID,
|
|
326
|
+
run_attempt_id: RUN_ID,
|
|
327
|
+
name: 'build',
|
|
328
|
+
status: 'failed',
|
|
329
|
+
steps: [
|
|
330
|
+
workflowStepDto({
|
|
331
|
+
id: stepId,
|
|
332
|
+
key: 'implement',
|
|
333
|
+
name: 'Fix the failing tests.',
|
|
334
|
+
type: 'agent',
|
|
335
|
+
status: 'failed',
|
|
336
|
+
config: {
|
|
337
|
+
provider: 'anthropic',
|
|
338
|
+
model: 'claude-opus-4-8',
|
|
339
|
+
thinking: 'high',
|
|
340
|
+
prompt: 'Fix the failing tests.',
|
|
341
|
+
},
|
|
342
|
+
error: {
|
|
343
|
+
message: 'Model provider credentials are not configured',
|
|
344
|
+
reason: 'agent_config_invalid',
|
|
345
|
+
agent_config_issue: 'provider_not_configured',
|
|
346
|
+
category: 'user',
|
|
347
|
+
},
|
|
348
|
+
attempts: [
|
|
349
|
+
workflowStepAttemptDto({
|
|
350
|
+
id: attemptId,
|
|
351
|
+
step_id: stepId,
|
|
352
|
+
status: 'failed',
|
|
353
|
+
exit_code: 1,
|
|
354
|
+
error: null,
|
|
355
|
+
finished_at: '2026-05-07T01:01:20.000Z',
|
|
356
|
+
}),
|
|
357
|
+
],
|
|
358
|
+
}),
|
|
359
|
+
],
|
|
360
|
+
}),
|
|
361
|
+
],
|
|
362
|
+
}),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
renderView();
|
|
369
|
+
await user.click(
|
|
370
|
+
await screen.findByRole('button', {
|
|
371
|
+
name: 'Fix the failing tests., Failed, attempt 1, User',
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
expect(screen.queryByText('anthropic')).not.toBeInTheDocument();
|
|
376
|
+
expect(screen.queryByText('claude-opus-4-8')).not.toBeInTheDocument();
|
|
377
|
+
expect(screen.queryByText('high')).not.toBeInTheDocument();
|
|
378
|
+
expect(screen.getByText('Configure credentials for the selected provider')).toBeInTheDocument();
|
|
379
|
+
expect(screen.getByRole('link', {name: 'Configure Agents'})).toHaveAttribute(
|
|
380
|
+
'href',
|
|
381
|
+
`/workspaces/${PROJECT_TEST_WID}/settings/agents`,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('renders skipped zero-attempt jobs as skipped instead of missing attempts', async () => {
|
|
386
|
+
configureApiClient({
|
|
387
|
+
fetchImpl: vi.fn(() =>
|
|
388
|
+
Promise.resolve(
|
|
389
|
+
jsonResponse(
|
|
390
|
+
workflowRunViewDetailDto({
|
|
391
|
+
jobs: [
|
|
392
|
+
workflowJobDto({
|
|
393
|
+
id: DEPLOY_JOB_ID,
|
|
394
|
+
run_attempt_id: RUN_ID,
|
|
395
|
+
name: 'deploy',
|
|
396
|
+
status: 'skipped',
|
|
397
|
+
status_reason: 'dependency_not_completed',
|
|
398
|
+
job_executions: [],
|
|
399
|
+
}),
|
|
400
|
+
],
|
|
401
|
+
}),
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
renderView();
|
|
408
|
+
|
|
409
|
+
expect(await screen.findByText('This job was skipped')).toBeInTheDocument();
|
|
410
|
+
expect(
|
|
411
|
+
screen.getByText('A required job did not complete, so this job was skipped.'),
|
|
412
|
+
).toBeInTheDocument();
|
|
413
|
+
expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('uses the selected execution status for zero-step execution states', async () => {
|
|
417
|
+
configureApiClient({
|
|
418
|
+
fetchImpl: vi.fn(() =>
|
|
419
|
+
Promise.resolve(
|
|
420
|
+
jsonResponse(
|
|
421
|
+
workflowRunViewDetailDto({
|
|
422
|
+
jobs: [
|
|
423
|
+
workflowJobDto({
|
|
424
|
+
id: DEPLOY_JOB_ID,
|
|
425
|
+
run_attempt_id: RUN_ID,
|
|
426
|
+
name: 'deploy',
|
|
427
|
+
status: 'succeeded',
|
|
428
|
+
job_executions: [
|
|
429
|
+
workflowJobExecutionDto({
|
|
430
|
+
id: 'retry-execution',
|
|
431
|
+
job_id: DEPLOY_JOB_ID,
|
|
432
|
+
sequence: 2,
|
|
433
|
+
name: 'retry',
|
|
434
|
+
status: 'running',
|
|
435
|
+
steps: [],
|
|
436
|
+
}),
|
|
437
|
+
],
|
|
438
|
+
}),
|
|
439
|
+
],
|
|
440
|
+
}),
|
|
441
|
+
),
|
|
442
|
+
),
|
|
443
|
+
),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
renderView();
|
|
447
|
+
|
|
448
|
+
expect(await screen.findByText('Waiting for the first step')).toBeInTheDocument();
|
|
449
|
+
expect(
|
|
450
|
+
screen.getByText('This job is running, but no steps have started yet.'),
|
|
451
|
+
).toBeInTheDocument();
|
|
452
|
+
expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('renders pending zero-step jobs as waiting to start', async () => {
|
|
456
|
+
configureApiClient({
|
|
457
|
+
fetchImpl: vi.fn(() =>
|
|
458
|
+
Promise.resolve(
|
|
459
|
+
jsonResponse(
|
|
460
|
+
workflowRunViewDetailDto({
|
|
461
|
+
jobs: [
|
|
462
|
+
workflowJobDto({
|
|
463
|
+
id: DEPLOY_JOB_ID,
|
|
464
|
+
run_attempt_id: RUN_ID,
|
|
465
|
+
name: 'deploy',
|
|
466
|
+
status: 'pending',
|
|
467
|
+
steps: [],
|
|
468
|
+
}),
|
|
469
|
+
],
|
|
470
|
+
}),
|
|
471
|
+
),
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
renderView();
|
|
477
|
+
|
|
478
|
+
expect(await screen.findByText('Waiting for this job to start')).toBeInTheDocument();
|
|
479
|
+
expect(screen.getByText('Steps will appear here once the job starts.')).toBeInTheDocument();
|
|
480
|
+
expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('renders running zero-step jobs as waiting for the first step', async () => {
|
|
484
|
+
configureApiClient({
|
|
485
|
+
fetchImpl: vi.fn(() =>
|
|
486
|
+
Promise.resolve(
|
|
487
|
+
jsonResponse(
|
|
488
|
+
workflowRunViewDetailDto({
|
|
489
|
+
jobs: [
|
|
490
|
+
workflowJobDto({
|
|
491
|
+
id: DEPLOY_JOB_ID,
|
|
492
|
+
run_attempt_id: RUN_ID,
|
|
493
|
+
name: 'deploy',
|
|
494
|
+
status: 'running',
|
|
495
|
+
job_executions: [
|
|
496
|
+
workflowJobExecutionDto({
|
|
497
|
+
job_id: DEPLOY_JOB_ID,
|
|
498
|
+
status: 'running',
|
|
499
|
+
steps: [],
|
|
500
|
+
}),
|
|
501
|
+
],
|
|
502
|
+
}),
|
|
503
|
+
],
|
|
504
|
+
}),
|
|
505
|
+
),
|
|
506
|
+
),
|
|
507
|
+
),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
renderView();
|
|
511
|
+
|
|
512
|
+
expect(await screen.findByText('Waiting for the first step')).toBeInTheDocument();
|
|
513
|
+
expect(
|
|
514
|
+
screen.getByText('This job is running, but no steps have started yet.'),
|
|
515
|
+
).toBeInTheDocument();
|
|
516
|
+
expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('renders active listening jobs with no executions as waiting for trigger events', async () => {
|
|
520
|
+
configureApiClient({
|
|
521
|
+
fetchImpl: vi.fn(() =>
|
|
522
|
+
Promise.resolve(
|
|
523
|
+
jsonResponse(
|
|
524
|
+
workflowRunViewDetailDto({
|
|
525
|
+
jobs: [
|
|
526
|
+
workflowJobDto({
|
|
527
|
+
id: DEPLOY_JOB_ID,
|
|
528
|
+
run_attempt_id: RUN_ID,
|
|
529
|
+
name: 'release-gates',
|
|
530
|
+
mode: 'listening',
|
|
531
|
+
status: 'running',
|
|
532
|
+
listener_status: 'listening',
|
|
533
|
+
job_executions: [],
|
|
534
|
+
}),
|
|
535
|
+
],
|
|
536
|
+
}),
|
|
537
|
+
),
|
|
538
|
+
),
|
|
539
|
+
),
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
renderView();
|
|
543
|
+
|
|
544
|
+
expect(await screen.findByText('Waiting for trigger events')).toBeInTheDocument();
|
|
545
|
+
expect(
|
|
546
|
+
screen.getByText('Matching trigger events will create job executions here.'),
|
|
547
|
+
).toBeInTheDocument();
|
|
548
|
+
expect(screen.queryByText('Execution details unavailable')).not.toBeInTheDocument();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('renders finished one-shot jobs with no execution as unavailable execution details', async () => {
|
|
552
|
+
configureApiClient({
|
|
553
|
+
fetchImpl: vi.fn(() =>
|
|
554
|
+
Promise.resolve(
|
|
555
|
+
jsonResponse(
|
|
556
|
+
workflowRunViewDetailDto({
|
|
557
|
+
jobs: [
|
|
558
|
+
workflowJobDto({
|
|
559
|
+
id: DEPLOY_JOB_ID,
|
|
560
|
+
run_attempt_id: RUN_ID,
|
|
561
|
+
name: 'deploy',
|
|
562
|
+
status: 'succeeded',
|
|
563
|
+
job_executions: [],
|
|
564
|
+
}),
|
|
565
|
+
],
|
|
566
|
+
}),
|
|
567
|
+
),
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
renderView();
|
|
573
|
+
|
|
574
|
+
expect(await screen.findByText('Execution details unavailable')).toBeInTheDocument();
|
|
575
|
+
expect(
|
|
576
|
+
screen.getByText('This job finished, but no job execution record is available.'),
|
|
577
|
+
).toBeInTheDocument();
|
|
578
|
+
expect(screen.queryByText('Waiting for trigger events')).not.toBeInTheDocument();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('renders cancelled zero-attempt jobs separately from skipped jobs', async () => {
|
|
582
|
+
configureApiClient({
|
|
583
|
+
fetchImpl: vi.fn(() =>
|
|
584
|
+
Promise.resolve(
|
|
585
|
+
jsonResponse(
|
|
586
|
+
workflowRunViewDetailDto({
|
|
587
|
+
jobs: [
|
|
588
|
+
workflowJobDto({
|
|
589
|
+
id: DEPLOY_JOB_ID,
|
|
590
|
+
run_attempt_id: RUN_ID,
|
|
591
|
+
name: 'deploy',
|
|
592
|
+
status: 'cancelled',
|
|
593
|
+
job_executions: [
|
|
594
|
+
workflowJobExecutionDto({
|
|
595
|
+
job_id: DEPLOY_JOB_ID,
|
|
596
|
+
status: 'cancelled',
|
|
597
|
+
steps: [],
|
|
598
|
+
}),
|
|
599
|
+
],
|
|
600
|
+
}),
|
|
601
|
+
],
|
|
602
|
+
}),
|
|
603
|
+
),
|
|
604
|
+
),
|
|
605
|
+
),
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
renderView();
|
|
609
|
+
|
|
610
|
+
expect(await screen.findByText('Cancelled before start')).toBeInTheDocument();
|
|
611
|
+
expect(screen.getByText('This job was cancelled before any step started.')).toBeInTheDocument();
|
|
612
|
+
expect(screen.queryByText('This job was skipped')).not.toBeInTheDocument();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('shows the not-found surface when the run 404s', async () => {
|
|
616
|
+
configureApiClient({
|
|
617
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}))),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
renderView();
|
|
621
|
+
|
|
622
|
+
expect(await screen.findByText('Run not found')).toBeInTheDocument();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('shows the load-error placeholder when the run fails to load', async () => {
|
|
626
|
+
configureApiClient({
|
|
627
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse({code: 'server-error'}, {status: 500}))),
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
renderView();
|
|
631
|
+
|
|
632
|
+
expect(
|
|
633
|
+
await screen.findByRole('button', {name: 'Retry loading workflow run'}),
|
|
634
|
+
).toBeInTheDocument();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test('opens and closes workflow source without resetting the selected job', async () => {
|
|
638
|
+
const user = userEvent.setup();
|
|
639
|
+
configureApiClient({
|
|
640
|
+
fetchImpl: vi.fn(() =>
|
|
641
|
+
Promise.resolve(
|
|
642
|
+
jsonResponse(
|
|
643
|
+
workflowRunViewDetailDto({
|
|
644
|
+
source_snapshot: {
|
|
645
|
+
format: 'yaml',
|
|
646
|
+
content: 'jobs:\n build:\n steps:\n - run: pnpm test',
|
|
647
|
+
},
|
|
648
|
+
}),
|
|
649
|
+
),
|
|
650
|
+
),
|
|
651
|
+
),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
renderView();
|
|
655
|
+
|
|
656
|
+
const deployNode = await screen.findByRole('button', {name: 'deploy, Running'});
|
|
657
|
+
await user.click(deployNode);
|
|
658
|
+
expect(deployNode).toHaveAttribute('aria-pressed', 'true');
|
|
659
|
+
|
|
660
|
+
const sourceButton = within(screen.getByRole('region', {name: 'deploy-web'})).getByRole(
|
|
661
|
+
'button',
|
|
662
|
+
{name: 'View source'},
|
|
663
|
+
);
|
|
664
|
+
const panelId = sourceButton.getAttribute('aria-controls');
|
|
665
|
+
expect(panelId).toBeTruthy();
|
|
666
|
+
expect(sourceButton).toHaveAttribute('aria-expanded', 'false');
|
|
667
|
+
|
|
668
|
+
await user.click(sourceButton);
|
|
669
|
+
|
|
670
|
+
const sourcePanel = screen.getByRole('dialog', {name: 'Workflow source'});
|
|
671
|
+
expect(sourceButton).toHaveAttribute('aria-expanded', 'true');
|
|
672
|
+
expect(sourcePanel).toHaveAttribute('id', panelId);
|
|
673
|
+
expect(sourcePanel).toHaveTextContent('pnpm test');
|
|
674
|
+
|
|
675
|
+
await user.click(screen.getByRole('button', {name: 'Close source'}));
|
|
676
|
+
|
|
677
|
+
await waitFor(() => expect(sourceButton).toHaveFocus());
|
|
678
|
+
expect(sourceButton).toHaveAttribute('aria-expanded', 'false');
|
|
679
|
+
expect(deployNode).toHaveAttribute('aria-pressed', 'true');
|
|
680
|
+
expect(screen.getByRole('region', {name: 'deploy'})).toBeInTheDocument();
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test('highlights selected step source lines when the source panel opens', async () => {
|
|
684
|
+
const user = userEvent.setup();
|
|
685
|
+
const stepId = '99999999-9999-4999-8999-000000000003';
|
|
686
|
+
configureApiClient({
|
|
687
|
+
fetchImpl: vi.fn(() =>
|
|
688
|
+
Promise.resolve(
|
|
689
|
+
jsonResponse(
|
|
690
|
+
workflowRunViewDetailDto({
|
|
691
|
+
source_snapshot: {
|
|
692
|
+
format: 'yaml',
|
|
693
|
+
content: 'jobs:\n deploy:\n steps:\n - run: ship',
|
|
694
|
+
},
|
|
695
|
+
jobs: [
|
|
696
|
+
workflowJobDto({
|
|
697
|
+
id: DEPLOY_JOB_ID,
|
|
698
|
+
run_attempt_id: RUN_ID,
|
|
699
|
+
name: 'deploy',
|
|
700
|
+
status: 'running',
|
|
701
|
+
steps: [
|
|
702
|
+
workflowStepDto({
|
|
703
|
+
id: stepId,
|
|
704
|
+
key: 'deploy',
|
|
705
|
+
name: 'deploy',
|
|
706
|
+
source_location: {start_line: 2, end_line: 3},
|
|
707
|
+
status: 'running',
|
|
708
|
+
}),
|
|
709
|
+
],
|
|
710
|
+
}),
|
|
711
|
+
],
|
|
712
|
+
}),
|
|
713
|
+
),
|
|
714
|
+
),
|
|
715
|
+
),
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
renderView({selection: {stepId}});
|
|
719
|
+
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
720
|
+
await user.click(within(summary).getByRole('button', {name: 'View source'}));
|
|
721
|
+
|
|
722
|
+
await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
723
|
+
const highlightedLines = document.body.querySelectorAll('.line.highlighted-line');
|
|
724
|
+
expect(highlightedLines).toHaveLength(2);
|
|
725
|
+
expect(highlightedLines[0]).toHaveTextContent('deploy:');
|
|
726
|
+
expect(highlightedLines[1]).toHaveTextContent('steps:');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('does not render a source control when the run has no source snapshot', async () => {
|
|
730
|
+
configureApiClient({
|
|
731
|
+
fetchImpl: vi.fn(() =>
|
|
732
|
+
Promise.resolve(jsonResponse(workflowRunViewDetailDto({source_snapshot: null}))),
|
|
733
|
+
),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
renderView();
|
|
737
|
+
|
|
738
|
+
await screen.findByRole('region', {name: 'deploy-web'});
|
|
739
|
+
|
|
740
|
+
expect(screen.queryByRole('button', {name: 'View source'})).not.toBeInTheDocument();
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test('opens the source panel highlighting a step from the job header Source button', async () => {
|
|
744
|
+
const user = userEvent.setup();
|
|
745
|
+
configureApiClient({
|
|
746
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(locatedStepSourceDetail()))),
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
renderView();
|
|
750
|
+
|
|
751
|
+
const jobRegion = await screen.findByRole('region', {name: 'build'});
|
|
752
|
+
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
753
|
+
const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
|
|
754
|
+
const summaryButton = within(screen.getByRole('region', {name: 'deploy-web'})).getByRole(
|
|
755
|
+
'button',
|
|
756
|
+
{name: 'View source'},
|
|
757
|
+
);
|
|
758
|
+
expect(jobSourceButton).toHaveAttribute('aria-expanded', 'false');
|
|
759
|
+
expect(jobSourceButton).toHaveAttribute(
|
|
760
|
+
'aria-controls',
|
|
761
|
+
summaryButton.getAttribute('aria-controls'),
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
await user.click(jobSourceButton);
|
|
765
|
+
|
|
766
|
+
await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
767
|
+
const highlighted = document.body.querySelectorAll('.line.highlighted-line');
|
|
768
|
+
expect(highlighted).toHaveLength(2);
|
|
769
|
+
expect(highlighted[0]).toHaveTextContent('build:');
|
|
770
|
+
expect(highlighted[1]).toHaveTextContent('steps:');
|
|
771
|
+
expect(jobSourceButton).toHaveAttribute('aria-expanded', 'true');
|
|
772
|
+
expect(summaryButton).toHaveAttribute('aria-expanded', 'false');
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('returns focus to the job Source button on close and preserves the selected job', async () => {
|
|
776
|
+
const user = userEvent.setup();
|
|
777
|
+
configureApiClient({
|
|
778
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(locatedStepSourceDetail()))),
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
renderView();
|
|
782
|
+
|
|
783
|
+
const jobRegion = await screen.findByRole('region', {name: 'build'});
|
|
784
|
+
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
785
|
+
const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
|
|
786
|
+
|
|
787
|
+
await user.click(jobSourceButton);
|
|
788
|
+
await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
789
|
+
await user.click(screen.getByRole('button', {name: 'Close source'}));
|
|
790
|
+
|
|
791
|
+
await waitFor(() => expect(jobSourceButton).toHaveFocus());
|
|
792
|
+
expect(jobSourceButton).toHaveAttribute('aria-expanded', 'false');
|
|
793
|
+
expect(screen.getByRole('region', {name: 'build'})).toBeInTheDocument();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test('falls back to the summary Source button when refetch removes the focused step source', async () => {
|
|
797
|
+
const user = userEvent.setup();
|
|
798
|
+
let detailRequests = 0;
|
|
799
|
+
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
800
|
+
const url = requestUrl(input);
|
|
801
|
+
if (url.pathname === `/workflows/runs/${RUN_ID}`) {
|
|
802
|
+
detailRequests += 1;
|
|
803
|
+
return Promise.resolve(
|
|
804
|
+
jsonResponse(
|
|
805
|
+
detailRequests === 1
|
|
806
|
+
? locatedStepSourceDetail()
|
|
807
|
+
: locatedStepSourceDetail({
|
|
808
|
+
jobs: [
|
|
809
|
+
workflowJobDto({
|
|
810
|
+
id: BUILD_JOB_ID,
|
|
811
|
+
name: 'build',
|
|
812
|
+
status: 'succeeded',
|
|
813
|
+
steps: [
|
|
814
|
+
workflowStepDto({
|
|
815
|
+
id: CHECKOUT_STEP_ID,
|
|
816
|
+
name: 'checkout',
|
|
817
|
+
status: 'succeeded',
|
|
818
|
+
source_location: null,
|
|
819
|
+
attempts: [
|
|
820
|
+
workflowStepAttemptDto({
|
|
821
|
+
id: CHECKOUT_ATTEMPT_ID,
|
|
822
|
+
step_id: CHECKOUT_STEP_ID,
|
|
823
|
+
status: 'succeeded',
|
|
824
|
+
exit_code: 0,
|
|
825
|
+
finished_at: '2026-05-07T01:01:20.000Z',
|
|
826
|
+
}),
|
|
827
|
+
],
|
|
828
|
+
}),
|
|
829
|
+
],
|
|
830
|
+
}),
|
|
831
|
+
],
|
|
832
|
+
}),
|
|
833
|
+
),
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return Promise.resolve(jsonResponse(inlineLogBody(outputLine('done\n'), 1)));
|
|
837
|
+
});
|
|
838
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
839
|
+
|
|
840
|
+
const {queryClient} = renderView();
|
|
841
|
+
|
|
842
|
+
const jobRegion = await screen.findByRole('region', {name: 'build'});
|
|
843
|
+
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
844
|
+
const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
|
|
845
|
+
const summaryButton = within(screen.getByRole('region', {name: 'deploy-web'})).getByRole(
|
|
846
|
+
'button',
|
|
847
|
+
{name: 'View source'},
|
|
848
|
+
);
|
|
849
|
+
await user.click(jobSourceButton);
|
|
850
|
+
await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
851
|
+
|
|
852
|
+
await queryClient.refetchQueries({queryKey: workflowRunsQueryKeys.detail(RUN_ID)});
|
|
853
|
+
await waitFor(() =>
|
|
854
|
+
expect(
|
|
855
|
+
within(jobRegion).queryByRole('button', {name: 'View source'}),
|
|
856
|
+
).not.toBeInTheDocument(),
|
|
857
|
+
);
|
|
858
|
+
await user.click(screen.getByRole('button', {name: 'Close source'}));
|
|
859
|
+
|
|
860
|
+
await waitFor(() => expect(summaryButton).toHaveFocus());
|
|
861
|
+
expect(summaryButton).toHaveAttribute('aria-expanded', 'false');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('re-runs all jobs from a succeeded run and navigates to the new run', async () => {
|
|
865
|
+
const user = userEvent.setup();
|
|
866
|
+
const rerunId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
|
867
|
+
const successSpy = vi.spyOn(toast, 'success').mockImplementation(() => 'toast-id');
|
|
868
|
+
const postBodies: unknown[] = [];
|
|
869
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
870
|
+
const request = input as Request;
|
|
871
|
+
const url = requestUrl(input);
|
|
872
|
+
if (request.method === 'POST' && url.pathname === `/workflows/runs/${RUN_ID}/rerun`) {
|
|
873
|
+
postBodies.push(await request.clone().json());
|
|
874
|
+
return jsonResponse(workflowRunDto({id: rerunId, status: 'pending'}));
|
|
875
|
+
}
|
|
876
|
+
return jsonResponse(
|
|
877
|
+
workflowRunViewDetailDto({
|
|
878
|
+
status: 'succeeded',
|
|
879
|
+
jobs: [
|
|
880
|
+
workflowJobDto({
|
|
881
|
+
id: BUILD_JOB_ID,
|
|
882
|
+
run_attempt_id: RUN_ID,
|
|
883
|
+
name: 'build',
|
|
884
|
+
status: 'succeeded',
|
|
885
|
+
}),
|
|
886
|
+
],
|
|
887
|
+
}),
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
891
|
+
|
|
892
|
+
const {router} = renderView({}, '?runAttempt=1');
|
|
893
|
+
await user.click(await screen.findByRole('button', {name: 'Re-run workflow'}));
|
|
894
|
+
|
|
895
|
+
const postRequest = await findRequest(fetchImpl, 'POST', `/workflows/runs/${RUN_ID}/rerun`);
|
|
896
|
+
expect(postRequest).toBeDefined();
|
|
897
|
+
expect(postBodies).toEqual([{mode: 'all'}]);
|
|
898
|
+
expect(successSpy).toHaveBeenCalledWith('Re-run started');
|
|
899
|
+
await waitFor(() =>
|
|
900
|
+
expect(router.state.location.pathname).toBe(
|
|
901
|
+
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${rerunId}`,
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
expect((router.state.location.search as Record<string, unknown>).runAttempt).toBeUndefined();
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test('re-runs failed jobs from the dropdown', async () => {
|
|
908
|
+
const user = userEvent.setup();
|
|
909
|
+
const postBodies: unknown[] = [];
|
|
910
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
911
|
+
const request = input as Request;
|
|
912
|
+
const url = requestUrl(input);
|
|
913
|
+
if (request.method === 'POST' && url.pathname === `/workflows/runs/${RUN_ID}/rerun`) {
|
|
914
|
+
postBodies.push(await request.clone().json());
|
|
915
|
+
return jsonResponse(workflowRunDto({id: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'}));
|
|
916
|
+
}
|
|
917
|
+
return jsonResponse(
|
|
918
|
+
workflowRunViewDetailDto({
|
|
919
|
+
status: 'failed',
|
|
920
|
+
jobs: [
|
|
921
|
+
workflowJobDto({
|
|
922
|
+
id: BUILD_JOB_ID,
|
|
923
|
+
run_attempt_id: RUN_ID,
|
|
924
|
+
name: 'build',
|
|
925
|
+
status: 'failed',
|
|
926
|
+
}),
|
|
927
|
+
workflowJobDto({
|
|
928
|
+
id: DEPLOY_JOB_ID,
|
|
929
|
+
run_attempt_id: RUN_ID,
|
|
930
|
+
name: 'deploy',
|
|
931
|
+
status: 'cancelled',
|
|
932
|
+
position: 1,
|
|
933
|
+
}),
|
|
934
|
+
],
|
|
935
|
+
}),
|
|
936
|
+
);
|
|
937
|
+
});
|
|
938
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
939
|
+
|
|
940
|
+
renderView();
|
|
941
|
+
await user.click(await screen.findByRole('button', {name: 'Re-run jobs'}));
|
|
942
|
+
await user.click(await screen.findByRole('menuitem', {name: 'Re-run failed jobs'}));
|
|
943
|
+
|
|
944
|
+
const postRequest = await findRequest(fetchImpl, 'POST', `/workflows/runs/${RUN_ID}/rerun`);
|
|
945
|
+
expect(postRequest).toBeDefined();
|
|
946
|
+
expect(postBodies).toEqual([{mode: 'failed'}]);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test('shows an error toast when rerun creation fails', async () => {
|
|
950
|
+
const user = userEvent.setup();
|
|
951
|
+
const errorSpy = vi.spyOn(toast, 'error').mockImplementation(() => 'toast-id');
|
|
952
|
+
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
953
|
+
const request = input as Request;
|
|
954
|
+
const url = requestUrl(input);
|
|
955
|
+
if (request.method === 'POST' && url.pathname === `/workflows/runs/${RUN_ID}/rerun`) {
|
|
956
|
+
return Promise.resolve(
|
|
957
|
+
jsonResponse({code: 'no-failed-jobs', message: 'Run has no failed jobs'}, {status: 409}),
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
return Promise.resolve(jsonResponse(workflowRunViewDetailDto({status: 'succeeded'})));
|
|
961
|
+
});
|
|
962
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
963
|
+
|
|
964
|
+
renderView();
|
|
965
|
+
await user.click(await screen.findByRole('button', {name: 'Re-run workflow'}));
|
|
966
|
+
|
|
967
|
+
await waitFor(() => expect(errorSpy).toHaveBeenCalledWith('Run has no failed jobs'));
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
test('selects another run attempt and clears job selection search', async () => {
|
|
971
|
+
const user = userEvent.setup();
|
|
972
|
+
const secondAttemptId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
|
973
|
+
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
974
|
+
const url = requestUrl(input);
|
|
975
|
+
if (url.pathname === `/workflows/runs/${RUN_ID}/attempts`) {
|
|
976
|
+
return Promise.resolve(
|
|
977
|
+
jsonResponse(
|
|
978
|
+
runAttemptsResponseDto({
|
|
979
|
+
attempts: [
|
|
980
|
+
workflowRunAttemptDto({
|
|
981
|
+
id: RUN_ID,
|
|
982
|
+
attempt: 1,
|
|
983
|
+
status: 'succeeded',
|
|
984
|
+
created_at: '2026-05-07T01:01:00.000Z',
|
|
985
|
+
}),
|
|
986
|
+
workflowRunAttemptDto({
|
|
987
|
+
id: secondAttemptId,
|
|
988
|
+
attempt: 2,
|
|
989
|
+
status: 'running',
|
|
990
|
+
created_at: '2026-05-07T01:02:00.000Z',
|
|
991
|
+
rerun_mode: 'all',
|
|
992
|
+
}),
|
|
993
|
+
],
|
|
994
|
+
}),
|
|
995
|
+
),
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
return Promise.resolve(
|
|
999
|
+
jsonResponse(
|
|
1000
|
+
workflowRunViewDetailDto({
|
|
1001
|
+
current_attempt: 1,
|
|
1002
|
+
latest_attempt: 2,
|
|
1003
|
+
run_attempt: workflowRunAttemptDto({id: RUN_ID, workflow_run_id: RUN_ID, attempt: 1}),
|
|
1004
|
+
}),
|
|
1005
|
+
),
|
|
1006
|
+
);
|
|
1007
|
+
});
|
|
1008
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
1009
|
+
|
|
1010
|
+
const {router} = renderProjectPage(
|
|
1011
|
+
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${RUN_ID}?job=${BUILD_JOB_ID}&step=${CHECKOUT_STEP_ID}&stepAttempt=${CHECKOUT_ATTEMPT_ID}`,
|
|
1012
|
+
() => (
|
|
1013
|
+
<WorkflowRunView
|
|
1014
|
+
workspaceId={PROJECT_TEST_WID}
|
|
1015
|
+
projectId={PROJECT_ID}
|
|
1016
|
+
workflowRunId={RUN_ID}
|
|
1017
|
+
/>
|
|
1018
|
+
),
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
await user.click(await screen.findByRole('button', {name: 'Switch attempt, currently 1 of 2'}));
|
|
1022
|
+
await user.click(await screen.findByRole('menuitem', {name: ATTEMPT_2_PATTERN}));
|
|
1023
|
+
|
|
1024
|
+
await waitFor(() =>
|
|
1025
|
+
expect(router.state.location.pathname).toBe(
|
|
1026
|
+
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${RUN_ID}`,
|
|
1027
|
+
),
|
|
1028
|
+
);
|
|
1029
|
+
expect(router.state.location.search).toEqual({runAttempt: 2});
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test('does not render rerun controls for non-terminal runs', async () => {
|
|
1033
|
+
configureApiClient({
|
|
1034
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
renderView();
|
|
1038
|
+
|
|
1039
|
+
await screen.findByRole('region', {name: 'deploy-web'});
|
|
1040
|
+
expect(screen.queryByRole('button', {name: RERUN_BUTTON_NAME})).not.toBeInTheDocument();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test('renders carried-over steps without requesting attempt logs', async () => {
|
|
1044
|
+
const user = userEvent.setup();
|
|
1045
|
+
const fetchImpl = vi.fn(() =>
|
|
1046
|
+
Promise.resolve(
|
|
1047
|
+
jsonResponse(
|
|
1048
|
+
workflowRunViewDetailDto({
|
|
1049
|
+
status: 'succeeded',
|
|
1050
|
+
jobs: [
|
|
1051
|
+
workflowJobDto({
|
|
1052
|
+
id: BUILD_JOB_ID,
|
|
1053
|
+
run_attempt_id: RUN_ID,
|
|
1054
|
+
name: 'build',
|
|
1055
|
+
status: 'succeeded',
|
|
1056
|
+
carried_over: true,
|
|
1057
|
+
steps: [
|
|
1058
|
+
workflowStepDto({
|
|
1059
|
+
id: CHECKOUT_STEP_ID,
|
|
1060
|
+
key: 'checkout',
|
|
1061
|
+
name: 'checkout',
|
|
1062
|
+
status: 'succeeded',
|
|
1063
|
+
attempts: [],
|
|
1064
|
+
}),
|
|
1065
|
+
],
|
|
1066
|
+
}),
|
|
1067
|
+
],
|
|
1068
|
+
}),
|
|
1069
|
+
),
|
|
1070
|
+
),
|
|
1071
|
+
);
|
|
1072
|
+
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
1073
|
+
|
|
1074
|
+
renderView();
|
|
1075
|
+
expect(
|
|
1076
|
+
await screen.findByRole('button', {name: 'build, Succeeded, reused'}),
|
|
1077
|
+
).toBeInTheDocument();
|
|
1078
|
+
expect(screen.getAllByText('reused')).toHaveLength(2);
|
|
1079
|
+
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
1080
|
+
|
|
1081
|
+
expect(await screen.findByText('Not executed in this run.')).toBeInTheDocument();
|
|
1082
|
+
expect(
|
|
1083
|
+
mockRequests(fetchImpl).some((request) => requestUrl(request).pathname.includes('/logs')),
|
|
1084
|
+
).toBe(false);
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
function renderView(props: Partial<Parameters<typeof WorkflowRunView>[0]> = {}, search = '') {
|
|
1089
|
+
return renderProjectPage(
|
|
1090
|
+
`/workspaces/${PROJECT_TEST_WID}/projects/x/runs/${RUN_ID}${search}`,
|
|
1091
|
+
() => (
|
|
1092
|
+
<WorkflowRunView
|
|
1093
|
+
workspaceId={PROJECT_TEST_WID}
|
|
1094
|
+
projectId={PROJECT_ID}
|
|
1095
|
+
workflowRunId={RUN_ID}
|
|
1096
|
+
{...props}
|
|
1097
|
+
/>
|
|
1098
|
+
),
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function requestUrl(input: RequestInfo | URL): URL {
|
|
1103
|
+
if (input instanceof Request) return new URL(input.url);
|
|
1104
|
+
return new URL(String(input));
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
async function findRequest(
|
|
1108
|
+
fetchImpl: ReturnType<typeof vi.fn>,
|
|
1109
|
+
method: string,
|
|
1110
|
+
pathname: string,
|
|
1111
|
+
): Promise<Request> {
|
|
1112
|
+
await waitFor(() => {
|
|
1113
|
+
const match = mockRequests(fetchImpl).find(
|
|
1114
|
+
(request) => request.method === method && requestUrl(request).pathname === pathname,
|
|
1115
|
+
);
|
|
1116
|
+
expect(match).toBeDefined();
|
|
1117
|
+
});
|
|
1118
|
+
const match = mockRequests(fetchImpl).find(
|
|
1119
|
+
(request) => request.method === method && requestUrl(request).pathname === pathname,
|
|
1120
|
+
);
|
|
1121
|
+
if (!match) throw new Error(`Missing ${method} ${pathname}`);
|
|
1122
|
+
return match;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function mockRequests(fetchImpl: ReturnType<typeof vi.fn>): Request[] {
|
|
1126
|
+
return (fetchImpl.mock.calls as unknown[][])
|
|
1127
|
+
.map((call) => call[0])
|
|
1128
|
+
.filter((input): input is Request => input instanceof Request);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function locatedStepSourceDetail(
|
|
1132
|
+
overrides: Partial<WorkflowRunDetailResponseDto> = {},
|
|
1133
|
+
): WorkflowRunDetailResponseDto {
|
|
1134
|
+
return workflowRunViewDetailDto({
|
|
1135
|
+
source_snapshot: {
|
|
1136
|
+
format: 'yaml',
|
|
1137
|
+
content: 'jobs:\n build:\n steps:\n - run: pnpm test',
|
|
1138
|
+
},
|
|
1139
|
+
jobs: [
|
|
1140
|
+
workflowJobDto({
|
|
1141
|
+
id: BUILD_JOB_ID,
|
|
1142
|
+
name: 'build',
|
|
1143
|
+
status: 'succeeded',
|
|
1144
|
+
steps: [
|
|
1145
|
+
workflowStepDto({
|
|
1146
|
+
id: CHECKOUT_STEP_ID,
|
|
1147
|
+
name: 'checkout',
|
|
1148
|
+
status: 'succeeded',
|
|
1149
|
+
source_location: {start_line: 2, end_line: 3},
|
|
1150
|
+
attempts: [
|
|
1151
|
+
workflowStepAttemptDto({
|
|
1152
|
+
id: CHECKOUT_ATTEMPT_ID,
|
|
1153
|
+
step_id: CHECKOUT_STEP_ID,
|
|
1154
|
+
status: 'succeeded',
|
|
1155
|
+
exit_code: 0,
|
|
1156
|
+
finished_at: '2026-05-07T01:01:20.000Z',
|
|
1157
|
+
}),
|
|
1158
|
+
],
|
|
1159
|
+
}),
|
|
1160
|
+
],
|
|
1161
|
+
}),
|
|
1162
|
+
],
|
|
1163
|
+
...overrides,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function workflowRunViewDetailDto(
|
|
1168
|
+
overrides: Partial<WorkflowRunDetailResponseDto> = {},
|
|
1169
|
+
): WorkflowRunDetailResponseDto {
|
|
1170
|
+
return workflowRunDetailDto({
|
|
1171
|
+
id: RUN_ID,
|
|
1172
|
+
project_id: PROJECT_ID,
|
|
1173
|
+
definition_id: DEFINITION_ID,
|
|
1174
|
+
name: 'deploy-web',
|
|
1175
|
+
status: 'running',
|
|
1176
|
+
trigger_payload: {},
|
|
1177
|
+
created_at: '2026-05-07T01:01:00.000Z',
|
|
1178
|
+
updated_at: '2026-05-07T01:02:00.000Z',
|
|
1179
|
+
jobs: [
|
|
1180
|
+
workflowJobDto({
|
|
1181
|
+
id: BUILD_JOB_ID,
|
|
1182
|
+
run_attempt_id: RUN_ID,
|
|
1183
|
+
name: 'build',
|
|
1184
|
+
status: 'succeeded',
|
|
1185
|
+
steps: [
|
|
1186
|
+
workflowStepDto({
|
|
1187
|
+
id: CHECKOUT_STEP_ID,
|
|
1188
|
+
key: 'checkout',
|
|
1189
|
+
name: 'checkout',
|
|
1190
|
+
status: 'succeeded',
|
|
1191
|
+
attempts: [
|
|
1192
|
+
workflowStepAttemptDto({
|
|
1193
|
+
id: CHECKOUT_ATTEMPT_ID,
|
|
1194
|
+
step_id: CHECKOUT_STEP_ID,
|
|
1195
|
+
status: 'succeeded',
|
|
1196
|
+
exit_code: 0,
|
|
1197
|
+
finished_at: '2026-05-07T01:01:20.000Z',
|
|
1198
|
+
}),
|
|
1199
|
+
],
|
|
1200
|
+
}),
|
|
1201
|
+
],
|
|
1202
|
+
}),
|
|
1203
|
+
workflowJobDto({
|
|
1204
|
+
id: DEPLOY_JOB_ID,
|
|
1205
|
+
run_attempt_id: RUN_ID,
|
|
1206
|
+
name: 'deploy',
|
|
1207
|
+
status: 'running',
|
|
1208
|
+
position: 1,
|
|
1209
|
+
dependencies: ['build'],
|
|
1210
|
+
}),
|
|
1211
|
+
],
|
|
1212
|
+
...overrides,
|
|
1213
|
+
});
|
|
1214
|
+
}
|