@shipfox/api-workflows 12.1.0 → 12.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.
Files changed (73) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +29 -0
  3. package/dist/core/entities/job.d.ts +1 -1
  4. package/dist/core/entities/job.d.ts.map +1 -1
  5. package/dist/core/entities/step.d.ts +2 -2
  6. package/dist/core/entities/step.d.ts.map +1 -1
  7. package/dist/core/errors.d.ts +16 -0
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +18 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/step-config/assemble-run-context.d.ts +15 -5
  12. package/dist/core/step-config/assemble-run-context.d.ts.map +1 -1
  13. package/dist/core/step-config/assemble-run-context.js +77 -27
  14. package/dist/core/step-config/assemble-run-context.js.map +1 -1
  15. package/dist/core/step-config/fields.d.ts +5 -0
  16. package/dist/core/step-config/fields.d.ts.map +1 -1
  17. package/dist/core/step-config/fields.js +21 -3
  18. package/dist/core/step-config/fields.js.map +1 -1
  19. package/dist/core/step-config/index.d.ts +1 -1
  20. package/dist/core/step-config/index.d.ts.map +1 -1
  21. package/dist/core/step-config/index.js +1 -1
  22. package/dist/core/step-config/index.js.map +1 -1
  23. package/dist/core/step-config/job-output-limits.d.ts +11 -0
  24. package/dist/core/step-config/job-output-limits.d.ts.map +1 -0
  25. package/dist/core/step-config/job-output-limits.js +88 -0
  26. package/dist/core/step-config/job-output-limits.js.map +1 -0
  27. package/dist/core/step-config/materialize-workflow-model.d.ts +1 -1
  28. package/dist/core/step-config/materialize-workflow-model.d.ts.map +1 -1
  29. package/dist/core/step-config/materialize-workflow-model.js +41 -14
  30. package/dist/core/step-config/materialize-workflow-model.js.map +1 -1
  31. package/dist/db/db.d.ts +50 -50
  32. package/dist/db/job-listeners.d.ts.map +1 -1
  33. package/dist/db/job-listeners.js +6 -5
  34. package/dist/db/job-listeners.js.map +1 -1
  35. package/dist/db/schema/job-executions.d.ts +3 -3
  36. package/dist/db/schema/job-listener-events.d.ts +1 -1
  37. package/dist/db/schema/jobs.d.ts +7 -7
  38. package/dist/db/schema/outbox.d.ts +1 -1
  39. package/dist/db/schema/step-attempts.d.ts +2 -2
  40. package/dist/db/schema/steps.d.ts +5 -5
  41. package/dist/db/schema/workflow-run-attempts.d.ts +3 -3
  42. package/dist/db/schema/workflow-run-counters.d.ts +1 -1
  43. package/dist/db/schema/workflow-runs.d.ts +2 -2
  44. package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
  45. package/dist/db/workflow-runs/job-executions.js +4 -2
  46. package/dist/db/workflow-runs/job-executions.js.map +1 -1
  47. package/dist/db/workflow-runs/jobs.d.ts.map +1 -1
  48. package/dist/db/workflow-runs/jobs.js +29 -3
  49. package/dist/db/workflow-runs/jobs.js.map +1 -1
  50. package/dist/presentation/routes/project-access.d.ts +1 -1
  51. package/dist/temporal/activities/index.d.ts +1 -1
  52. package/dist/temporal/activities/index.d.ts.map +1 -1
  53. package/dist/temporal/activities/orchestration-activities.d.ts +1 -1
  54. package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
  55. package/dist/temporal/workflows/test-env.d.ts +2 -2
  56. package/dist/tsconfig.test.tsbuildinfo +1 -1
  57. package/package.json +16 -16
  58. package/src/core/errors.ts +35 -0
  59. package/src/core/step-config/assemble-run-context.test.ts +232 -20
  60. package/src/core/step-config/assemble-run-context.ts +126 -28
  61. package/src/core/step-config/fields.ts +42 -3
  62. package/src/core/step-config/index.ts +2 -0
  63. package/src/core/step-config/job-output-limits.ts +130 -0
  64. package/src/core/step-config/materialize-workflow-model.test.ts +306 -1
  65. package/src/core/step-config/materialize-workflow-model.ts +60 -15
  66. package/src/db/job-listeners.ts +14 -3
  67. package/src/db/workflow-runs/job-executions.test.ts +207 -1
  68. package/src/db/workflow-runs/job-executions.ts +15 -2
  69. package/src/db/workflow-runs/jobs.test.ts +49 -0
  70. package/src/db/workflow-runs/jobs.ts +46 -6
  71. package/src/db/workflow-runs/run-create.test.ts +56 -0
  72. package/test/factories/workflow-model.ts +2 -0
  73. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-workflows",
