@shipfox/api-agent-access 20.2.0 → 20.3.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 (38) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +16 -0
  5. package/dist/core/paged-tools.d.ts +15 -0
  6. package/dist/core/paged-tools.d.ts.map +1 -0
  7. package/dist/core/paged-tools.js +454 -0
  8. package/dist/core/paged-tools.js.map +1 -0
  9. package/dist/core/response.d.ts +22 -0
  10. package/dist/core/response.d.ts.map +1 -0
  11. package/dist/core/response.js +75 -0
  12. package/dist/core/response.js.map +1 -0
  13. package/dist/core/tools.d.ts +2 -0
  14. package/dist/core/tools.d.ts.map +1 -1
  15. package/dist/core/tools.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/presentation/mcp-server.d.ts.map +1 -1
  21. package/dist/presentation/mcp-server.js +23 -3
  22. package/dist/presentation/mcp-server.js.map +1 -1
  23. package/dist/presentation/routes.d.ts +10 -0
  24. package/dist/presentation/routes.d.ts.map +1 -1
  25. package/dist/presentation/routes.js +20 -3
  26. package/dist/presentation/routes.js.map +1 -1
  27. package/dist/tsconfig.test.tsbuildinfo +1 -1
  28. package/package.json +31 -14
  29. package/src/core/paged-tools.test.ts +519 -0
  30. package/src/core/paged-tools.ts +657 -0
  31. package/src/core/response.test.ts +65 -0
  32. package/src/core/response.ts +101 -0
  33. package/src/core/tools.ts +2 -0
  34. package/src/index.ts +11 -0
  35. package/src/presentation/mcp-server.test.ts +58 -0
  36. package/src/presentation/mcp-server.ts +25 -3
  37. package/src/presentation/routes.ts +37 -1
  38. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-agent-access",
3
3
  "license": "MIT",
4
- "version": "20.2.0",
4
+ "version": "20.3.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -11,31 +11,48 @@
11
11
  "type": "module",
12
12
  "main": "dist/index.js",
13
13
  "types": "dist/index.d.ts",
14
+ "imports": {
15
+ "#test/*": "./test/*",
16
+ "#*": {
17
+ "workspace-source": "./src/*",
18
+ "default": "./dist/*"
19
+ }
20
+ },
14
21
  "exports": {
15
22
  ".": {
16
- "types": "./dist/index.d.ts",
17
- "default": "./dist/index.js"
23
+ "workspace-source": {
24
+ "types": "./src/index.ts",
25
+ "default": "./src/index.ts"
26
+ },
27
+ "default": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
18
31
  }
19
32
  },
20
33
  "dependencies": {
21
34
  "@modelcontextprotocol/sdk": "1.29.0",
22
- "@shipfox/api-agent-access-dto": "20.2.0",
35
+ "@shipfox/api-agent-access-dto": "20.3.0",
36
+ "@shipfox/annotations-dto": "20.3.0",
23
37
  "@shipfox/api-auth-context": "20.2.0",
38
+ "@shipfox/api-definitions-dto": "20.3.0",
39
+ "@shipfox/api-projects-dto": "20.0.0",
40
+ "@shipfox/api-triggers-dto": "19.0.0",
41
+ "@shipfox/api-workflows-dto": "20.3.0",
42
+ "@shipfox/inter-module": "0.2.3",
43
+ "@shipfox/node-drizzle": "0.3.5",
24
44
  "@shipfox/node-error-monitoring": "0.3.0",
25
45
  "@shipfox/node-fastify": "0.4.4",
26
46
  "@shipfox/node-module": "1.0.9",
27
47
  "@shipfox/node-opentelemetry": "0.6.5"
28
48
  },
