@shipfox/client-workflows 15.0.0 → 16.0.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.
Files changed (91) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +21 -0
  4. package/dist/components/job-detail/job-detail-header.d.ts +8 -1
  5. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  6. package/dist/components/job-detail/job-detail-header.js +25 -12
  7. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  8. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  9. package/dist/components/job-detail/job-detail-view.js +17 -2
  10. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  11. package/dist/components/workflow-run-summary/workflow-run-summary.js +1 -0
  12. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  13. package/dist/components/workflow-run-tabs/index.d.ts +3 -0
  14. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  15. package/dist/components/workflow-run-tabs/index.js +3 -0
  16. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  17. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  18. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  19. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  20. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  21. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  22. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  23. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  24. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  25. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +32 -0
  26. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  27. package/dist/components/workflow-run-tabs/run-annotation-list.js +202 -0
  28. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  29. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  30. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  31. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +87 -22
  32. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  33. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  34. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  35. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  36. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  37. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +1 -1
  38. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  39. package/dist/components/workflow-run-view/run-workspace-nav.js +9 -3
  40. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  41. package/dist/components/workflow-run-view/workflow-run-view.d.ts +1 -3
  42. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  43. package/dist/components/workflow-run-view/workflow-run-view.js +150 -52
  44. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  45. package/dist/core/run-annotation.d.ts +73 -0
  46. package/dist/core/run-annotation.d.ts.map +1 -0
  47. package/dist/core/run-annotation.js +125 -0
  48. package/dist/core/run-annotation.js.map +1 -0
  49. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  50. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  51. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  52. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  53. package/dist/hooks/api/run-annotations.d.ts +25 -0
  54. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  55. package/dist/hooks/api/run-annotations.js +78 -0
  56. package/dist/hooks/api/run-annotations.js.map +1 -0
  57. package/dist/routes/inputs.d.ts +7 -2
  58. package/dist/routes/inputs.d.ts.map +1 -1
  59. package/dist/routes/inputs.js +5 -6
  60. package/dist/routes/inputs.js.map +1 -1
  61. package/dist/tsconfig.test.tsbuildinfo +1 -1
  62. package/package.json +10 -9
  63. package/src/components/job-detail/job-detail-header.test.tsx +3 -0
  64. package/src/components/job-detail/job-detail-header.tsx +30 -5
  65. package/src/components/job-detail/job-detail-view.stories.tsx +5 -0
  66. package/src/components/job-detail/job-detail-view.tsx +18 -1
  67. package/src/components/workflow-run-summary/workflow-run-summary.tsx +1 -1
  68. package/src/components/workflow-run-tabs/index.ts +3 -0
  69. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  70. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  71. package/src/components/workflow-run-tabs/run-annotation-list.tsx +258 -0
  72. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  73. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +92 -17
  74. package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
  75. package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
  76. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +44 -0
  77. package/src/components/workflow-run-view/run-workspace-nav.tsx +15 -2
  78. package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
  79. package/src/components/workflow-run-view/workflow-run-view.tsx +148 -30
  80. package/src/core/run-annotation.test.ts +315 -0
  81. package/src/core/run-annotation.ts +238 -0
  82. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  83. package/src/hooks/api/run-annotations.ts +137 -0
  84. package/src/pages/workflow-job-detail-page.test.tsx +66 -0
  85. package/src/routes/inputs.ts +7 -2
  86. package/tsconfig.build.tsbuildinfo +1 -1
  87. package/dist/core/workflow-run-tabs.d.ts +0 -9
  88. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  89. package/dist/core/workflow-run-tabs.js +0 -3
  90. package/dist/core/workflow-run-tabs.js.map +0 -1
  91. package/src/core/workflow-run-tabs.ts +0 -8
@@ -6,13 +6,13 @@ import {cn} from '@shipfox/react-ui/utils';
6
6
  import {Link} from '@tanstack/react-router';
7
7
  import {useEffect, useMemo, useRef, useState} from 'react';
8
8
  import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
9
+ import type {RunAnnotationSummary} from '#core/run-annotation.js';
9
10
  import {
10
11
  defaultJobExecution,
11
12
  deriveJobDisplayStatus,
12
13
  type Job,
13
14
  type WorkflowRunDetail,
14
15
  } from '#core/workflow-run.js';