3
3
  "license": "MIT",
4
- "version": "12.1.0",
4
+ "version": "12.2.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -22,29 +22,29 @@
22
22
  "@temporalio/workflow": "1.18.1",
23
23
  "drizzle-orm": "^0.45.2",
24
24
  "zod": "^4.4.3",
25
- "@shipfox/api-auth-context": "12.0.0",
25
+ "@shipfox/api-auth-context": "12.2.0",
26
26
  "@shipfox/api-auth-dto": "12.0.0",
27
- "@shipfox/api-agent-dto": "12.0.0",
27
+ "@shipfox/api-agent-dto": "12.2.0",
28
28
  "@shipfox/annotations-dto": "12.0.0",
29
- "@shipfox/api-definitions-dto": "12.0.0",
30
- "@shipfox/api-projects-dto": "12.0.0",
31
- "@shipfox/api-integration-core-dto": "12.0.0",
32
- "@shipfox/api-runners-dto": "12.0.0",
29
+ "@shipfox/api-definitions-dto": "12.2.0",
30
+ "@shipfox/api-projects-dto": "12.2.0",
31
+ "@shipfox/api-integration-core-dto": "12.2.0",
32
+ "@shipfox/api-runners-dto": "12.2.0",
33
33
  "@shipfox/api-secrets-dto": "12.0.0",
34
- "@shipfox/api-workflows-dto": "12.1.0",
35
- "@shipfox/api-workspaces-dto": "12.0.0",
34
+ "@shipfox/api-workflows-dto": "12.2.0",
36
35
  "@shipfox/inter-module": "0.2.3",
37
- "@shipfox/expression": "2.0.0",
36
+ "@shipfox/api-workspaces-dto": "12.0.0",
37
+ "@shipfox/expression": "2.1.0",
38
38
  "@shipfox/node-drizzle": "0.3.5",
39
39
  "@shipfox/node-error-monitoring": "0.3.0",
40
- "@shipfox/node-fastify": "0.4.1",
41
- "@shipfox/node-module": "1.0.5",
42
- "@shipfox/node-opentelemetry": "0.6.3",
40
+ "@shipfox/node-fastify": "0.4.2",
41
+ "@shipfox/node-module": "1.0.6",
42
+ "@shipfox/node-opentelemetry": "0.6.4",
43
43
  "@shipfox/node-outbox": "0.2.6",
44
44
  "@shipfox/node-postgres": "0.5.0",
45
- "@shipfox/node-temporal": "0.4.4",
46
- "@shipfox/runner-labels": "0.1.3",
47
- "@shipfox/workflow-document": "3.0.0"
45
+ "@shipfox/node-temporal": "0.4.5",
46
+ "@shipfox/workflow-document": "3.0.1",
47
+ "@shipfox/runner-labels": "0.2.0"
48
48
  },
49
49
  "imports": {
50
50
  "#*": "./dist/*"
@@ -140,6 +140,41 @@ export class InvalidJobRunnerLabelsError extends Error {
140
140
  }
141
141
  }
142
142
 
