@shipfox/api-definitions 10.2.0 → 12.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.
- package/.turbo/turbo-build.log +13 -12
- package/CHANGELOG.md +91 -0
- package/dist/core/entities/index.d.ts +1 -0
- package/dist/core/entities/index.d.ts.map +1 -1
- package/dist/core/entities/index.js +1 -0
- package/dist/core/entities/index.js.map +1 -1
- package/dist/core/entities/sync-state.d.ts +4 -0
- package/dist/core/entities/sync-state.d.ts.map +1 -1
- package/dist/core/entities/sync-state.js +10 -0
- package/dist/core/entities/sync-state.js.map +1 -1
- package/dist/core/entities/validation-warning.d.ts +6 -0
- package/dist/core/entities/validation-warning.d.ts.map +1 -0
- package/dist/core/entities/validation-warning.js +3 -0
- package/dist/core/entities/validation-warning.js.map +1 -0
- package/dist/core/parse-definition.d.ts +9 -7
- package/dist/core/parse-definition.d.ts.map +1 -1
- package/dist/core/parse-definition.js +14 -2
- package/dist/core/parse-definition.js.map +1 -1
- package/dist/core/sync-definitions.d.ts +2 -0
- package/dist/core/sync-definitions.d.ts.map +1 -1
- package/dist/core/sync-definitions.js +6 -5
- package/dist/core/sync-definitions.js.map +1 -1
- package/dist/core/validate-definition.d.ts +9 -5
- package/dist/core/validate-definition.d.ts.map +1 -1
- package/dist/core/validate-definition.js +23 -2
- package/dist/core/validate-definition.js.map +1 -1
- package/dist/core/workflow-model/index.d.ts +1 -1
- package/dist/core/workflow-model/index.d.ts.map +1 -1
- package/dist/core/workflow-model/index.js.map +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +3 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
- package/dist/core/workflow-model/normalize-if-condition.d.ts +0 -1
- package/dist/core/workflow-model/normalize-if-condition.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-if-condition.js +0 -1
- package/dist/core/workflow-model/normalize-if-condition.js.map +1 -1
- package/dist/core/workflow-model/normalize-job-checkout.d.ts +15 -3
- package/dist/core/workflow-model/normalize-job-checkout.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-job-checkout.js +83 -4
- package/dist/core/workflow-model/normalize-job-checkout.js.map +1 -1
- package/dist/core/workflow-model/normalize-job-listening.js +0 -1
- package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
- package/dist/core/workflow-model/normalize-job-success.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-job-success.js +0 -1
- package/dist/core/workflow-model/normalize-job-success.js.map +1 -1
- package/dist/core/workflow-model/normalize-jobs.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-jobs.js +310 -45
- package/dist/core/workflow-model/normalize-jobs.js.map +1 -1
- package/dist/core/workflow-model/normalize-step-gate.js +0 -1
- package/dist/core/workflow-model/normalize-step-gate.js.map +1 -1
- package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-triggers.js +0 -1
- package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
- package/dist/core/workflow-model/normalize-workflow-document.d.ts +3 -0
- package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-workflow-document.js +34 -4
- package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.d.ts +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.js +27 -57
- package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
- package/dist/core/workflow-model/validate-literal-name.d.ts +12 -0
- package/dist/core/workflow-model/validate-literal-name.d.ts.map +1 -0
- package/dist/core/workflow-model/validate-literal-name.js +36 -0
- package/dist/core/workflow-model/validate-literal-name.js.map +1 -0
- package/dist/core/workflow-model/validate-predicate-expression.d.ts +1 -2
- package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
- package/dist/core/workflow-model/validate-predicate-expression.js +31 -90
- package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
- package/dist/core/workflow-model/validation-issue.d.ts +2 -1
- package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
- package/dist/core/workflow-model/validation-issue.js +8 -2
- package/dist/core/workflow-model/validation-issue.js.map +1 -1
- package/dist/core/workflow-model/workflow-field-label.d.ts.map +1 -1
- package/dist/core/workflow-model/workflow-field-label.js +16 -2
- package/dist/core/workflow-model/workflow-field-label.js.map +1 -1
- package/dist/db/db.d.ts +38 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/sync-states.d.ts +19 -0
- package/dist/db/schema/sync-states.d.ts.map +1 -1
- package/dist/db/schema/sync-states.js +3 -1
- package/dist/db/schema/sync-states.js.map +1 -1
- package/dist/db/sync-states.d.ts +2 -1
- package/dist/db/sync-states.d.ts.map +1 -1
- package/dist/db/sync-states.js +4 -0
- package/dist/db/sync-states.js.map +1 -1
- package/dist/presentation/dto/definition.d.ts.map +1 -1
- package/dist/presentation/dto/definition.js +2 -1
- package/dist/presentation/dto/definition.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -0
- package/dist/presentation/routes/project-access.d.ts.map +1 -1
- package/dist/presentation/routes/validate-definition.d.ts.map +1 -1
- package/dist/presentation/routes/validate-definition.js +5 -3
- package/dist/presentation/routes/validate-definition.js.map +1 -1
- package/dist/temporal/activities/sync-activities.d.ts +6 -2
- package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
- package/dist/temporal/activities/sync-activities.js +8 -1
- package/dist/temporal/activities/sync-activities.js.map +1 -1
- package/dist/temporal/workflows/definition-sync.js +4 -1
- package/dist/temporal/workflows/definition-sync.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +9029 -3
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_lumpy_ben_parker.sql +1 -0
- package/drizzle/meta/0002_snapshot.json +534 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +14 -14
- package/src/core/entities/index.ts +1 -0
- package/src/core/entities/sync-state.ts +23 -0
- package/src/core/entities/validation-warning.ts +5 -0
- package/src/core/parse-definition-warnings.test.ts +33 -0
- package/src/core/parse-definition.test.ts +7 -1
- package/src/core/parse-definition.ts +28 -10
- package/src/core/sync-definitions.test.ts +39 -0
- package/src/core/sync-definitions.ts +14 -4
- package/src/core/validate-definition.test.ts +128 -0
- package/src/core/validate-definition.ts +39 -8
- package/src/core/workflow-model/README.md +2 -2
- package/src/core/workflow-model/index.ts +1 -0
- package/src/core/workflow-model/invalid-workflow-model-error.ts +8 -2
- package/src/core/workflow-model/normalize-if-condition.ts +0 -2
- package/src/core/workflow-model/normalize-job-checkout.ts +105 -8
- package/src/core/workflow-model/normalize-job-listening.ts +0 -1
- package/src/core/workflow-model/normalize-job-success.ts +0 -1
- package/src/core/workflow-model/normalize-jobs.ts +330 -49
- package/src/core/workflow-model/normalize-step-gate.ts +0 -1
- package/src/core/workflow-model/normalize-triggers.ts +0 -1
- package/src/core/workflow-model/normalize-workflow-document.test.ts +759 -185
- package/src/core/workflow-model/normalize-workflow-document.ts +29 -4
- package/src/core/workflow-model/parse-interpolation-field.ts +50 -79
- package/src/core/workflow-model/validate-literal-name.ts +64 -0
- package/src/core/workflow-model/validate-predicate-expression.test.ts +153 -15
- package/src/core/workflow-model/validate-predicate-expression.ts +52 -120
- package/src/core/workflow-model/validation-issue.ts +4 -0
- package/src/core/workflow-model/workflow-field-label.ts +16 -2
- package/src/core/workflow-yaml/README.md +2 -2
- package/src/db/schema/sync-states.ts +4 -1
- package/src/db/sync-states.test.ts +57 -0
- package/src/db/sync-states.ts +6 -0
- package/src/presentation/dto/definition.ts +1 -0
- package/src/presentation/routes/create-definition.test.ts +3 -3
- package/src/presentation/routes/get-definition.test.ts +1 -1
- package/src/presentation/routes/list-definitions.test.ts +2 -1
- package/src/presentation/routes/validate-definition.test.ts +1 -0
- package/src/presentation/routes/validate-definition.ts +7 -1
- package/src/temporal/activities/sync-activities.test.ts +16 -0
- package/src/temporal/activities/sync-activities.ts +18 -3
- package/src/temporal/workflows/definition-sync.ts +1 -1
- package/test/agent-validation-catalog.ts +17 -0
- package/test/fixtures/valid-listening-job.yml +2 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -79,6 +79,199 @@ const integrationValidationContext = {
|
|
|
79
79
|
} satisfies IntegrationValidationContext;
|
|
80
80
|
|
|
81
81
|
describe('normalizeWorkflowDocument', () => {
|
|
82
|
+
it('rejects interpolated workflow names in favor of run_name', () => {
|
|
83
|
+
const error = expectInvalid({
|
|
84
|
+
name: interpolation('inputs.environment'),
|
|
85
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(error.issues).toEqual([
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
code: 'invalid-interpolation-template',
|
|
91
|
+
message: 'Workflow name must be literal. Move runtime interpolation to run_name.',
|
|
92
|
+
path: ['name'],
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects unterminated workflow-name templates with the migration guidance', () => {
|
|
98
|
+
const error = expectInvalid({
|
|
99
|
+
name: '$' + '{{ unterminated',
|
|
100
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(error.issues).toEqual([
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
code: 'invalid-interpolation-template',
|
|
106
|
+
message: 'Workflow name must be literal. Move runtime interpolation to run_name.',
|
|
107
|
+
path: ['name'],
|
|
108
|
+
details: expect.objectContaining({reason: 'unterminated opener'}),
|
|
109
|
+
}),
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('rejects unterminated job-name templates with the migration guidance', () => {
|
|
114
|
+
const error = expectInvalid({
|
|
115
|
+
name: 'Deploy',
|
|
116
|
+
jobs: {
|
|
117
|
+
build: {name: '$' + '{{ unterminated', steps: [{run: 'build'}]},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(error.issues).toEqual([
|
|
122
|
+
expect.objectContaining({
|
|
123
|
+
code: 'invalid-interpolation-template',
|
|
124
|
+
message: 'Job name must be literal. Move runtime interpolation to execution_name.',
|
|
125
|
+
path: ['jobs', 'build', 'name'],
|
|
126
|
+
details: expect.objectContaining({reason: 'unterminated opener'}),
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('normalizes workflow run and job execution name templates', () => {
|
|
132
|
+
const model = normalizeWorkflowDocument({
|
|
133
|
+
name: 'Deploy application',
|
|
134
|
+
run_name: `Deploy ${interpolation('inputs.environment')}`,
|
|
135
|
+
jobs: {
|
|
136
|
+
deploy: {
|
|
137
|
+
name: 'Deploy',
|
|
138
|
+
execution_name: `Deploy ${interpolation('event.environment')}`,
|
|
139
|
+
steps: [{run: 'deploy'}],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(model.runName).toMatchObject([
|
|
145
|
+
{kind: 'literal', value: 'Deploy '},
|
|
146
|
+
{kind: 'deferred', roots: ['inputs'], fillTarget: 'run-creation'},
|
|
147
|
+
]);
|
|
148
|
+
expect(model.jobs[0]?.executionName).toMatchObject([
|
|
149
|
+
{kind: 'literal', value: 'Deploy '},
|
|
150
|
+
{kind: 'deferred', roots: ['event'], fillTarget: 'execution-creation'},
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('preserves literal workflow run and job execution names', () => {
|
|
155
|
+
const model = normalizeWorkflowDocument({
|
|
156
|
+
name: 'Workflow',
|
|
157
|
+
run_name: 'Static run name',
|
|
158
|
+
jobs: {
|
|
159
|
+
build: {
|
|
160
|
+
execution_name: 'Static execution name',
|
|
161
|
+
steps: [{run: 'build'}],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(model.runName).toEqual([{kind: 'literal', value: 'Static run name'}]);
|
|
167
|
+
expect(model.jobs[0]?.executionName).toEqual([
|
|
168
|
+
{kind: 'literal', value: 'Static execution name'},
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('unescapes literal workflow and job names', () => {
|
|
173
|
+
const escapedName = `$${interpolation('inputs.environment')}`;
|
|
174
|
+
const model = normalizeWorkflowDocument({
|
|
175
|
+
name: escapedName,
|
|
176
|
+
jobs: {
|
|
177
|
+
build: {name: escapedName, steps: [{run: 'build'}]},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(model.name).toBe(interpolation('inputs.environment'));
|
|
182
|
+
expect(model.jobs[0]?.name).toBe(interpolation('inputs.environment'));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it.each([
|
|
186
|
+
[
|
|
187
|
+
'run_name',
|
|
188
|
+
{
|
|
189
|
+
name: 'Workflow',
|
|
190
|
+
run_name: interpolation('steps.build.outputs.name'),
|
|
191
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
'execution_name',
|
|
196
|
+
{
|
|
197
|
+
name: 'Workflow',
|
|
198
|
+
jobs: {
|
|
199
|
+
build: {
|
|
200
|
+
execution_name: interpolation('steps.build.outputs.name'),
|
|
201
|
+
steps: [{run: 'build'}],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
] as const)('rejects unavailable context in %s', (_field, document) => {
|
|
207
|
+
const error = expectInvalid(document as unknown as WorkflowDocument);
|
|
208
|
+
expect(error.issues).toEqual([
|
|
209
|
+
expect.objectContaining({code: 'context-unavailable-at-fill-site'}),
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('allows computed access to unrelated dynamic-name context fields', () => {
|
|
214
|
+
const model = normalizeWorkflowDocument({
|
|
215
|
+
name: 'Workflow',
|
|
216
|
+
run_name: interpolation('run["id"]'),
|
|
217
|
+
jobs: {
|
|
218
|
+
build: {
|
|
219
|
+
execution_name: interpolation('execution["status"]'),
|
|
220
|
+
steps: [{run: 'build'}],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(model.runName).toMatchObject([{kind: 'deferred', roots: ['run']}]);
|
|
226
|
+
expect(model.jobs[0]?.executionName).toMatchObject([{kind: 'deferred', roots: ['execution']}]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it.each([
|
|
230
|
+
[
|
|
231
|
+
'run_name',
|
|
232
|
+
{
|
|
233
|
+
name: 'Workflow',
|
|
234
|
+
run_name: interpolation('run.name'),
|
|
235
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
[
|
|
239
|
+
'run_name bracket access',
|
|
240
|
+
{
|
|
241
|
+
name: 'Workflow',
|
|
242
|
+
run_name: interpolation('run["name"]'),
|
|
243
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
'execution_name',
|
|
248
|
+
{
|
|
249
|
+
name: 'Workflow',
|
|
250
|
+
jobs: {
|
|
251
|
+
build: {
|
|
252
|
+
execution_name: interpolation('execution.name'),
|
|
253
|
+
steps: [{run: 'build'}],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
'execution_name bracket access',
|
|
260
|
+
{
|
|
261
|
+
name: 'Workflow',
|
|
262
|
+
jobs: {
|
|
263
|
+
build: {
|
|
264
|
+
execution_name: interpolation('execution["name"]'),
|
|
265
|
+
steps: [{run: 'build'}],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
] as const)('rejects dynamic-name self-reference in %s', (_field, document) => {
|
|
271
|
+
const error = expectInvalid(document as unknown as WorkflowDocument);
|
|
272
|
+
expect(error.issues).toEqual([expect.objectContaining({code: 'dynamic-name-self-reference'})]);
|
|
273
|
+
});
|
|
274
|
+
|
|
82
275
|
it('normalizes a workflow document into a WorkflowModel', () => {
|
|
83
276
|
const document: WorkflowDocument = {
|
|
84
277
|
name: 'simple build',
|
|
@@ -154,7 +347,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
154
347
|
{
|
|
155
348
|
key: 'review',
|
|
156
349
|
harness: 'claude',
|
|
157
|
-
model: '
|
|
350
|
+
model: 'claude-opus-4-8',
|
|
158
351
|
provider: 'anthropic',
|
|
159
352
|
prompt: 'Review the fix.',
|
|
160
353
|
thinking: 'low',
|
|
@@ -538,6 +731,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
538
731
|
{
|
|
539
732
|
harness: 'pi',
|
|
540
733
|
provider: 'workspace-openai-compatible',
|
|
734
|
+
model: 'workspace-model',
|
|
541
735
|
prompt: 'Fix it.',
|
|
542
736
|
},
|
|
543
737
|
],
|
|
@@ -551,6 +745,80 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
551
745
|
kind: 'agent',
|
|
552
746
|
harness: 'pi',
|
|
553
747
|
provider: 'workspace-openai-compatible',
|
|
748
|
+
model: 'workspace-model',
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('reports unsupported literal models', () => {
|
|
753
|
+
const document: WorkflowDocument = {
|
|
754
|
+
name: 'agent build',
|
|
755
|
+
jobs: {
|
|
756
|
+
fix: {
|
|
757
|
+
steps: [{harness: 'pi', provider: 'anthropic', model: 'not-a-model', prompt: 'Fix it.'}],
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const error = expectInvalid(document);
|
|
763
|
+
|
|
764
|
+
expect(error.issues).toEqual([
|
|
765
|
+
{
|
|
766
|
+
code: 'invalid-model',
|
|
767
|
+
message:
|
|
768
|
+
'Agent model "not-a-model" is not available for harness "pi" and provider "anthropic".',
|
|
769
|
+
path: ['jobs', 'fix', 'steps', 0, 'model'],
|
|
770
|
+
details: {harness: 'pi', provider: 'anthropic', model: 'not-a-model'},
|
|
771
|
+
},
|
|
772
|
+
]);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('allows custom providers without an explicit harness', () => {
|
|
776
|
+
const document: WorkflowDocument = {
|
|
777
|
+
name: 'agent build',
|
|
778
|
+
jobs: {
|
|
779
|
+
fix: {
|
|
780
|
+
steps: [
|
|
781
|
+
{
|
|
782
|
+
provider: 'workspace-openai-compatible',
|
|
783
|
+
model: 'workspace-model',
|
|
784
|
+
prompt: 'Fix it.',
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const model = normalizeWorkflowDocument(document);
|
|
792
|
+
|
|
793
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
794
|
+
kind: 'agent',
|
|
795
|
+
provider: 'workspace-openai-compatible',
|
|
796
|
+
model: 'workspace-model',
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('defers harness-specific validation when harness is omitted', () => {
|
|
801
|
+
const document: WorkflowDocument = {
|
|
802
|
+
name: 'workspace-default harness',
|
|
803
|
+
jobs: {
|
|
804
|
+
fix: {
|
|
805
|
+
steps: [
|
|
806
|
+
{
|
|
807
|
+
provider: 'anthropic',
|
|
808
|
+
model: 'claude-sonnet-4-6',
|
|
809
|
+
prompt: 'Fix it.',
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const model = normalizeWorkflowDocument(document);
|
|
817
|
+
|
|
818
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
819
|
+
kind: 'agent',
|
|
820
|
+
provider: 'anthropic',
|
|
821
|
+
model: 'claude-sonnet-4-6',
|
|
554
822
|
});
|
|
555
823
|
});
|
|
556
824
|
|
|
@@ -743,6 +1011,29 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
743
1011
|
});
|
|
744
1012
|
});
|
|
745
1013
|
|
|
1014
|
+
it('defers an interpolated thinking level to dispatch', () => {
|
|
1015
|
+
const document: WorkflowDocument = {
|
|
1016
|
+
name: 'agent build',
|
|
1017
|
+
jobs: {
|
|
1018
|
+
fix: {
|
|
1019
|
+
steps: [
|
|
1020
|
+
{
|
|
1021
|
+
harness: 'claude',
|
|
1022
|
+
prompt: 'Fix it.',
|
|
1023
|
+
thinking: workflowInterpolation('vars.THINKING'),
|
|
1024
|
+
},
|
|
1025
|
+
],
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const model = normalizeWorkflowDocument(document);
|
|
1031
|
+
const step = model.jobs[0]?.steps[0];
|
|
1032
|
+
|
|
1033
|
+
if (step?.kind !== 'agent') throw new Error('Expected an agent step');
|
|
1034
|
+
expect(step.templates?.thinking).toMatchObject([{kind: 'deferred', roots: ['vars']}]);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
746
1037
|
it('normalizes job success expressions and execution timeouts', () => {
|
|
747
1038
|
const document: WorkflowDocument = {
|
|
748
1039
|
name: 'job controls',
|
|
@@ -831,6 +1122,144 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
831
1122
|
expect(model.jobs[0]?.checkout).toEqual(DEFAULT_JOB_CHECKOUT);
|
|
832
1123
|
});
|
|
833
1124
|
|
|
1125
|
+
it('preserves the job checkout opt-out in the model', () => {
|
|
1126
|
+
const model = normalizeWorkflowDocument({
|
|
1127
|
+
name: 'checkout disabled',
|
|
1128
|
+
jobs: {
|
|
1129
|
+
build: {
|
|
1130
|
+
checkout: false,
|
|
1131
|
+
steps: [{run: 'npm test'}],
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
expect(model.jobs[0]?.checkout).toBe(false);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('normalizes checkout-step targets from earlier step outputs', () => {
|
|
1140
|
+
const model = normalizeWorkflowDocument({
|
|
1141
|
+
name: 'templated checkout step',
|
|
1142
|
+
jobs: {
|
|
1143
|
+
build: {
|
|
1144
|
+
steps: [
|
|
1145
|
+
{
|
|
1146
|
+
key: 'route',
|
|
1147
|
+
run: 'echo route',
|
|
1148
|
+
outputs: {
|
|
1149
|
+
connection: {type: 'string'},
|
|
1150
|
+
repository: {type: 'string'},
|
|
1151
|
+
ref: {type: 'string'},
|
|
1152
|
+
path: {type: 'string'},
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
key: 'checkout',
|
|
1157
|
+
checkout: {
|
|
1158
|
+
connection: interpolation('inputs.connection'),
|
|
1159
|
+
repository: interpolation('steps.route.outputs.repository'),
|
|
1160
|
+
ref: interpolation('steps.route.outputs.ref'),
|
|
1161
|
+
path: interpolation('steps.route.outputs.path'),
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
],
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
1170
|
+
kind: 'checkout',
|
|
1171
|
+
checkout: {
|
|
1172
|
+
connection: interpolation('inputs.connection'),
|
|
1173
|
+
repository: interpolation('steps.route.outputs.repository'),
|
|
1174
|
+
ref: interpolation('steps.route.outputs.ref'),
|
|
1175
|
+
path: interpolation('steps.route.outputs.path'),
|
|
1176
|
+
fetchDepth: 1,
|
|
1177
|
+
permissions: {contents: 'read'},
|
|
1178
|
+
persistCredentials: true,
|
|
1179
|
+
templates: {
|
|
1180
|
+
connection: [{kind: 'deferred', roots: ['inputs'], fillTarget: 'run-creation'}],
|
|
1181
|
+
repository: [{kind: 'deferred', roots: ['steps'], fillTarget: 'step-dispatch'}],
|
|
1182
|
+
ref: [{kind: 'deferred', roots: ['steps'], fillTarget: 'step-dispatch'}],
|
|
1183
|
+
path: [{kind: 'deferred', roots: ['steps'], fillTarget: 'step-dispatch'}],
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it('reports invalid checkout target combinations from model normalization', () => {
|
|
1190
|
+
const error = expectInvalid({
|
|
1191
|
+
name: 'invalid checkout target',
|
|
1192
|
+
jobs: {
|
|
1193
|
+
build: {
|
|
1194
|
+
steps: [{checkout: {project: 'project-id', repository: 'acme/api'}}],
|
|
1195
|
+
},
|
|
1196
|
+
},
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
expect(error.issues).toContainEqual(
|
|
1200
|
+
expect.objectContaining({
|
|
1201
|
+
code: 'checkout-target-invalid',
|
|
1202
|
+
path: ['jobs', 'build', 'steps', 0, 'checkout', 'repository'],
|
|
1203
|
+
}),
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
const missingRepository = expectInvalid({
|
|
1207
|
+
name: 'missing checkout repository',
|
|
1208
|
+
jobs: {
|
|
1209
|
+
build: {
|
|
1210
|
+
steps: [{checkout: {connection: 'github'}}],
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
expect(missingRepository.issues).toContainEqual(
|
|
1216
|
+
expect.objectContaining({
|
|
1217
|
+
code: 'checkout-target-invalid',
|
|
1218
|
+
path: ['jobs', 'build', 'steps', 0, 'checkout', 'repository'],
|
|
1219
|
+
details: {fields: ['connection', 'repository']},
|
|
1220
|
+
}),
|
|
1221
|
+
);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('normalizes checkout steps and their static defaults', () => {
|
|
1225
|
+
const model = normalizeWorkflowDocument({
|
|
1226
|
+
name: 'checkout step',
|
|
1227
|
+
jobs: {
|
|
1228
|
+
build: {
|
|
1229
|
+
steps: [
|
|
1230
|
+
{
|
|
1231
|
+
key: 'fetch-target',
|
|
1232
|
+
checkout: {
|
|
1233
|
+
repository: 'acme/api',
|
|
1234
|
+
ref: 'refs/heads/main',
|
|
1235
|
+
'fetch-depth': 0,
|
|
1236
|
+
path: 'target',
|
|
1237
|
+
permissions: {contents: 'write'},
|
|
1238
|
+
'persist-credentials': false,
|
|
1239
|
+
force: true,
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
],
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
1248
|
+
id: expect.stringContaining('fetch-target'),
|
|
1249
|
+
key: 'fetch-target',
|
|
1250
|
+
kind: 'checkout',
|
|
1251
|
+
checkout: {
|
|
1252
|
+
repository: 'acme/api',
|
|
1253
|
+
ref: 'refs/heads/main',
|
|
1254
|
+
fetchDepth: 0,
|
|
1255
|
+
path: 'target',
|
|
1256
|
+
permissions: {contents: 'write'},
|
|
1257
|
+
persistCredentials: false,
|
|
1258
|
+
force: true,
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
|
|
834
1263
|
it('normalizes checkout contents write and defaults persisted credentials', () => {
|
|
835
1264
|
const document: WorkflowDocument = {
|
|
836
1265
|
name: 'write checkout',
|
|
@@ -880,7 +1309,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
880
1309
|
name: 'listen for reviews',
|
|
881
1310
|
jobs: {
|
|
882
1311
|
review: {
|
|
883
|
-
|
|
1312
|
+
execution_name: displayName,
|
|
884
1313
|
listening: {
|
|
885
1314
|
on: [
|
|
886
1315
|
{
|
|
@@ -915,7 +1344,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
915
1344
|
onResolve: 'cancel',
|
|
916
1345
|
},
|
|
917
1346
|
});
|
|
918
|
-
expect(model.jobs[0]?.
|
|
1347
|
+
expect(model.jobs[0]?.executionName?.[1]).toMatchObject({
|
|
919
1348
|
kind: 'deferred',
|
|
920
1349
|
expression: {source: 'execution.index', check: 'typed'},
|
|
921
1350
|
});
|
|
@@ -968,7 +1397,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
968
1397
|
]);
|
|
969
1398
|
});
|
|
970
1399
|
|
|
971
|
-
it('validates listener filters against
|
|
1400
|
+
it('validates listener filters against their persisted snapshot roots', () => {
|
|
972
1401
|
const document: WorkflowDocument = {
|
|
973
1402
|
name: 'listener filter roots',
|
|
974
1403
|
jobs: {
|
|
@@ -983,7 +1412,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
983
1412
|
source: 'github',
|
|
984
1413
|
event: 'pull_request_review',
|
|
985
1414
|
filter:
|
|
986
|
-
'run.id != "" && inputs.target == event.issue.number &&
|
|
1415
|
+
'run.id != "" && trigger.event == "pull_request_review" && inputs.target == event.issue.number && vars.ENABLED == "true" && job.key == "review" && jobs.build.outputs.pr_number == event.issue.number',
|
|
987
1416
|
},
|
|
988
1417
|
],
|
|
989
1418
|
until: [
|
|
@@ -1002,7 +1431,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1002
1431
|
const model = normalizeWorkflowDocument(document);
|
|
1003
1432
|
|
|
1004
1433
|
expect(model.jobs[1]?.listening?.on[0]?.filter).toBe(
|
|
1005
|
-
'run.id != "" && inputs.target == event.issue.number &&
|
|
1434
|
+
'run.id != "" && trigger.event == "pull_request_review" && inputs.target == event.issue.number && vars.ENABLED == "true" && job.key == "review" && jobs.build.outputs.pr_number == event.issue.number',
|
|
1006
1435
|
);
|
|
1007
1436
|
expect(model.jobs[1]?.listening?.until?.[0]?.filter).toBe('event.action == "closed"');
|
|
1008
1437
|
});
|
|
@@ -1010,6 +1439,8 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1010
1439
|
it.each([
|
|
1011
1440
|
['step root', 'step.status == "succeeded"', 'context-unavailable-at-predicate-site'],
|
|
1012
1441
|
['steps root', 'steps.build.outputs.sha == "abc"', 'context-unavailable-at-predicate-site'],
|
|
1442
|
+
['executions root', 'executions.size() > 0', 'context-unavailable-at-predicate-site'],
|
|
1443
|
+
['matrix root', 'matrix.os == "linux"', 'context-unavailable-at-predicate-site'],
|
|
1013
1444
|
['runner root', 'runner.os == "linux"', 'runner-context-in-server-predicate'],
|
|
1014
1445
|
['non-boolean source', 'event.action', 'invalid-listener-filter'],
|
|
1015
1446
|
] as const)('reports invalid listener on filters for %s', (_label, filter, code) => {
|
|
@@ -1114,12 +1545,12 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1114
1545
|
]);
|
|
1115
1546
|
});
|
|
1116
1547
|
|
|
1117
|
-
it('
|
|
1548
|
+
it('accepts explicit model ids present in the harness catalog', () => {
|
|
1118
1549
|
const document: WorkflowDocument = {
|
|
1119
1550
|
name: 'agent build',
|
|
1120
1551
|
jobs: {
|
|
1121
1552
|
fix: {
|
|
1122
|
-
steps: [{provider: 'openai', model: 'gpt-4.1', prompt: 'Fix it.'}],
|
|
1553
|
+
steps: [{harness: 'pi', provider: 'openai', model: 'gpt-4.1', prompt: 'Fix it.'}],
|
|
1123
1554
|
},
|
|
1124
1555
|
},
|
|
1125
1556
|
};
|
|
@@ -1602,7 +2033,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1602
2033
|
});
|
|
1603
2034
|
});
|
|
1604
2035
|
|
|
1605
|
-
it('
|
|
2036
|
+
it('rejects server roots omitted from the gate success context', () => {
|
|
1606
2037
|
const document: WorkflowDocument = {
|
|
1607
2038
|
name: 'server-context gate',
|
|
1608
2039
|
jobs: {
|
|
@@ -1610,14 +2041,21 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1610
2041
|
},
|
|
1611
2042
|
};
|
|
1612
2043
|
|
|
1613
|
-
const
|
|
2044
|
+
const error = expectInvalid(document);
|
|
1614
2045
|
|
|
1615
|
-
expect(
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2046
|
+
expect(error.issues).toEqual([
|
|
2047
|
+
expect.objectContaining({
|
|
2048
|
+
code: 'context-unavailable-at-predicate-site',
|
|
2049
|
+
message: expect.stringContaining('Step gate success references context "run"'),
|
|
2050
|
+
path: ['jobs', 'build', 'steps', 0, 'gate', 'success'],
|
|
2051
|
+
details: expect.objectContaining({
|
|
2052
|
+
field: 'step.success',
|
|
2053
|
+
source: 'run.id != ""',
|
|
2054
|
+
unavailableRoots: ['run'],
|
|
2055
|
+
site: 'step-report',
|
|
2056
|
+
}),
|
|
2057
|
+
}),
|
|
2058
|
+
]);
|
|
1621
2059
|
});
|
|
1622
2060
|
|
|
1623
2061
|
it('rejects runner-host roots in gate success with a server-predicate issue', () => {
|
|
@@ -1647,7 +2085,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1647
2085
|
]);
|
|
1648
2086
|
});
|
|
1649
2087
|
|
|
1650
|
-
it('
|
|
2088
|
+
it('accepts vars in gate success', () => {
|
|
1651
2089
|
const document: WorkflowDocument = {
|
|
1652
2090
|
name: 'vars-context gate',
|
|
1653
2091
|
jobs: {
|
|
@@ -1657,24 +2095,15 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1657
2095
|
},
|
|
1658
2096
|
};
|
|
1659
2097
|
|
|
1660
|
-
const
|
|
2098
|
+
const model = normalizeWorkflowDocument(document);
|
|
1661
2099
|
|
|
1662
|
-
expect(
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
path: ['jobs', 'build', 'steps', 0, 'gate', 'success'],
|
|
1667
|
-
details: expect.objectContaining({
|
|
1668
|
-
field: 'step.success',
|
|
1669
|
-
source: 'vars.REQUIRED == "true"',
|
|
1670
|
-
rejectedRoots: ['vars'],
|
|
1671
|
-
site: 'step-report',
|
|
1672
|
-
}),
|
|
1673
|
-
}),
|
|
1674
|
-
]);
|
|
2100
|
+
expect(model.jobs[0]?.steps[0]?.gate?.success).toMatchObject({
|
|
2101
|
+
source: 'vars.REQUIRED == "true"',
|
|
2102
|
+
check: 'syntax',
|
|
2103
|
+
});
|
|
1675
2104
|
});
|
|
1676
2105
|
|
|
1677
|
-
it('
|
|
2106
|
+
it('rejects jobs root references omitted from the gate success context', () => {
|
|
1678
2107
|
const document: WorkflowDocument = {
|
|
1679
2108
|
name: 'jobs-context gate',
|
|
1680
2109
|
jobs: {
|
|
@@ -1688,13 +2117,21 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1688
2117
|
},
|
|
1689
2118
|
};
|
|
1690
2119
|
|
|
1691
|
-
const
|
|
2120
|
+
const error = expectInvalid(document);
|
|
1692
2121
|
|
|
1693
|
-
expect(
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2122
|
+
expect(error.issues).toEqual([
|
|
2123
|
+
expect.objectContaining({
|
|
2124
|
+
code: 'context-unavailable-at-predicate-site',
|
|
2125
|
+
message: expect.stringContaining('Step gate success references context "jobs"'),
|
|
2126
|
+
path: ['jobs', 'deploy', 'steps', 0, 'gate', 'success'],
|
|
2127
|
+
details: expect.objectContaining({
|
|
2128
|
+
field: 'step.success',
|
|
2129
|
+
source: 'jobs.build.status == "succeeded"',
|
|
2130
|
+
unavailableRoots: ['jobs'],
|
|
2131
|
+
site: 'step-report',
|
|
2132
|
+
}),
|
|
2133
|
+
}),
|
|
2134
|
+
]);
|
|
1698
2135
|
});
|
|
1699
2136
|
|
|
1700
2137
|
it('parses job output mappings at execution resolution', () => {
|
|
@@ -1954,7 +2391,34 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1954
2391
|
});
|
|
1955
2392
|
});
|
|
1956
2393
|
|
|
1957
|
-
it('
|
|
2394
|
+
it('allows integer equality literals for number step outputs in gate success expressions', () => {
|
|
2395
|
+
const document: WorkflowDocument = {
|
|
2396
|
+
name: 'number output equality',
|
|
2397
|
+
jobs: {
|
|
2398
|
+
build: {
|
|
2399
|
+
steps: [
|
|
2400
|
+
{
|
|
2401
|
+
key: 'collect',
|
|
2402
|
+
run: 'npm run collect',
|
|
2403
|
+
outputs: {issue_number: {type: 'number'}},
|
|
2404
|
+
gate: {success: 'step.outputs.issue_number == 1'},
|
|
2405
|
+
},
|
|
2406
|
+
],
|
|
2407
|
+
},
|
|
2408
|
+
},
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
const model = normalizeWorkflowDocument(document);
|
|
2412
|
+
|
|
2413
|
+
expect(model.jobs[0]?.steps[0]?.gate?.success).toEqual({
|
|
2414
|
+
language: 'cel',
|
|
2415
|
+
source: 'step.outputs.issue_number == 1',
|
|
2416
|
+
check: 'typed',
|
|
2417
|
+
resultType: 'bool',
|
|
2418
|
+
});
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2421
|
+
it('allows step outputs in run fields', () => {
|
|
1958
2422
|
const document: WorkflowDocument = {
|
|
1959
2423
|
name: 'untrusted step output',
|
|
1960
2424
|
jobs: {
|
|
@@ -1967,15 +2431,17 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1967
2431
|
},
|
|
1968
2432
|
};
|
|
1969
2433
|
|
|
1970
|
-
const
|
|
2434
|
+
const model = normalizeWorkflowDocument(document);
|
|
1971
2435
|
|
|
1972
|
-
expect(
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2436
|
+
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
2437
|
+
kind: 'run',
|
|
2438
|
+
templates: {
|
|
2439
|
+
command: [
|
|
2440
|
+
{kind: 'literal', value: 'echo '},
|
|
2441
|
+
{kind: 'deferred', roots: ['steps']},
|
|
2442
|
+
],
|
|
2443
|
+
},
|
|
2444
|
+
});
|
|
1979
2445
|
});
|
|
1980
2446
|
|
|
1981
2447
|
it('infers typed job output scalars for downstream job overlays', () => {
|
|
@@ -2291,29 +2757,29 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2291
2757
|
);
|
|
2292
2758
|
});
|
|
2293
2759
|
|
|
2294
|
-
it('
|
|
2760
|
+
it('accepts vars in job and step if predicates', () => {
|
|
2295
2761
|
const document: WorkflowDocument = {
|
|
2296
2762
|
name: 'vars condition',
|
|
2297
2763
|
jobs: {
|
|
2298
2764
|
build: {
|
|
2299
2765
|
if: interpolation('vars.REQUIRED == "true"'),
|
|
2300
|
-
steps: [{run: 'npm run build'}],
|
|
2766
|
+
steps: [{if: interpolation('vars.REQUIRED == "true"'), run: 'npm run build'}],
|
|
2301
2767
|
},
|
|
2302
2768
|
},
|
|
2303
2769
|
};
|
|
2304
2770
|
|
|
2305
|
-
const
|
|
2771
|
+
const model = normalizeWorkflowDocument(document);
|
|
2306
2772
|
|
|
2307
|
-
expect(
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2773
|
+
expect(model.jobs[0]?.if).toMatchObject({
|
|
2774
|
+
source: 'vars.REQUIRED == "true"',
|
|
2775
|
+
check: 'typed',
|
|
2776
|
+
resultType: 'bool',
|
|
2777
|
+
});
|
|
2778
|
+
expect(model.jobs[0]?.steps[0]?.if).toMatchObject({
|
|
2779
|
+
source: 'vars.REQUIRED == "true"',
|
|
2780
|
+
check: 'typed',
|
|
2781
|
+
resultType: 'bool',
|
|
2782
|
+
});
|
|
2317
2783
|
});
|
|
2318
2784
|
|
|
2319
2785
|
it('rejects if job references without a direct needs edge', () => {
|
|
@@ -2415,7 +2881,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2415
2881
|
path: ['jobs', 'build', 'if'],
|
|
2416
2882
|
details: expect.objectContaining({
|
|
2417
2883
|
field: 'job.if',
|
|
2418
|
-
unavailableRoots: ['execution
|
|
2884
|
+
unavailableRoots: ['execution'],
|
|
2419
2885
|
}),
|
|
2420
2886
|
}),
|
|
2421
2887
|
]);
|
|
@@ -2546,7 +3012,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2546
3012
|
]);
|
|
2547
3013
|
});
|
|
2548
3014
|
|
|
2549
|
-
it('
|
|
3015
|
+
it('accepts vars in job success', () => {
|
|
2550
3016
|
const document: WorkflowDocument = {
|
|
2551
3017
|
name: 'vars-context job success',
|
|
2552
3018
|
jobs: {
|
|
@@ -2557,21 +3023,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2557
3023
|
},
|
|
2558
3024
|
};
|
|
2559
3025
|
|
|
2560
|
-
const
|
|
3026
|
+
const model = normalizeWorkflowDocument(document);
|
|
2561
3027
|
|
|
2562
|
-
expect(
|
|
2563
|
-
expect.objectContaining({
|
|
2564
|
-
code: 'vars-context-in-server-predicate',
|
|
2565
|
-
message: expect.stringContaining('cannot reference vars'),
|
|
2566
|
-
path: ['jobs', 'build', 'success'],
|
|
2567
|
-
details: expect.objectContaining({
|
|
2568
|
-
field: 'job.success',
|
|
2569
|
-
source: 'vars.ENVIRONMENT == "prod"',
|
|
2570
|
-
rejectedRoots: ['vars'],
|
|
2571
|
-
site: 'job-resolution',
|
|
2572
|
-
}),
|
|
2573
|
-
}),
|
|
2574
|
-
]);
|
|
3028
|
+
expect(model.jobs[0]?.success).toBe('vars.ENVIRONMENT == "prod"');
|
|
2575
3029
|
});
|
|
2576
3030
|
|
|
2577
3031
|
it('reports malformed job execution timeouts', () => {
|
|
@@ -3084,12 +3538,12 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3084
3538
|
[
|
|
3085
3539
|
'vars root',
|
|
3086
3540
|
'vars.environment == "prod"',
|
|
3087
|
-
'
|
|
3541
|
+
'context-unavailable-at-predicate-site',
|
|
3088
3542
|
{
|
|
3089
3543
|
field: 'trigger.filter',
|
|
3090
3544
|
source: 'vars.environment == "prod"',
|
|
3091
3545
|
contextRoots: ['vars'],
|
|
3092
|
-
|
|
3546
|
+
unavailableRoots: ['vars'],
|
|
3093
3547
|
site: 'ingest',
|
|
3094
3548
|
},
|
|
3095
3549
|
],
|
|
@@ -3524,11 +3978,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3524
3978
|
steps: [
|
|
3525
3979
|
{
|
|
3526
3980
|
name: `deploy ${interpolation('event.action')}`,
|
|
3981
|
+
working_directory: 'packages/api',
|
|
3527
3982
|
run: `deploy ${interpolation('run.id')}`,
|
|
3528
3983
|
env: {PR_TITLE: interpolation('event.pull_request.title'), DEBUG: false},
|
|
3529
3984
|
},
|
|
3530
3985
|
{
|
|
3531
3986
|
name: `review ${interpolation('inputs.topic')}`,
|
|
3987
|
+
working_directory: `packages/${interpolation('inputs.topic')}`,
|
|
3532
3988
|
provider: 'openai',
|
|
3533
3989
|
prompt: `Review ${interpolation('event.pull_request.title')}`,
|
|
3534
3990
|
},
|
|
@@ -3575,6 +4031,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3575
4031
|
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
3576
4032
|
kind: 'run',
|
|
3577
4033
|
command: {value: `deploy ${interpolation('run.id')}`},
|
|
4034
|
+
workingDirectory: 'packages/api',
|
|
3578
4035
|
templates: {
|
|
3579
4036
|
command: [
|
|
3580
4037
|
{kind: 'literal', value: 'deploy '},
|
|
@@ -3609,7 +4066,16 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3609
4066
|
});
|
|
3610
4067
|
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
3611
4068
|
kind: 'agent',
|
|
4069
|
+
workingDirectory: `packages/${interpolation('inputs.topic')}`,
|
|
3612
4070
|
templates: {
|
|
4071
|
+
workingDirectory: [
|
|
4072
|
+
{kind: 'literal', value: 'packages/'},
|
|
4073
|
+
{
|
|
4074
|
+
kind: 'deferred',
|
|
4075
|
+
expression: {source: 'inputs.topic'},
|
|
4076
|
+
roots: ['inputs'],
|
|
4077
|
+
},
|
|
4078
|
+
],
|
|
3613
4079
|
prompt: [
|
|
3614
4080
|
{kind: 'literal', value: 'Review '},
|
|
3615
4081
|
{
|
|
@@ -3651,7 +4117,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3651
4117
|
expect(model).not.toHaveProperty('templates');
|
|
3652
4118
|
expect(model.jobs[0]?.steps[0]).not.toHaveProperty('templates');
|
|
3653
4119
|
expect(model.env).toEqual({VALUE: '$${{ event.ref }}'});
|
|
3654
|
-
expect(model.jobs[0]?.name).
|
|
4120
|
+
expect(model.jobs[0]?.name).toBe('Build app');
|
|
3655
4121
|
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
3656
4122
|
kind: 'run',
|
|
3657
4123
|
command: {value: 'echo $${{ event.ref }}'},
|
|
@@ -3710,30 +4176,32 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3710
4176
|
|
|
3711
4177
|
it.each([
|
|
3712
4178
|
['event payload', 'event.pull_request.title', ['event']],
|
|
3713
|
-
['job
|
|
3714
|
-
] as const)('
|
|
4179
|
+
['job status', 'jobs.build.status', ['jobs']],
|
|
4180
|
+
] as const)('allows %s context in run commands', (_label, source, roots) => {
|
|
3715
4181
|
const document: WorkflowDocument = {
|
|
3716
4182
|
name: 'unsafe run',
|
|
3717
4183
|
jobs: {
|
|
3718
4184
|
build: {
|
|
4185
|
+
steps: [{run: 'echo build'}],
|
|
4186
|
+
},
|
|
4187
|
+
deploy: {
|
|
4188
|
+
needs: 'build',
|
|
3719
4189
|
steps: [{run: `echo ${interpolation(source)}`}],
|
|
3720
4190
|
},
|
|
3721
4191
|
},
|
|
3722
4192
|
};
|
|
3723
4193
|
|
|
3724
|
-
const
|
|
4194
|
+
const model = normalizeWorkflowDocument(document);
|
|
3725
4195
|
|
|
3726
|
-
expect(
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
}),
|
|
3736
|
-
]);
|
|
4196
|
+
expect(model.jobs[1]?.steps[0]).toMatchObject({
|
|
4197
|
+
kind: 'run',
|
|
4198
|
+
templates: {
|
|
4199
|
+
command: [
|
|
4200
|
+
{kind: 'literal', value: 'echo '},
|
|
4201
|
+
{kind: 'deferred', roots},
|
|
4202
|
+
],
|
|
4203
|
+
},
|
|
4204
|
+
});
|
|
3737
4205
|
});
|
|
3738
4206
|
|
|
3739
4207
|
it('rejects secrets in agent fields', () => {
|
|
@@ -3805,8 +4273,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3805
4273
|
it.each([
|
|
3806
4274
|
'execution.events[0].data.body',
|
|
3807
4275
|
'execution["events"][0].data.body',
|
|
3808
|
-
|
|
3809
|
-
])('rejects untrusted execution sub-paths in run commands: %s', (source) => {
|
|
4276
|
+
])('allows execution event sub-paths in run commands: %s', (source) => {
|
|
3810
4277
|
const document: WorkflowDocument = {
|
|
3811
4278
|
name: 'unsafe execution run',
|
|
3812
4279
|
jobs: {
|
|
@@ -3816,42 +4283,17 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3816
4283
|
},
|
|
3817
4284
|
};
|
|
3818
4285
|
|
|
3819
|
-
const
|
|
3820
|
-
|
|
3821
|
-
expect(error.issues).toEqual([
|
|
3822
|
-
expect.objectContaining({
|
|
3823
|
-
code: 'untrusted-context-in-field',
|
|
3824
|
-
path: ['jobs', 'build', 'steps', 0, 'run'],
|
|
3825
|
-
details: expect.objectContaining({
|
|
3826
|
-
field: 'run',
|
|
3827
|
-
rejectedRoots: ['execution'],
|
|
3828
|
-
}),
|
|
3829
|
-
}),
|
|
3830
|
-
]);
|
|
3831
|
-
});
|
|
4286
|
+
const model = normalizeWorkflowDocument(document);
|
|
3832
4287
|
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
4288
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4289
|
+
kind: 'run',
|
|
4290
|
+
templates: {
|
|
4291
|
+
command: [
|
|
4292
|
+
{kind: 'literal', value: 'echo '},
|
|
4293
|
+
{kind: 'deferred', roots: ['execution']},
|
|
4294
|
+
],
|
|
3840
4295
|
},
|
|
3841
|
-
};
|
|
3842
|
-
|
|
3843
|
-
const error = expectInvalid(document);
|
|
3844
|
-
|
|
3845
|
-
expect(error.issues).toEqual([
|
|
3846
|
-
expect.objectContaining({
|
|
3847
|
-
code: 'untrusted-context-in-field',
|
|
3848
|
-
path: ['jobs', 'build', 'steps', 0, 'run'],
|
|
3849
|
-
details: expect.objectContaining({
|
|
3850
|
-
field: 'run',
|
|
3851
|
-
rejectedRoots: ['executions'],
|
|
3852
|
-
}),
|
|
3853
|
-
}),
|
|
3854
|
-
]);
|
|
4296
|
+
});
|
|
3855
4297
|
});
|
|
3856
4298
|
|
|
3857
4299
|
it.each([
|
|
@@ -3967,7 +4409,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3967
4409
|
});
|
|
3968
4410
|
});
|
|
3969
4411
|
|
|
3970
|
-
it('
|
|
4412
|
+
it('rejects dynamic job names in favor of execution_name', () => {
|
|
3971
4413
|
const document: WorkflowDocument = {
|
|
3972
4414
|
name: 'job display context',
|
|
3973
4415
|
jobs: {
|
|
@@ -3983,16 +4425,19 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3983
4425
|
},
|
|
3984
4426
|
};
|
|
3985
4427
|
|
|
3986
|
-
const
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
4428
|
+
const error = expectInvalid(document);
|
|
4429
|
+
expect(error.issues).toEqual([
|
|
4430
|
+
expect.objectContaining({
|
|
4431
|
+
code: 'invalid-interpolation-template',
|
|
4432
|
+
message: 'Job name must be literal. Move runtime interpolation to execution_name.',
|
|
4433
|
+
path: ['jobs', 'build', 'name'],
|
|
4434
|
+
}),
|
|
4435
|
+
expect.objectContaining({
|
|
4436
|
+
code: 'invalid-interpolation-template',
|
|
4437
|
+
message: 'Job name must be literal. Move runtime interpolation to execution_name.',
|
|
4438
|
+
path: ['jobs', 'review', 'name'],
|
|
4439
|
+
}),
|
|
4440
|
+
]);
|
|
3996
4441
|
});
|
|
3997
4442
|
|
|
3998
4443
|
it('does not apply availability checks to workflow-level or job-level env', () => {
|
|
@@ -4122,7 +4567,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4122
4567
|
});
|
|
4123
4568
|
});
|
|
4124
4569
|
|
|
4125
|
-
it('
|
|
4570
|
+
it('rejects execution events in static job names', () => {
|
|
4126
4571
|
const document: WorkflowDocument = {
|
|
4127
4572
|
name: 'execution events allowed',
|
|
4128
4573
|
jobs: {
|
|
@@ -4134,25 +4579,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4134
4579
|
},
|
|
4135
4580
|
};
|
|
4136
4581
|
|
|
4137
|
-
const
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4145
|
-
kind: 'agent',
|
|
4146
|
-
templates: {
|
|
4147
|
-
prompt: [
|
|
4148
|
-
{
|
|
4149
|
-
kind: 'deferred',
|
|
4150
|
-
expression: {check: 'typed'},
|
|
4151
|
-
roots: ['execution'],
|
|
4152
|
-
},
|
|
4153
|
-
],
|
|
4154
|
-
},
|
|
4155
|
-
});
|
|
4582
|
+
const error = expectInvalid(document);
|
|
4583
|
+
expect(error.issues).toEqual([
|
|
4584
|
+
expect.objectContaining({
|
|
4585
|
+
code: 'invalid-interpolation-template',
|
|
4586
|
+
path: ['jobs', 'build', 'name'],
|
|
4587
|
+
}),
|
|
4588
|
+
]);
|
|
4156
4589
|
});
|
|
4157
4590
|
|
|
4158
4591
|
it('allows untrusted context in env, prompt, and step names', () => {
|
|
@@ -4189,9 +4622,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4189
4622
|
it.each([
|
|
4190
4623
|
['model', {model: interpolation('event.model'), prompt: 'Fix it.'}, 'event'],
|
|
4191
4624
|
['provider', {provider: interpolation('inputs.provider'), prompt: 'Fix it.'}, 'inputs'],
|
|
4192
|
-
] as const)('
|
|
4625
|
+
] as const)('allows external context in agent %s interpolation', (field, step, root) => {
|
|
4193
4626
|
const document: WorkflowDocument = {
|
|
4194
|
-
name: '
|
|
4627
|
+
name: 'external agent field',
|
|
4195
4628
|
jobs: {
|
|
4196
4629
|
fix: {
|
|
4197
4630
|
steps: [step],
|
|
@@ -4199,14 +4632,77 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4199
4632
|
},
|
|
4200
4633
|
};
|
|
4201
4634
|
|
|
4202
|
-
const
|
|
4635
|
+
const model = normalizeWorkflowDocument(document);
|
|
4203
4636
|
|
|
4204
|
-
expect(
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
}
|
|
4209
|
-
|
|
4637
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4638
|
+
kind: 'agent',
|
|
4639
|
+
templates: {
|
|
4640
|
+
[field]: [{kind: 'deferred', roots: [root]}],
|
|
4641
|
+
},
|
|
4642
|
+
});
|
|
4643
|
+
});
|
|
4644
|
+
|
|
4645
|
+
it.each([
|
|
4646
|
+
['model', {model: interpolation('steps.collect.outputs.value'), prompt: 'Fix it.'}, 'steps'],
|
|
4647
|
+
[
|
|
4648
|
+
'provider',
|
|
4649
|
+
{provider: interpolation('steps.collect.outputs.value'), prompt: 'Fix it.'},
|
|
4650
|
+
'steps',
|
|
4651
|
+
],
|
|
4652
|
+
[
|
|
4653
|
+
'model',
|
|
4654
|
+
{
|
|
4655
|
+
model: interpolation('steps.filter(s, true).map(s, s.outputs.value)[0]'),
|
|
4656
|
+
prompt: 'Fix it.',
|
|
4657
|
+
},
|
|
4658
|
+
'steps',
|
|
4659
|
+
],
|
|
4660
|
+
[
|
|
4661
|
+
'provider',
|
|
4662
|
+
{
|
|
4663
|
+
provider: interpolation('steps.filter(s, true).map(s, s.outputs.value)[0]'),
|
|
4664
|
+
prompt: 'Fix it.',
|
|
4665
|
+
},
|
|
4666
|
+
'steps',
|
|
4667
|
+
],
|
|
4668
|
+
[
|
|
4669
|
+
'model',
|
|
4670
|
+
{model: interpolation('execution.events[0].data.body'), prompt: 'Fix it.'},
|
|
4671
|
+
'execution',
|
|
4672
|
+
],
|
|
4673
|
+
[
|
|
4674
|
+
'provider',
|
|
4675
|
+
{provider: interpolation('execution.events[0].data.body'), prompt: 'Fix it.'},
|
|
4676
|
+
'execution',
|
|
4677
|
+
],
|
|
4678
|
+
[
|
|
4679
|
+
'model',
|
|
4680
|
+
{model: interpolation('execution["events"][0].data.body'), prompt: 'Fix it.'},
|
|
4681
|
+
'execution',
|
|
4682
|
+
],
|
|
4683
|
+
[
|
|
4684
|
+
'provider',
|
|
4685
|
+
{provider: interpolation('execution["events"][0].data.body'), prompt: 'Fix it.'},
|
|
4686
|
+
'execution',
|
|
4687
|
+
],
|
|
4688
|
+
] as const)('allows path-specific external context in agent %s interpolation', (field, step, root) => {
|
|
4689
|
+
const document: WorkflowDocument = {
|
|
4690
|
+
name: 'path-specific external agent field',
|
|
4691
|
+
jobs: {
|
|
4692
|
+
fix: {
|
|
4693
|
+
steps: [{key: 'collect', run: 'collect'}, step],
|
|
4694
|
+
},
|
|
4695
|
+
},
|
|
4696
|
+
};
|
|
4697
|
+
|
|
4698
|
+
const model = normalizeWorkflowDocument(document);
|
|
4699
|
+
|
|
4700
|
+
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
4701
|
+
kind: 'agent',
|
|
4702
|
+
templates: {
|
|
4703
|
+
[field]: [{kind: 'deferred', roots: [root]}],
|
|
4704
|
+
},
|
|
4705
|
+
});
|
|
4210
4706
|
});
|
|
4211
4707
|
|
|
4212
4708
|
it.each([
|
|
@@ -4271,6 +4767,77 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4271
4767
|
});
|
|
4272
4768
|
});
|
|
4273
4769
|
|
|
4770
|
+
it('does not add provider catalog issues for invalid provider interpolation', () => {
|
|
4771
|
+
const document: WorkflowDocument = {
|
|
4772
|
+
name: 'invalid provider interpolation',
|
|
4773
|
+
jobs: {
|
|
4774
|
+
fix: {
|
|
4775
|
+
steps: [{harness: 'claude', provider: interpolation('foo.bar'), prompt: 'Fix it.'}],
|
|
4776
|
+
},
|
|
4777
|
+
},
|
|
4778
|
+
};
|
|
4779
|
+
|
|
4780
|
+
const error = expectInvalid(document);
|
|
4781
|
+
|
|
4782
|
+
expect(error.issues).toEqual([
|
|
4783
|
+
expect.objectContaining({
|
|
4784
|
+
code: 'unknown-interpolation-context',
|
|
4785
|
+
path: ['jobs', 'fix', 'steps', 0, 'provider'],
|
|
4786
|
+
}),
|
|
4787
|
+
]);
|
|
4788
|
+
});
|
|
4789
|
+
|
|
4790
|
+
it('skips static model catalog validation when model is interpolated', () => {
|
|
4791
|
+
const document: WorkflowDocument = {
|
|
4792
|
+
name: 'templated model',
|
|
4793
|
+
jobs: {
|
|
4794
|
+
fix: {
|
|
4795
|
+
steps: [
|
|
4796
|
+
{
|
|
4797
|
+
provider: 'anthropic',
|
|
4798
|
+
model: interpolation('run.name'),
|
|
4799
|
+
prompt: 'Fix it.',
|
|
4800
|
+
},
|
|
4801
|
+
],
|
|
4802
|
+
},
|
|
4803
|
+
},
|
|
4804
|
+
};
|
|
4805
|
+
|
|
4806
|
+
const model = normalizeWorkflowDocument(document);
|
|
4807
|
+
|
|
4808
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4809
|
+
kind: 'agent',
|
|
4810
|
+
templates: {model: [{kind: 'deferred', roots: ['run']}]},
|
|
4811
|
+
});
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4814
|
+
it('does not add model catalog issues for invalid model interpolation', () => {
|
|
4815
|
+
const document: WorkflowDocument = {
|
|
4816
|
+
name: 'invalid model interpolation',
|
|
4817
|
+
jobs: {
|
|
4818
|
+
fix: {
|
|
4819
|
+
steps: [
|
|
4820
|
+
{
|
|
4821
|
+
harness: 'pi',
|
|
4822
|
+
provider: 'anthropic',
|
|
4823
|
+
model: interpolation('foo.bar'),
|
|
4824
|
+
prompt: 'Fix it.',
|
|
4825
|
+
},
|
|
4826
|
+
],
|
|
4827
|
+
},
|
|
4828
|
+
},
|
|
4829
|
+
};
|
|
4830
|
+
|
|
4831
|
+
const error = expectInvalid(document);
|
|
4832
|
+
|
|
4833
|
+
expect(error.issues).toEqual([
|
|
4834
|
+
expect.objectContaining({
|
|
4835
|
+
code: 'unknown-interpolation-context',
|
|
4836
|
+
path: ['jobs', 'fix', 'steps', 0, 'model'],
|
|
4837
|
+
}),
|
|
4838
|
+
]);
|
|
4839
|
+
});
|
|
4840
|
+
|
|
4274
4841
|
it('still validates literal providers through the catalog', () => {
|
|
4275
4842
|
const document: WorkflowDocument = {
|
|
4276
4843
|
name: 'literal provider',
|
|
@@ -4400,7 +4967,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4400
4967
|
]);
|
|
4401
4968
|
});
|
|
4402
4969
|
|
|
4403
|
-
it('uses syntax mode for mixed open contexts
|
|
4970
|
+
it('uses syntax mode for mixed open contexts without a trust restriction', () => {
|
|
4404
4971
|
const envDocument: WorkflowDocument = {
|
|
4405
4972
|
name: 'mixed env',
|
|
4406
4973
|
env: {MIXED: interpolation('run.nope + event.x')},
|
|
@@ -4420,22 +4987,25 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4420
4987
|
};
|
|
4421
4988
|
|
|
4422
4989
|
const model = normalizeWorkflowDocument(envDocument);
|
|
4423
|
-
const
|
|
4990
|
+
const runModel = normalizeWorkflowDocument(runDocument);
|
|
4424
4991
|
|
|
4425
4992
|
expect(model.templates?.env?.MIXED?.[0]).toMatchObject({
|
|
4426
4993
|
kind: 'deferred',
|
|
4427
4994
|
expression: {check: 'syntax'},
|
|
4428
4995
|
roots: expect.arrayContaining(['run', 'event']),
|
|
4429
4996
|
});
|
|
4430
|
-
expect(
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4997
|
+
expect(runModel.jobs[0]?.steps[0]).toMatchObject({
|
|
4998
|
+
kind: 'run',
|
|
4999
|
+
templates: {
|
|
5000
|
+
command: [
|
|
5001
|
+
{kind: 'literal', value: 'echo '},
|
|
5002
|
+
{kind: 'deferred', roots: expect.arrayContaining(['run', 'event'])},
|
|
5003
|
+
],
|
|
5004
|
+
},
|
|
5005
|
+
});
|
|
4436
5006
|
});
|
|
4437
5007
|
|
|
4438
|
-
it('
|
|
5008
|
+
it('allows multi-segment run fields with external context', () => {
|
|
4439
5009
|
const document: WorkflowDocument = {
|
|
4440
5010
|
name: 'multi segment run',
|
|
4441
5011
|
jobs: {
|
|
@@ -4445,14 +5015,18 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4445
5015
|
},
|
|
4446
5016
|
};
|
|
4447
5017
|
|
|
4448
|
-
const
|
|
5018
|
+
const model = normalizeWorkflowDocument(document);
|
|
4449
5019
|
|
|
4450
|
-
expect(
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
5020
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
5021
|
+
kind: 'run',
|
|
5022
|
+
templates: {
|
|
5023
|
+
command: [
|
|
5024
|
+
{kind: 'deferred', roots: ['run']},
|
|
5025
|
+
{kind: 'literal', value: '-'},
|
|
5026
|
+
{kind: 'deferred', roots: ['event']},
|
|
5027
|
+
],
|
|
5028
|
+
},
|
|
5029
|
+
});
|
|
4456
5030
|
});
|
|
4457
5031
|
|
|
4458
5032
|
it('does not parse templates in non-string env values', () => {
|