29
- "imports": {
30
- "#*": "./dist/*"
31
- },
32
- "shipfox": {
33
- "architecture": {
34
- "schema": 1,
35
- "realm": "source-available",
36
- "kind": "implementation",
37
- "context": "agent-access"
38
- }
49
+ "devDependencies": {
50
+ "@shipfox/biome": "1.10.1",
51
+ "@shipfox/depcruise": "1.0.5",
52
+ "@shipfox/swc": "1.2.8",
53
+ "@shipfox/ts-config": "1.3.9",
54
+ "@shipfox/typescript": "1.1.10",
55
+ "@shipfox/vitest": "1.2.6"
39
56
  },
40
57
  "scripts": {
41
58
  "build": "shipfox-swc",
@@ -0,0 +1,519 @@
1
+ import type {AnnotationsInterModuleClient} from '@shipfox/annotations-dto/inter-module';
2
+ import type {AgentAccessEnvelopeDto} from '@shipfox/api-agent-access-dto';
3
+ import {agentAccessEnvelopeSchema} from '@shipfox/api-agent-access-dto';
4
+ import type {AgentAccessContext} from '@shipfox/api-auth-context';
5
+ import {
6
+ type DefinitionsInterModuleClient,
7
+ definitionsInterModuleContract,
8
+ } from '@shipfox/api-definitions-dto/inter-module';
9
+ import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
10
+ import type {TriggersInterModuleClient} from '@shipfox/api-triggers-dto/inter-module';
11
+ import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
12
+ import {createInterModuleKnownError} from '@shipfox/inter-module';
13
+ import {decodeNumberIdCursor, decodeStringIdCursor} from '@shipfox/node-drizzle';
14
+ import {createAgentAccessTools} from './paged-tools.js';
15
+ import {serializedAgentAccessEnvelopeByteLength} from './response.js';
16
+
17
+ const workspaceId = uuid(1);
18
+ const projectId = uuid(2);
19
+ const definitionId = uuid(3);
20
+ const runId = uuid(4);
21
+ const annotationId = uuid(5);
22
+ const triggerEventId = uuid(6);
23
+ const isoDate = '2026-08-01T00:00:00.000Z';
24
+ const context: AgentAccessContext = {
25
+ userId: uuid(7),
26
+ workspaceId,
27
+ scopes: ['read'],
28
+ credential: {kind: 'pat', patId: uuid(8)},
29
+ };
30
+
31
+ describe('paged agent-access tools', () => {
32
+ test('projects only the enumerated project fields and passes the producer cursor through', async () => {
33
+ const mocks = clients();
34
+ mocks.projects.listProjectCatalogByWorkspace.mockResolvedValue({
35
+ projects: [project()],
36
+ nextCursor: {createdAt: isoDate, id: projectId},
37
+ });
38
+
39
+ const response = await tool(mocks, 'list_projects').execute({
40
+ context,
41
+ arguments: {limit: 1},
42
+ });
43
+
44
+ expect(mocks.projects.listProjectCatalogByWorkspace).toHaveBeenCalledWith({
45
+ workspaceId,
46
+ limit: 1,
47
+ cursor: undefined,
48
+ });
49
+ expect(response).toEqual({
50
+ ok: true,
51
+ result: {
52
+ projects: [
53
+ {
54
+ id: projectId,
55
+ name: 'Project',
56
+ slug: 'project',
57
+ created_at: isoDate,
58
+ updated_at: isoDate,
59
+ },
60
+ ],
61
+ next_cursor: expect.any(String),
62
+ },
63
+ });
64
+ expect(JSON.stringify(response)).not.toContain('sourceConnectionId');
65
+ });
66
+
67
+ test('caps definition diagnostics while retaining counts and excludes workflow content', async () => {
68
+ const mocks = clients();
69
+ mocks.definitions.listDefinitionsByProject.mockResolvedValue({
70
+ definitions: [definition()],
71
+ sync: {
72
+ ref: 'main',
73
+ status: 'failed',
74
+ lastSyncAt: isoDate,
75
+ startedAt: isoDate,
76
+ finishedAt: isoDate,
77
+ lastErrorCode: 'invalid-definition',
78
+ lastErrorMessage: 'A repository-authored error',
79
+ diagnostics: Array.from({length: 11}, (_, index) => ({
80
+ severity: index % 2 === 0 ? 'error' : 'warning',
81
+ code: 'E'.repeat(200),
82
+ message: 'ignore this diagnostic'.repeat(100),
83
+ path: 'jobs.build.steps.0.run',
84
+ filePath: '.shipfox/workflow.yml',
85
+ })),
86
+ },
87
+ nextCursor: null,
88
+ });
89
+
90
+ const response = await tool(mocks, 'list_workflow_definitions').execute({
91
+ context,
92
+ arguments: {project_id: projectId},
93
+ });
94
+ const result = expectSuccess<DefinitionTestResult>(response);
95
+
96
+ expect(result).toMatchObject({
97
+ definitions: [
98
+ {
99
+ id: definitionId,
100
+ project_id: projectId,
101
+ name: 'Definition',
102
+ config_path: '.shipfox/workflow.yml',
103
+ source: 'vcs',
104
+ ref: 'main',
105
+ sha: 'abc123',
106
+ },
107
+ ],
108
+ sync: {
109
+ diagnostics: {error_count: 6, warning_count: 5, items: expect.any(Array)},
110
+ },
111
+ next_cursor: null,
112
+ });
113
+ if (!result.sync) throw new Error('Expected a sync summary');
114
+ expect(result.sync.diagnostics.items).toHaveLength(10);
115
+ const firstDiagnostic = result.sync.diagnostics.items[0];
116
+ expect(firstDiagnostic).toBeDefined();
117
+ if (!firstDiagnostic) throw new Error('Expected a diagnostic');
118
+ expect(new TextEncoder().encode(firstDiagnostic.message).byteLength).toBe(512);
119
+ expect(JSON.stringify(result)).not.toContain('workflowDocument');
120
+ expect(JSON.stringify(result)).not.toContain('workflowModel');
121
+ });
122
+
123
+ test('regenerates a definition cursor from the uncapped producer name', async () => {
124
+ const mocks = clients();
125
+ const definitions = Array.from({length: 100}, (_, index) => ({
126
+ ...definition(),
127
+ id: uuid(300 + index),
128
+ name: `raw-definition-${index}-${'🙂'.repeat(800)}`,
129
+ configPath: 'p'.repeat(512),
130
+ ref: 'r'.repeat(512),
131
+ sha: 's'.repeat(512),
132
+ }));
133
+ mocks.definitions.listDefinitionsByProject.mockResolvedValue({
134
+ definitions,
135
+ sync: null,
136
+ nextCursor: null,
137
+ });
138
+
139
+ const response = await tool(mocks, 'list_workflow_definitions').execute({
140
+ context,
141
+ arguments: {project_id: projectId, limit: 100},
142
+ });
143
+ const result = expectSuccess<DefinitionPageTestResult>(response);
144
+ const last = result.definitions.at(-1);
145
+
146
+ expect(result.definitions.length).toBeLessThan(100);
147
+ expect(response).toMatchObject({ok: true, response_truncated: true});
148
+ expect(response.response_total_bytes).toBeGreaterThan(128 * 1024);
149
+ expect(last).toBeDefined();
150
+ expect(decodeStringIdCursor(result.next_cursor ?? undefined)).toEqual({
151
+ value: definitions[result.definitions.length - 1]?.name,
152
+ id: last?.id,
153
+ });
154
+ expect(serializedAgentAccessEnvelopeByteLength(response)).toBeLessThanOrEqual(128 * 1024);
155
+ });
156
+
157
+ test('checks project ownership before listing runs and excludes payload, inputs, and jobs', async () => {
158
+ const mocks = clients();
159
+ mocks.projects.requireProjectForWorkspace.mockResolvedValue({project: project()});
160
+ mocks.workflows.listWorkflowRuns.mockResolvedValue({
161
+ runs: [run()],
162
+ nextCursor: null,
163
+ filteredTotalCount: 1,
164
+ });
165
+
166
+ const response = await tool(mocks, 'list_workflow_runs').execute({
167
+ context,
168
+ arguments: {project_id: projectId, status: 'failed', trigger_source: 'push'},
169
+ });
170
+ const result = expectSuccess<RunTestResult>(response);
171
+
172
+ expect(mocks.projects.requireProjectForWorkspace).toHaveBeenCalledWith({
173
+ workspaceId,
174
+ projectId,
175
+ });
176
+ expect(mocks.workflows.listWorkflowRuns).toHaveBeenCalledWith({
177
+ workspaceId,
178
+ projectId,
179
+ limit: 50,
180
+ cursor: undefined,
181
+ filters: {status: 'failed', triggerSource: 'push'},
182
+ });
183
+ expect(result.runs[0]).not.toHaveProperty('trigger_payload');
184
+ expect(result.runs[0]).not.toHaveProperty('inputs');
185
+ expect(result.runs[0]).not.toHaveProperty('jobs');
186
+ });
187
+
188
+ test('resolves the latest annotation attempt and marks UTF-8 body truncation', async () => {
189
+ const mocks = clients();
190
+ mocks.workflows.getLatestRunAttempt.mockResolvedValue({attempt: 2});
191
+ mocks.annotations.listAnnotationsForRunAttempt.mockResolvedValue({
192
+ annotations: [
193
+ {
194
+ id: annotationId,
195
+ job_id: uuid(9),
196
+ job_execution_id: uuid(10),
197
+ origin_step_id: uuid(11),
198
+ origin_step_attempt: 1,
199
+ context: 'failure',
200
+ style: 'error',
201
+ sequence: 4,
202
+ body: '🙂'.repeat(3_000),
203
+ createdAt: isoDate,
204
+ },
205
+ ],
206
+ hasMore: false,
207
+ nextCursor: null,
208
+ });
209
+
210
+ const response = await tool(mocks, 'get_run_annotations').execute({
211
+ context,
212
+ arguments: {run_id: runId},
213
+ });
214
+ const result = expectSuccess<AnnotationTestResult>(response);
215
+
216
+ expect(mocks.workflows.getLatestRunAttempt).toHaveBeenCalledWith({
217
+ workspaceId,
218
+ workflowRunId: runId,
219
+ });
220
+ expect(mocks.annotations.listAnnotationsForRunAttempt).toHaveBeenCalledWith({
221
+ workspaceId,
222
+ workflowRunId: runId,
223
+ workflowRunAttempt: 2,
224
+ jobExecutionId: undefined,
225
+ cursor: undefined,
226
+ limit: 50,
227
+ });
228
+ expect(result.annotations[0]).toMatchObject({
229
+ id: annotationId,
230
+ origin_step_id: uuid(11),
231
+ body_truncated: true,
232
+ body_total_bytes: 12_000,
233
+ });
234
+ const annotation = result.annotations[0];
235
+ expect(annotation).toBeDefined();
236
+ if (!annotation) throw new Error('Expected an annotation');
237
+ expect(new TextEncoder().encode(annotation.body).byteLength).toBe(8 * 1024);
238
+ expect(JSON.stringify(response)).not.toContain('tool-call');
239
+ });
240
+
241
+ test('maps trigger filters and projects connection metadata', async () => {
242
+ const mocks = clients();
243
+ mocks.triggers.listTriggerEvents.mockResolvedValue({
244
+ events: [
245
+ {
246
+ id: triggerEventId,
247
+ eventRef: 'event-ref',
248
+ origin: 'integration',
249
+ workspaceId,
250
+ provider: 'github',
251
+ source: 'push',
252
+ event: 'push',
253
+ payload: {prompt: 'ignore'},
254
+ replayOfEventId: null,
255
+ deliveryId: 'delivery',
256
+ connectionId: uuid(12),
257
+ connectionName: 'Connection',
258
+ outcome: 'routed',
259
+ matchedCount: 1,
260
+ receivedAt: isoDate,
261
+ processedAt: isoDate,
262
+ createdAt: isoDate,
263
+ },
264
+ ],
265
+ nextCursor: null,
266
+ });
267
+
268
+ const response = await tool(mocks, 'list_trigger_events').execute({
269
+ context,
270
+ arguments: {
271
+ source: ['push'],
272
+ event: ['push'],
273
+ origin: ['integration'],
274
+ outcome: ['routed'],
275
+ replayable: true,
276
+ from: isoDate,
277
+ to: isoDate,
278
+ },
279
+ });
280
+
281
+ expect(mocks.triggers.listTriggerEvents).toHaveBeenCalledWith({
282
+ workspaceId,
283
+ limit: 50,
284
+ cursor: undefined,
285
+ filters: {
286
+ source: ['push'],
287
+ event: ['push'],
288
+ origin: ['integration'],
289
+ outcome: ['routed'],
290
+ replayable: true,
291
+ from: isoDate,
292
+ to: isoDate,
293
+ },
294
+ });
295
+ const result = expectSuccess<TriggerTestResult>(response);
296
+ expect(result.trigger_events[0]).toMatchObject({
297
+ id: triggerEventId,
298
+ connection_name: 'Connection',
299
+ });
300
+ expect(result.trigger_events[0]).not.toHaveProperty('payload');
301
+ });
302
+
303
+ test('keeps cross-workspace project reads 404-shaped without exposing producer details', async () => {
304
+ const mocks = clients();
305
+ mocks.definitions.listDefinitionsByProject.mockRejectedValue(
306
+ createInterModuleKnownError(
307
+ definitionsInterModuleContract.methods.listDefinitionsByProject,
308
+ 'project-workspace-mismatch',
309
+ {projectId, workspaceId},
310
+ ),
311
+ );
312
+
313
+ const response = await tool(mocks, 'list_workflow_definitions').execute({
314
+ context,
315
+ arguments: {project_id: projectId},
316
+ });
317
+
318
+ expect(response).toEqual({ok: false, error: {code: 'not-found'}});
319
+ });
320
+
321
+ test('does not query annotations for a run from another workspace', async () => {
322
+ const mocks = clients();
323
+ mocks.workflows.getLatestRunAttempt.mockResolvedValue({attempt: null});
324
+
325
+ const response = await tool(mocks, 'get_run_annotations').execute({
326
+ context,
327
+ arguments: {run_id: runId},
328
+ });
329
+
330
+ expect(response).toEqual({ok: false, error: {code: 'not-found'}});
331
+ expect(mocks.annotations.listAnnotationsForRunAttempt).not.toHaveBeenCalled();
332
+ });
333
+
334
+ test('reduces an oversized annotation page and anchors its cursor to the retained item', async () => {
335
+ const mocks = clients();
336
+ mocks.workflows.getLatestRunAttempt.mockResolvedValue({attempt: 1});
337
+ const annotations = Array.from({length: 100}, (_, index) => ({
338
+ id: uuid(100 + index),
339
+ job_id: uuid(200),
340
+ job_execution_id: uuid(201),
341
+ origin_step_id: uuid(202),
342
+ origin_step_attempt: 1,
343
+ context: `annotation-${index}`,
344
+ style: 'info' as const,
345
+ sequence: index + 1,
346
+ body: 'x'.repeat(9_000),
347
+ createdAt: isoDate,
348
+ }));
349
+ mocks.annotations.listAnnotationsForRunAttempt.mockResolvedValue({
350
+ annotations,
351
+ hasMore: false,
352
+ nextCursor: null,
353
+ });
354
+
355
+ const response = await tool(mocks, 'get_run_annotations').execute({
356
+ context,
357
+ arguments: {run_id: runId, limit: 100},
358
+ });
359
+ const result = expectSuccess<AnnotationPageTestResult>(response);
360
+ const last = result.annotations.at(-1);
361
+
362
+ expect(result.annotations.length).toBeLessThan(100);
363
+ expect(response).toMatchObject({ok: true, response_truncated: true});
364
+ expect(response.response_total_bytes).toBeGreaterThan(128 * 1024);
365
+ expect(last).toBeDefined();
366
+ expect(result.next_cursor).toBeDefined();
367
+ expect(decodeNumberIdCursor(result.next_cursor ?? undefined)).toEqual({
368
+ value: last?.sequence,
369
+ id: last?.id,
370
+ });
371
+ expect(serializedAgentAccessEnvelopeByteLength(response)).toBeLessThanOrEqual(128 * 1024);
372
+ });
373
+ });
374
+
375
+ function clients() {
376
+ return {
377
+ projects: {
378
+ listProjectCatalogByWorkspace: vi.fn(),
379
+ requireProjectForWorkspace: vi.fn(),
380
+ } as unknown as ProjectsModuleClient & {
381
+ listProjectCatalogByWorkspace: ReturnType<typeof vi.fn>;
382
+ requireProjectForWorkspace: ReturnType<typeof vi.fn>;
383
+ },
384
+ definitions: {
385
+ listDefinitionsByProject: vi.fn(),
386
+ } as unknown as DefinitionsInterModuleClient & {
387
+ listDefinitionsByProject: ReturnType<typeof vi.fn>;
388
+ },
389
+ workflows: {
390
+ listWorkflowRuns: vi.fn(),
391
+ getLatestRunAttempt: vi.fn(),
392
+ getWorkflowRunDetail: vi.fn(),
393
+ } as unknown as WorkflowsModuleClient & {
394
+ listWorkflowRuns: ReturnType<typeof vi.fn>;
395
+ getLatestRunAttempt: ReturnType<typeof vi.fn>;
396
+ getWorkflowRunDetail: ReturnType<typeof vi.fn>;
397
+ },
398
+ annotations: {
399
+ listAnnotationsForRunAttempt: vi.fn(),
400
+ } as unknown as AnnotationsInterModuleClient & {
401
+ listAnnotationsForRunAttempt: ReturnType<typeof vi.fn>;
402
+ },
403
+ triggers: {
404
+ listTriggerEvents: vi.fn(),
405
+ } as unknown as TriggersInterModuleClient & {
406
+ listTriggerEvents: ReturnType<typeof vi.fn>;
407
+ },
408
+ };
409
+ }
410
+
411
+ function tool(mocks: ReturnType<typeof clients>, name: string) {
412
+ const result = createAgentAccessTools(mocks).find((candidate) => candidate.name === name);
413
+ if (!result) throw new Error(`Missing tool ${name}`);
414
+ return result;
415
+ }
416
+
417
+ function expectSuccess<T>(response: AgentAccessEnvelopeDto): T {
418
+ expect(response.ok).toBe(true);
419
+ if (!response.ok) throw new Error('Expected a success response');
420
+ expect(agentAccessEnvelopeSchema.safeParse(response).success).toBe(true);
421
+ return response.result as T;
422
+ }
423
+
424
+ type DefinitionTestResult = {
425
+ definitions: Array<Record<string, unknown>>;
426
+ sync: {diagnostics: {items: Array<{message: string}>}} | null;
427
+ next_cursor: string | null;
428
+ };
429
+
430
+ type DefinitionPageTestResult = {
431
+ definitions: Array<{id: string}>;
432
+ next_cursor: string | null;
433
+ };
434
+
435
+ type RunTestResult = {
436
+ runs: Array<Record<string, unknown>>;
437
+ };
438
+
439
+ type AnnotationTestResult = {
440
+ annotations: Array<{body: string}>;
441
+ };
442
+
443
+ type AnnotationPageTestResult = {
444
+ annotations: Array<{id: string; sequence: number}>;
445
+ next_cursor: string | null;
446
+ };
447
+
448
+ type TriggerTestResult = {
449
+ trigger_events: Array<Record<string, unknown>>;
450
+ };
451
+
452
+ function project(id = projectId, name = 'Project', slug = 'project') {
453
+ return {
454
+ id,
455
+ workspaceId,
456
+ sourceConnectionId: uuid(20),
457
+ sourceExternalRepositoryId: 'external-repository',
458
+ sourceRepositoryOwner: 'shipfox',
459
+ sourceRepositoryName: 'platform',
460
+ sourceDefaultBranch: 'main',
461
+ name,
462
+ slug,
463
+ createdAt: isoDate,
464
+ updatedAt: isoDate,
465
+ };
466
+ }
467
+
468
+ function definition() {
469
+ return {
470
+ id: definitionId,
471
+ projectId,
472
+ configPath: '.shipfox/workflow.yml',
473
+ source: 'vcs' as const,
474
+ sha: 'abc123',
475
+ ref: 'main',
476
+ name: 'Definition',
477
+ workflowDocument: {prompt: 'ignore'},
478
+ workflowModel: {jobs: {build: {steps: []}}},
479
+ manualTrigger: null,
480
+ fetchedAt: isoDate,
481
+ createdAt: isoDate,
482
+ updatedAt: isoDate,
483
+ };
484
+ }
485
+
486
+ function run() {
487
+ return {
488
+ id: runId,
489
+ project_id: projectId,
490
+ definition_id: definitionId,
491
+ number: 1,
492
+ name: 'Run',
493
+ workflow_name: 'Workflow',
494
+ status: 'failed' as const,
495
+ origin: 'synced' as const,
496
+ dev_source: null,
497
+ current_attempt: 1,
498
+ latest_attempt: 1,
499
+ trigger_provider: 'github',
500
+ trigger_source: 'push',
501
+ trigger_event: 'push',
502
+ trigger_payload: {prompt: 'ignore'},
503
+ trigger_reference: {repository: 'shipfox/platform', ref: 'main', commit: 'abc', actor: 'noe'},
504
+ inputs: {prompt: 'ignore'},
505
+ source_snapshot: null,
506
+ created_at: isoDate,
507
+ updated_at: isoDate,
508
+ started_at: isoDate,
509
+ finished_at: isoDate,
510
+ jobs: [],
511
+ job_status_counts: [{status: 'failed' as const, count: 1}],
512
+ job_display_status_counts: [],
513
+ has_started_job_execution: true,
514
+ };
515
+ }
516
+
517
+ function uuid(value: number): string {
518
+ return `00000000-0000-4000-8000-${String(value).padStart(12, '0')}`;
519
+ }