143
+ export class JobOutputTooLargeError extends Error {
144
+ constructor(
145
+ readonly outputKey: string,
146
+ readonly limitBytes: number,
147
+ readonly scope: 'value' | 'total',
148
+ ) {
149
+ super(
150
+ scope === 'total'
151
+ ? `Job outputs exceed the ${limitBytes}-byte total limit (at "${outputKey}")`
152
+ : `Job output "${outputKey}" exceeds the ${limitBytes}-byte size limit`,
153
+ );
154
+ this.name = 'JobOutputTooLargeError';
155
+ }
156
+ }
157
+
158
+ export class JobOutputTooManyEntriesError extends Error {
159
+ constructor(
160
+ readonly entryCount: number,
161
+ readonly limitEntries: number,
162
+ ) {
163
+ super(`Job outputs cannot define more than ${limitEntries} entries (found ${entryCount})`);
164
+ this.name = 'JobOutputTooManyEntriesError';
165
+ }
166
+ }
167
+
168
+ export class JobOutputNotJsonSafeError extends Error {
169
+ constructor(
170
+ readonly outputKey: string,
171
+ readonly reason: string,
172
+ ) {
173
+ super(`Job output "${outputKey}" cannot be persisted as JSON: ${reason}`);
174
+ this.name = 'JobOutputNotJsonSafeError';
175
+ }
176
+ }
177
+
143
178
  // The job named by a lease is terminal, so it must not exchange its lease for
144
179
  // fresh checkout credentials. Server state is the final gate, not the token.