15
- import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
16
16
  import {
17
17
  type WorkflowJobSearch,
18
18
  type WorkflowRunTab,
@@ -224,6 +224,7 @@ function RunWorkspaceNavContent({
224
224
  section="annotations"
225
225
  current={!currentJobId && activeSection === 'annotations'}
226
226
  count={annotationSummary?.total}
227
+ countTruncated={annotationSummary?.truncated}
227
228
  onNavigate={onNavigate}
228
229
  />
229
230
  </li>
@@ -252,6 +253,7 @@ function RunSectionLink({
252
253
  section,
253
254
  current,
254
255
  count,
256
+ countTruncated = false,
255
257
  onNavigate,
256
258
  }: {
257
259
  workspaceSlug: string;
@@ -261,6 +263,8 @@ function RunSectionLink({
261
263
  section: RunWorkspaceSection;
262
264
  current: boolean;
263
265
  count?: number | undefined;
266
+ /** The read hit its page budget, so the count is a lower bound and renders as `N+`. */
267
+ countTruncated?: boolean | undefined;
264
268
  onNavigate?: (() => void) | undefined;
265
269
  }) {
266
270
  return (
@@ -275,6 +279,11 @@ function RunSectionLink({
275
279
  }
276
280
  onClick={onNavigate}
277
281
  aria-current={current ? 'page' : undefined}
282
+ aria-label={
283
+ count
284
+ ? `${sectionLabel(section)}, ${countTruncated ? `${count} or more` : count}`
285
+ : undefined
286
+ }
278
287
  className={cn(
279
288
  'flex min-h-32 items-center gap-8 rounded-4 px-8 text-xs font-medium text-foreground-neutral-base outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
280
289
  current && 'bg-background-neutral-hover',
@@ -282,8 +291,12 @@ function RunSectionLink({
282
291
  >
283
292
  {sectionLabel(section)}
284
293
  {count ? (
285
- <span className="ml-auto font-code text-xs text-foreground-neutral-muted tabular-nums">
294
+ <span
295
+ aria-hidden="true"
296
+ className="ml-auto font-code text-xs text-foreground-neutral-muted tabular-nums"
297
+ >
286
298
  {count}
299
+ {countTruncated ? '+' : ''}
287
300
  </span>
288
301
  ) : null}
289
302
  </Link>
@@ -1,10 +1,13 @@
1
+ import type {AnnotationDto} from '@shipfox/annotations-dto';
1
2
  import type {WorkflowRunDetailResponseDto} from '@shipfox/api-workflows-dto';
2
3
  import {configureApiClient} from '@shipfox/client-api';
3
4
  import {screen, waitFor, within} from '@testing-library/react';
4
5
  import userEvent from '@testing-library/user-event';
5
6
  import {
6
7
  workflowJobDto,
8
+ workflowJobExecutionDto,
7
9
  workflowRunDetailDto,
10
+ workflowStepAttemptDto,
8
11
  workflowStepDto,
9
12
  } from '#test/fixtures/workflow-run.js';
10
13
  import {jsonResponse, PROJECT_TEST_WSLUG, renderProjectPage} from '#test/pages.js';
@@ -14,6 +17,13 @@ const RUN_ID = '66666666-6666-4666-8666-666666666666';
14
17
  const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
15
18
  const BUILD_JOB_ID = '77777777-7777-4777-8777-777777777777';
16
19
  const DEPLOY_JOB_ID = '88888888-8888-4888-8888-888888888888';
20
+ const BUILD_EXECUTION_ID = '99999999-9999-4999-8999-000000000001';
21
+ const BUILD_STEP_ID = '55555555-5555-4555-8555-000000000001';
22
+ const BUILD_ATTEMPT_ID = '66666666-6666-4666-8666-000000000001';
23
+ const ANNOTATION_ID_ONE = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000001';
24
+ const ANNOTATION_ID_TWO = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000002';
25
+ const TASK_NINE_PATTERN = /Task nine/;
26
+ const SHOW_MORE_PATTERN = /Show \d+ more/;
17
27
 
18
28
  describe('WorkflowRunView', () => {
19
29
  beforeEach(() => {
@@ -32,9 +42,7 @@ describe('WorkflowRunView', () => {
32
42
  });
33
43
 
34
44
  test('opens the all-jobs Summary on the dependency graph by default', async () => {
35
- configureApiClient({
36
- fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
37
- });
45
+ configureRunFetch();
38
46
 
39
47
  const {container} = renderView();
40
48
 
@@ -59,9 +67,7 @@ describe('WorkflowRunView', () => {
59
67
  });
60
68
 
61
69
  test('treats the removed Jobs tab URL as the graph Summary', async () => {
62
- configureApiClient({
63
- fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
64
- });
70
+ configureRunFetch();
65
71
 
66
72
  renderView({tab: 'jobs'});
67
73
 
@@ -71,9 +77,7 @@ describe('WorkflowRunView', () => {
71
77
 
72
78
  test('navigates graph and rail jobs to their dedicated job routes', async () => {
73
79
  const user = userEvent.setup();
74
- configureApiClient({
75
- fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
76
- });
80
+ configureRunFetch();
77
81
 
78
82
  const {router} = renderView();
79
83
  await user.click(await screen.findByRole('button', {name: 'deploy, Running'}));
@@ -87,9 +91,7 @@ describe('WorkflowRunView', () => {
87
91
 
88
92
  test('keeps run Annotations and Source in the workspace navigation', async () => {
89
93
  const user = userEvent.setup();
90
- configureApiClient({
91
- fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
92
- });
94
+ configureRunFetch();
93
95
 
94
96
  const {router} = renderView();
95
97
  await screen.findByRole('region', {name: 'Workflow jobs'});
@@ -99,19 +101,182 @@ describe('WorkflowRunView', () => {
99
101
  });
100
102
 
101
103
  test('filters the single run-level Annotations page by job', async () => {
102
- configureApiClient({
103
- fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
104
- });
104
+ configureRunFetch();
105
105
 
106
106
  renderView({tab: 'annotations', selection: {jobId: BUILD_JOB_ID}});
107
107
 
108
108
  expect(
109
109
  await screen.findByRole('combobox', {name: 'Filter annotations by job'}),
110
110
  ).toHaveTextContent('build');
111
- expect(screen.getByText('build has no annotations to show in this run.')).toBeInTheDocument();
111
+ expect(await screen.findByText('This run has no annotations to show.')).toBeInTheDocument();
112
112
  expect(screen.queryByRole('tab', {name: 'Annotations'})).not.toBeInTheDocument();
113
113
  });
114
114
 
115
+ test('renders annotation bodies once, severity first, with provenance', async () => {
116
+ configureRunFetch([
117
+ annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info', sequence: 1}),
118
+ annotationDto({
119
+ id: ANNOTATION_ID_TWO,
120
+ context: 'smoke check',
121
+ style: 'error',
122
+ sequence: 2,
123
+ body: 'Task nine **failed**.',
124
+ }),
125
+ ]);
126
+
127
+ renderView({tab: 'annotations'});
128
+
129
+ const headings = await screen.findAllByRole('heading', {level: 3});
130
+ expect(headings.map((heading) => heading.textContent)).toEqual(['smoke check', 'coverage']);
131
+ expect(screen.getByText(TASK_NINE_PATTERN)).toBeInTheDocument();
132
+ expect(screen.getAllByText('build · checkout · attempt 1')).toHaveLength(2);
133
+ expect(screen.getByText('2 annotations')).toBeInTheDocument();
134
+ });
135
+
136
+ test('links an annotation back to the step that emitted it', async () => {
137
+ configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage'})]);
138
+
139
+ renderView({tab: 'annotations'});
140
+
141
+ const href = (await screen.findByRole('link', {name: 'Open step'})).getAttribute('href') ?? '';
142
+ const [pathname, query = ''] = href.split('?');
143
+
144
+ expect(pathname).toBe(`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}/jobs/${BUILD_JOB_ID}`);
145
+ expect(Object.fromEntries(new URLSearchParams(query))).toEqual({
146
+ jobExecution: BUILD_EXECUTION_ID,
147
+ step: BUILD_STEP_ID,
148
+ stepAttempt: BUILD_ATTEMPT_ID,
149
+ runAttempt: '"1"',
150
+ });
151
+ });
152
+
153
+ test('shows a failed annotations read as an error, never as an empty run', async () => {
154
+ configureApiClient({
155
+ fetchImpl: vi.fn((input: RequestInfo | URL) =>
156
+ Promise.resolve(
157
+ requestUrl(input).includes('/annotations')
158
+ ? jsonResponse({code: 'internal'}, {status: 500})
159
+ : jsonResponse(workflowRunViewDetailDto()),
160
+ ),
161
+ ),
162
+ });
163
+
164
+ renderView({tab: 'annotations'});
165
+
166
+ expect(await screen.findByRole('button', {name: 'Retry loading annotations'})).toBeVisible();
167
+ expect(screen.queryByText('This run has no annotations to show.')).not.toBeInTheDocument();
168
+ });
169
+
170
+ test('bounds how many annotation bodies render at once', async () => {
171
+ configureRunFetch(
172
+ Array.from({length: 30}, (_unused, index) =>
173
+ annotationDto({
174
+ id: `aaaaaaaa-aaaa-4aaa-8aaa-${String(index).padStart(12, '0')}`,
175
+ context: `context-${index}`,
176
+ sequence: index + 1,
177
+ }),
178
+ ),
179
+ );
180
+
181
+ renderView({tab: 'annotations'});
182
+
183
+ expect(await screen.findAllByRole('heading', {level: 3})).toHaveLength(25);
184
+ const showMore = screen.getByRole('button', {name: 'Show 5 more of 30'});
185
+
186
+ await userEvent.click(showMore);
187
+
188
+ expect(screen.getAllByRole('heading', {level: 3})).toHaveLength(30);
189
+ expect(screen.queryByRole('button', {name: SHOW_MORE_PATTERN})).not.toBeInTheDocument();
190
+ });
191
+
192
+ test('focuses a deep-linked annotation beyond the render window', async () => {
193
+ const deepLinkedId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000029';
194
+ configureRunFetch(
195
+ Array.from({length: 30}, (_unused, index) =>
196
+ annotationDto({
197
+ id:
198
+ index === 29
199
+ ? deepLinkedId
200
+ : `aaaaaaaa-aaaa-4aaa-8aaa-${String(index).padStart(12, '0')}`,
201
+ context: `context-${index}`,
202
+ sequence: index + 1,
203
+ }),
204
+ ),
205
+ );
206
+
207
+ renderView({tab: 'annotations', selection: {annotation: deepLinkedId}});
208
+
209
+ const target = await screen.findByRole('heading', {level: 3, name: 'context-29'});
210
+ const selected = target.closest('li');
211
+ await waitFor(() => expect(selected).toHaveFocus());
212
+ expect(selected).toHaveAttribute('aria-current', 'true');
213
+ expect(selected).toHaveClass('shadow-border-interactive-with-active');
214
+ });
215
+
216
+ test('separates a filtered miss from a run with no annotations', async () => {
217
+ configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})]);
218
+
219
+ renderView({tab: 'annotations', selection: {severity: 'error'}});
220
+
221
+ expect(await screen.findByText('No matching annotations')).toBeInTheDocument();
222
+ expect(
223
+ screen.getByText('This run has annotations, but none at error severity.'),
224
+ ).toBeInTheDocument();
225
+ expect(screen.getByRole('button', {name: 'Clear filters'})).toBeInTheDocument();
226
+ });
227
+
228
+ test('does not claim a filtered miss beyond the loaded annotation budget', async () => {
229
+ configureRunFetch(
230
+ [annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})],
231
+ {},
232
+ {has_more: true, next_cursor: 'next-page'},
233
+ );
234
+
235
+ renderView({tab: 'annotations', selection: {severity: 'error'}});
236
+
237
+ expect(await screen.findByText('No matches in loaded annotations')).toBeInTheDocument();
238
+ expect(
239
+ screen.getByText(
240
+ 'None of the loaded annotations are at error severity. Load more annotations to continue searching.',
241
+ ),
242
+ ).toBeInTheDocument();
243
+ expect(screen.getByRole('button', {name: 'Load more annotations'})).toBeInTheDocument();
244
+ expect(screen.queryByText('No matching annotations')).not.toBeInTheDocument();
245
+ });
246
+
247
+ test('clears annotation filters and legacy step selection together', async () => {
248
+ const user = userEvent.setup();
249
+ configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})]);
250
+
251
+ const {router} = renderView({
252
+ tab: 'annotations',
253
+ selection: {
254
+ jobId: DEPLOY_JOB_ID,
255
+ jobExecutionId: BUILD_EXECUTION_ID,
256
+ stepId: BUILD_STEP_ID,
257
+ stepAttemptId: BUILD_ATTEMPT_ID,
258
+ severity: 'error',
259
+ annotation: ANNOTATION_ID_ONE,
260
+ },
261
+ });
262
+
263
+ await user.click(await screen.findByRole('button', {name: 'Clear filters'}));
264
+
265
+ await waitFor(() => expect(router.state.location.search).toEqual({tab: 'annotations'}));
266
+ });
267
+
268
+ test('blames only the filters that are actually active', async () => {
269
+ // The annotation belongs to build, so filtering to deploy is a job-only miss. Naming
270
+ // severity here would send the reader to change a control they never touched.
271
+ configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})]);
272
+
273
+ renderView({tab: 'annotations', selection: {jobId: DEPLOY_JOB_ID}});
274
+
275
+ expect(
276
+ await screen.findByText('This run has annotations, but none from deploy.'),
277
+ ).toBeInTheDocument();
278
+ });
279
+
115
280
  test('renders the captured workflow source in the Source section', async () => {
116
281
  configureApiClient({
117
282
  fetchImpl: vi.fn(() =>
@@ -132,9 +297,7 @@ describe('WorkflowRunView', () => {
132
297
  });
133
298
 
134
299
  test('explains when the run has no source snapshot', async () => {
135
- configureApiClient({
136
- fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
137
- });
300
+ configureRunFetch();
138
301
 
139
302
  renderView({tab: 'source'});
140
303
 
@@ -164,6 +327,48 @@ function renderView(props: Partial<Parameters<typeof WorkflowRunView>[0]> = {})
164
327
  ));
165
328
  }
166
329
 
330
+ /** The API client hands `fetchImpl` a `Request`, whose URL only `.url` exposes. */
331
+ function requestUrl(input: RequestInfo | URL): string {
332
+ if (typeof input === 'string') return input;
333
+ return input instanceof URL ? input.href : input.url;
334
+ }
335
+
336
+ /** Routes the run detail and the annotations read, which are two independent fetches. */
337
+ function configureRunFetch(
338
+ annotations: AnnotationDto[] = [],
339
+ runOverrides: Partial<WorkflowRunDetailResponseDto> = {},
340
+ annotationPageOverrides: Partial<{has_more: boolean; next_cursor: string | null}> = {},
341
+ ) {
342
+ configureApiClient({
343
+ fetchImpl: vi.fn((input: RequestInfo | URL) =>
344
+ Promise.resolve(
345
+ requestUrl(input).includes('/annotations')
346
+ ? jsonResponse({
347
+ annotations,
348
+ has_more: false,
349
+ next_cursor: null,
350
+ ...annotationPageOverrides,
351
+ })
352
+ : jsonResponse(workflowRunViewDetailDto(runOverrides)),
353
+ ),
354
+ ),
355
+ });
356
+ }
357
+
358
+ function annotationDto(overrides: Partial<AnnotationDto> & {id: string}): AnnotationDto {
359
+ return {
360
+ job_id: BUILD_JOB_ID,
361
+ job_execution_id: BUILD_EXECUTION_ID,
362
+ origin_step_id: BUILD_STEP_ID,
363
+ origin_step_attempt: 1,
364
+ context: 'default',
365
+ style: 'default',
366
+ sequence: 1,
367
+ body: 'Body',
368
+ ...overrides,
369
+ };
370
+ }
371
+
167
372
  function workflowRunViewDetailDto(
168
373
  overrides: Partial<WorkflowRunDetailResponseDto> = {},
169
374
  ): WorkflowRunDetailResponseDto {
@@ -181,7 +386,28 @@ function workflowRunViewDetailDto(
181
386
  run_attempt_id: RUN_ID,
182
387
  name: 'build',
183
388
  status: 'succeeded',
184
- steps: [workflowStepDto({name: 'checkout', status: 'succeeded'})],
389
+ job_executions: [
390
+ workflowJobExecutionDto({
391
+ id: BUILD_EXECUTION_ID,
392
+ job_id: BUILD_JOB_ID,
393
+ status: 'succeeded',
394
+ steps: [
395
+ workflowStepDto({
396
+ id: BUILD_STEP_ID,
397
+ name: 'checkout',
398
+ status: 'succeeded',
399
+ attempts: [
400
+ workflowStepAttemptDto({
401
+ id: BUILD_ATTEMPT_ID,
402
+ step_id: BUILD_STEP_ID,
403
+ attempt: 1,
404
+ status: 'succeeded',
405
+ }),
406
+ ],
407
+ }),
408
+ ],
409
+ }),
410
+ ],
185
411
  }),
186
412
  workflowJobDto({
187
413
  id: DEPLOY_JOB_ID,