145
180
  export class JobNotActiveError extends Error {
@@ -1,4 +1,8 @@
1
- import {getWorkflowPredicateContextRoots} from '@shipfox/expression';
1
+ import {
2
+ createWorkflowExpression,
3
+ evaluateWorkflowExpression,
4
+ getWorkflowPredicateContextRoots,
5
+ } from '@shipfox/expression';
2
6
  import type {JobExecution} from '#core/entities/job-execution.js';
3
7
  import type {Step, StepAttempt} from '#core/entities/step.js';
4
8
  import {
@@ -9,14 +13,94 @@ import {
9
13
  assembleGateContext,
10
14
  assembleJobActivationContext,
11
15
  assembleJobResolutionContext,
16
+ assembleJobsContext,
12
17
  assembleListenerSnapshotContext,
13
18
  assembleStepDispatchContext,
14
19
  assembleWorkflowRunContext,
20
+ listenerFilterOutputTypesForJobs,
15
21
  planListenerFilterSnapshots,
16
22
  } from './assemble-run-context.js';
17
23
 
18
24
  const date = new Date('2026-06-30T12:00:00.000Z');
19
25
 
26
+ describe('assembleJobsContext', () => {
27
+ it.each([
28
+ {persistedValue: 42, expectedValue: 43n},
29
+ {persistedValue: '9007199254740993', expectedValue: 9007199254740994n},
30
+ ])('rehydrates persisted int outputs for CEL arithmetic', ({persistedValue, expectedValue}) => {
31
+ const context = assembleJobsContext([
32
+ {
33
+ job: {key: 'review', status: 'succeeded', outputs: {count: persistedValue}},
34
+ outputTypes: {count: 'int'},
35
+ executions: [],
36
+ },
37
+ ]);
38
+ const expression = createWorkflowExpression({
39
+ source: `jobs.review.outputs.count + 1 == ${expectedValue.toString()}`,
40
+ check: {mode: 'syntax'},
41
+ });
42
+
43
+ const result = evaluateWorkflowExpression(expression, context);
44
+
45
+ expect(result).toBe(true);
46
+ });
47
+
48
+ it('rehydrates persisted timestamp outputs for CEL comparison', () => {
49
+ const persistedValue = '2026-06-30T12:00:00.000Z';
50
+ const context = assembleJobsContext([
51
+ {
52
+ job: {key: 'review', status: 'succeeded', outputs: {createdAt: persistedValue}},
53
+ outputTypes: {createdAt: 'timestamp'},
54
+ executions: [],
55
+ },
56
+ ]);
57
+ const expression = createWorkflowExpression({
58
+ source: 'jobs.review.outputs.createdAt < timestamp("2026-07-01T00:00:00Z")',
59
+ check: {mode: 'syntax'},
60
+ });
61
+
62
+ const result = evaluateWorkflowExpression(expression, context);
63
+
64
+ expect(result).toBe(true);
65
+ });
66
+
67
+ it('rehydrates persisted execution int outputs for CEL arithmetic', () => {
68
+ const context = assembleJobsContext([
69
+ {
70
+ job: {key: 'review', status: 'succeeded', outputs: {}},
71
+ outputTypes: {count: 'int'},
72
+ executions: [jobExecution({outputs: {count: 42}})],
73
+ },
74
+ ]);
75
+ const expression = createWorkflowExpression({
76
+ source: 'jobs.review.executions[0].outputs.count + 1 == 43',
77
+ check: {mode: 'syntax'},
78
+ });
79
+
80
+ const result = evaluateWorkflowExpression(expression, context);
81
+
82
+ expect(result).toBe(true);
83
+ });
84
+
85
+ it('rehydrates persisted execution timestamp outputs for CEL comparison', () => {
86
+ const context = assembleJobsContext([
87
+ {
88
+ job: {key: 'review', status: 'succeeded', outputs: {}},
89
+ outputTypes: {createdAt: 'timestamp'},
90
+ executions: [jobExecution({outputs: {createdAt: '2026-06-30T12:00:00.000Z'}})],
91
+ },
92
+ ]);
93
+ const expression = createWorkflowExpression({
94
+ source: 'jobs.review.executions[0].outputs.createdAt < timestamp("2026-07-01T00:00:00Z")',
95
+ check: {mode: 'syntax'},
96
+ });
97
+
98
+ const result = evaluateWorkflowExpression(expression, context);
99
+
100
+ expect(result).toBe(true);
101
+ });
102
+ });
103
+
20
104
  describe('assembleWorkflowRunContext', () => {
21
105
  const run = {
22
106
  id: 'run-1',
@@ -48,7 +132,7 @@ describe('assembleWorkflowRunContext', () => {
48
132
  },
49
133
  run: {
50
134
  id: 'run-1',
51
- number: 1,
135
+ number: 1n,
52
136
  name: 'Build',
53
137
  project_id: 'proj-1',
54
138
  workspace_id: 'workspace-1',
@@ -224,16 +308,16 @@ describe('assembleExecutionCreationContext', () => {
224
308
  }),
225
309
  executions: [
226
310
  {
227
- index: 0,
311
+ index: 0n,
228
312
  name: 'Build #1',
229
313
  status: 'failed',
230
314
  started_at: date,
231
315
  finished_at: date,
232
- events: prior.triggerEvents,
316
+ events: prior.triggerEvents.map((event) => ({...event, received_at: date})),
233
317
  outputs: {},
234
318
  },
235
319
  {
236
- index: 1,
320
+ index: 1n,
237
321
  name: 'Build #2',
238
322
  status: 'pending',
239
323
  started_at: null,
@@ -243,7 +327,7 @@ describe('assembleExecutionCreationContext', () => {
243
327
  source: 'github',
244
328
  event: 'deployment',
245
329
  delivery_id: 'delivery-2',
246
- received_at: '2026-06-30T12:01:00.000Z',
330
+ received_at: new Date('2026-06-30T12:01:00.000Z'),
247
331
  project: null,
248
332
  repository: null,
249
333
  ref: null,
@@ -256,7 +340,7 @@ describe('assembleExecutionCreationContext', () => {
256
340
  ],
257
341
  job: {key: 'build', name: 'Build'},
258
342
  execution: {
259
- index: 1,
343
+ index: 1n,
260
344
  name: 'Build #2',
261
345
  status: 'pending',
262
346
  started_at: null,
@@ -266,7 +350,7 @@ describe('assembleExecutionCreationContext', () => {
266
350
  source: 'github',
267
351
  event: 'deployment',
268
352
  delivery_id: 'delivery-2',
269
- received_at: '2026-06-30T12:01:00.000Z',
353
+ received_at: new Date('2026-06-30T12:01:00.000Z'),
270
354
  project: null,
271
355
  repository: null,
272
356
  ref: null,
@@ -346,12 +430,15 @@ describe('assembleJobActivationContext', () => {
346
430
  outputs: {image: 'app:123'},
347
431
  executions: [
348
432
  {
349
- index: 0,
433
+ index: 0n,
350
434
  name: 'Build #1',
351
435
  status: 'succeeded',
352
436
  started_at: date,
353
437
  finished_at: null,
354
- events: buildExecution.triggerEvents,
438
+ events: buildExecution.triggerEvents.map((event) => ({
439
+ ...event,
440
+ received_at: date,
441
+ })),
355
442
  outputs: {sha: 'abc123'},
356
443
  },
357
444
  ],
@@ -370,12 +457,15 @@ describe('assembleJobActivationContext', () => {
370
457
  outputs: {image: 'app:123'},
371
458
  executions: [
372
459
  {
373
- index: 0,
460
+ index: 0n,
374
461
  name: 'Build #1',
375
462
  status: 'succeeded',
376
463
  started_at: date,
377
464
  finished_at: null,
378
- events: buildExecution.triggerEvents,
465
+ events: buildExecution.triggerEvents.map((event) => ({
466
+ ...event,
467
+ received_at: date,
468
+ })),
379
469
  outputs: {sha: 'abc123'},
380
470
  },
381
471
  ],
@@ -501,6 +591,59 @@ describe('listener filter snapshots', () => {
501
591
  expect(matcher?.filter_snapshot).toEqual({vars: {ENABLED: 'true'}});
502
592
  });
503
593
 
594
+ it('persists output types beside JSON-safe listener snapshots', () => {
595
+ const plan = planListenerFilterSnapshots({
596
+ on: [
597
+ {
598
+ source: 'github',
599
+ event: 'pull_request',
600
+ filter: 'jobs.build.outputs.details.count + 1 == 43',
601
+ },
602
+ ],
603
+ until: null,
604
+ });
605
+ const dependencyJobs = [
606
+ {
607
+ job: {
608
+ key: 'build',
609
+ status: 'succeeded' as const,
610
+ outputs: {details: {count: 42}},
611
+ },
612
+ outputTypes: {
613
+ details: {kind: 'object' as const, fields: {count: 'int' as const}},
614
+ },
615
+ executions: [],
616
+ },
617
+ ];
618
+ const context = assembleListenerSnapshotContext({
619
+ job: {key: 'await'},
620
+ run,
621
+ triggerPayload,
622
+ plan,
623
+ dependencyJobs,
624
+ });
625
+
626
+ const [matcher] = applyListenerFilterSnapshots(
627
+ plan.on,
628
+ context,
629
+ listenerFilterOutputTypesForJobs(dependencyJobs),
630
+ );
631
+
632
+ expect(matcher).toEqual({
633
+ source: 'github',
634
+ event: 'pull_request',
635
+ filter: 'jobs.build.outputs.details.count + 1 == 43',
636
+ filter_snapshot: {
637
+ jobs: {
638
+ build: expect.objectContaining({outputs: {details: {count: 42}}}),
639
+ },
640
+ },
641
+ filter_output_types: {
642
+ build: {details: {kind: 'object', fields: {count: 'int'}}},
643
+ },
644
+ });
645
+ });
646
+
504
647
  it('omits snapshots for event-only filters and malformed filters', () => {
505
648
  const plan = planListenerFilterSnapshots({
506
649
  on: [
@@ -606,6 +749,39 @@ describe('listener filter snapshots', () => {
606
749
  },
607
750
  });
608
751
  });
752
+
753
+ it('keeps snapshots JSON-serializable so they survive the outbox payload', () => {
754
+ const plan = planListenerFilterSnapshots({
755
+ on: [
756
+ {
757
+ source: 'github',
758
+ event: 'pull_request',
759
+ filter: 'jobs.build.executions[0].index == 0 && run.number == 1',
760
+ },
761
+ ],
762
+ until: null,
763
+ });
764
+ const context = assembleListenerSnapshotContext({
765
+ job: {key: 'await'},
766
+ run,
767
+ triggerPayload,
768
+ plan,
769
+ dependencyJobs: [
770
+ {
771
+ job: {key: 'build', status: 'succeeded', outputs: {}},
772
+ executions: [jobExecution({id: 'exec-build', jobId: 'job-build'})],
773
+ },
774
+ ],
775
+ });
776
+
777
+ const [matcher] = applyListenerFilterSnapshots(plan.on, context);
778
+ const jobs = matcher?.filter_snapshot?.jobs as Record<string, {executions: {index: unknown}[]}>;
779
+ const snapshotRun = matcher?.filter_snapshot?.run as {number: unknown};
780
+
781
+ expect(typeof jobs.build?.executions[0]?.index).toBe('number');
782
+ expect(typeof snapshotRun.number).toBe('number');
783
+ expect(() => JSON.stringify(matcher?.filter_snapshot)).not.toThrow();
784
+ });
609
785
  });
610
786
 
611
787
  describe('assembleStepDispatchContext', () => {
@@ -642,7 +818,7 @@ describe('assembleStepDispatchContext', () => {
642
818
  site: 'step-dispatch',
643
819
  values: {
644
820
  execution: {
645
- index: 2,
821
+ index: 1n,
646
822
  name: 'Deploy',
647
823
  status: 'running',
648
824
  failed: false,
@@ -653,7 +829,7 @@ describe('assembleStepDispatchContext', () => {
653
829
  source: 'github',
654
830
  event: 'push',
655
831
  delivery_id: 'delivery-1',
656
- received_at: '2026-06-30T12:00:00.000Z',
832
+ received_at: date,
657
833
  project: null,
658
834
  repository: null,
659
835
  ref: null,
@@ -980,7 +1156,7 @@ describe('assembleJobResolutionContext', () => {
980
1156
  vars: {},
981
1157
  executions: [
982
1158
  {
983
- index: 0,
1159
+ index: 0n,
984
1160
  name: 'First',
985
1161
  status: 'failed',
986
1162
  started_at: date,
@@ -990,7 +1166,7 @@ describe('assembleJobResolutionContext', () => {
990
1166
  source: 'github',
991
1167
  event: 'push',
992
1168
  delivery_id: 'delivery-1',
993
- received_at: '2026-06-30T12:00:00.000Z',
1169
+ received_at: date,
994
1170
  project: null,
995
1171
  repository: null,
996
1172
  ref: null,
@@ -1001,7 +1177,7 @@ describe('assembleJobResolutionContext', () => {
1001
1177
  outputs: {},
1002
1178
  },
1003
1179
  {
1004
- index: 1,
1180
+ index: 1n,
1005
1181
  name: 'Second',
1006
1182
  status: 'succeeded',
1007
1183
  started_at: date,
@@ -1011,7 +1187,7 @@ describe('assembleJobResolutionContext', () => {
1011
1187
  source: 'github',
1012
1188
  event: 'push',
1013
1189
  delivery_id: 'delivery-1',
1014
- received_at: '2026-06-30T12:00:00.000Z',
1190
+ received_at: date,
1015
1191
  project: null,
1016
1192
  repository: null,
1017
1193
  ref: null,
@@ -1069,15 +1245,51 @@ describe('assembleExecutionResolutionContext', () => {
1069
1245
  });
1070
1246
 
1071
1247
  expect(context.values.execution).toEqual({
1072
- index: 0,
1248
+ index: 0n,
1073
1249
  name: 'Build #2',
1074
1250
  status: 'running',
1075
1251
  started_at: date,
1076
1252
  finished_at: null,
1077
- events: targetExecution.triggerEvents,
1253
+ events: targetExecution.triggerEvents.map((event) => ({...event, received_at: date})),
1078
1254
  outputs: {sha: 'target'},
1079
1255
  });
1080
1256
  });
1257
+
1258
+ it('exposes execution indices and event timestamps as CEL-native values', () => {
1259
+ const targetExecution = jobExecution({
1260
+ id: 'exec-2',
1261
+ sequence: 2,
1262
+ outputs: {sha: 'target'},
1263
+ });
1264
+
1265
+ const context = assembleExecutionResolutionContext({
1266
+ run,
1267
+ triggerPayload: {
1268
+ source: 'manual',
1269
+ event: 'fire',
1270
+ subscriptionId: 'sub-1',
1271
+ userId: 'user-1',
1272
+ },
1273
+ job: {key: 'build'},
1274
+ jobExecution: targetExecution,
1275
+ executions: [targetExecution],
1276
+ steps: [],
1277
+ attempts: [],
1278
+ });
1279
+
1280
+ const expressions = [
1281
+ 'executions[0].index + 1 == 1',
1282
+ 'execution.index + 1 == 1',
1283
+ 'run.number + 1 == 2',
1284
+ 'executions[0].events[0].received_at < timestamp("2026-07-01T00:00:00Z")',
1285
+ ];
1286
+
1287
+ for (const source of expressions) {
1288
+ const expression = createWorkflowExpression({source, check: {mode: 'syntax'}});
1289
+
1290
+ expect(evaluateWorkflowExpression(expression, context.values)).toBe(true);
1291
+ }
1292
+ });
1081
1293
  });
1082
1294
 
1083
1295
  function step(overrides: Partial<Step> = {}): Step {