@shipfox/api-definitions 11.0.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 +81 -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 +732 -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/list-definitions.test.ts +1 -0
- 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', () => {
|
|
@@ -1981,7 +2418,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1981
2418
|
});
|
|
1982
2419
|
});
|
|
1983
2420
|
|
|
1984
|
-
it('
|
|
2421
|
+
it('allows step outputs in run fields', () => {
|
|
1985
2422
|
const document: WorkflowDocument = {
|
|
1986
2423
|
name: 'untrusted step output',
|
|
1987
2424
|
jobs: {
|
|
@@ -1994,15 +2431,17 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1994
2431
|
},
|
|
1995
2432
|
};
|
|
1996
2433
|
|
|
1997
|
-
const
|
|
2434
|
+
const model = normalizeWorkflowDocument(document);
|
|
1998
2435
|
|
|
1999
|
-
expect(
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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
|
+
});
|
|
2006
2445
|
});
|
|
2007
2446
|
|
|
2008
2447
|
it('infers typed job output scalars for downstream job overlays', () => {
|
|
@@ -2318,29 +2757,29 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2318
2757
|
);
|
|
2319
2758
|
});
|
|
2320
2759
|
|
|
2321
|
-
it('
|
|
2760
|
+
it('accepts vars in job and step if predicates', () => {
|
|
2322
2761
|
const document: WorkflowDocument = {
|
|
2323
2762
|
name: 'vars condition',
|
|
2324
2763
|
jobs: {
|
|
2325
2764
|
build: {
|
|
2326
2765
|
if: interpolation('vars.REQUIRED == "true"'),
|
|
2327
|
-
steps: [{run: 'npm run build'}],
|
|
2766
|
+
steps: [{if: interpolation('vars.REQUIRED == "true"'), run: 'npm run build'}],
|
|
2328
2767
|
},
|
|
2329
2768
|
},
|
|
2330
2769
|
};
|
|
2331
2770
|
|
|
2332
|
-
const
|
|
2771
|
+
const model = normalizeWorkflowDocument(document);
|
|
2333
2772
|
|
|
2334
|
-
expect(
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
+
});
|
|
2344
2783
|
});
|
|
2345
2784
|
|
|
2346
2785
|
it('rejects if job references without a direct needs edge', () => {
|
|
@@ -2442,7 +2881,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2442
2881
|
path: ['jobs', 'build', 'if'],
|
|
2443
2882
|
details: expect.objectContaining({
|
|
2444
2883
|
field: 'job.if',
|
|
2445
|
-
unavailableRoots: ['execution
|
|
2884
|
+
unavailableRoots: ['execution'],
|
|
2446
2885
|
}),
|
|
2447
2886
|
}),
|
|
2448
2887
|
]);
|
|
@@ -2573,7 +3012,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2573
3012
|
]);
|
|
2574
3013
|
});
|
|
2575
3014
|
|
|
2576
|
-
it('
|
|
3015
|
+
it('accepts vars in job success', () => {
|
|
2577
3016
|
const document: WorkflowDocument = {
|
|
2578
3017
|
name: 'vars-context job success',
|
|
2579
3018
|
jobs: {
|
|
@@ -2584,21 +3023,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2584
3023
|
},
|
|
2585
3024
|
};
|
|
2586
3025
|
|
|
2587
|
-
const
|
|
3026
|
+
const model = normalizeWorkflowDocument(document);
|
|
2588
3027
|
|
|
2589
|
-
expect(
|
|
2590
|
-
expect.objectContaining({
|
|
2591
|
-
code: 'vars-context-in-server-predicate',
|
|
2592
|
-
message: expect.stringContaining('cannot reference vars'),
|
|
2593
|
-
path: ['jobs', 'build', 'success'],
|
|
2594
|
-
details: expect.objectContaining({
|
|
2595
|
-
field: 'job.success',
|
|
2596
|
-
source: 'vars.ENVIRONMENT == "prod"',
|
|
2597
|
-
rejectedRoots: ['vars'],
|
|
2598
|
-
site: 'job-resolution',
|
|
2599
|
-
}),
|
|
2600
|
-
}),
|
|
2601
|
-
]);
|
|
3028
|
+
expect(model.jobs[0]?.success).toBe('vars.ENVIRONMENT == "prod"');
|
|
2602
3029
|
});
|
|
2603
3030
|
|
|
2604
3031
|
it('reports malformed job execution timeouts', () => {
|
|
@@ -3111,12 +3538,12 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3111
3538
|
[
|
|
3112
3539
|
'vars root',
|
|
3113
3540
|
'vars.environment == "prod"',
|
|
3114
|
-
'
|
|
3541
|
+
'context-unavailable-at-predicate-site',
|
|
3115
3542
|
{
|
|
3116
3543
|
field: 'trigger.filter',
|
|
3117
3544
|
source: 'vars.environment == "prod"',
|
|
3118
3545
|
contextRoots: ['vars'],
|
|
3119
|
-
|
|
3546
|
+
unavailableRoots: ['vars'],
|
|
3120
3547
|
site: 'ingest',
|
|
3121
3548
|
},
|
|
3122
3549
|
],
|
|
@@ -3551,11 +3978,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3551
3978
|
steps: [
|
|
3552
3979
|
{
|
|
3553
3980
|
name: `deploy ${interpolation('event.action')}`,
|
|
3981
|
+
working_directory: 'packages/api',
|
|
3554
3982
|
run: `deploy ${interpolation('run.id')}`,
|
|
3555
3983
|
env: {PR_TITLE: interpolation('event.pull_request.title'), DEBUG: false},
|
|
3556
3984
|
},
|
|
3557
3985
|
{
|
|
3558
3986
|
name: `review ${interpolation('inputs.topic')}`,
|
|
3987
|
+
working_directory: `packages/${interpolation('inputs.topic')}`,
|
|
3559
3988
|
provider: 'openai',
|
|
3560
3989
|
prompt: `Review ${interpolation('event.pull_request.title')}`,
|
|
3561
3990
|
},
|
|
@@ -3602,6 +4031,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3602
4031
|
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
3603
4032
|
kind: 'run',
|
|
3604
4033
|
command: {value: `deploy ${interpolation('run.id')}`},
|
|
4034
|
+
workingDirectory: 'packages/api',
|
|
3605
4035
|
templates: {
|
|
3606
4036
|
command: [
|
|
3607
4037
|
{kind: 'literal', value: 'deploy '},
|
|
@@ -3636,7 +4066,16 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3636
4066
|
});
|
|
3637
4067
|
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
3638
4068
|
kind: 'agent',
|
|
4069
|
+
workingDirectory: `packages/${interpolation('inputs.topic')}`,
|
|
3639
4070
|
templates: {
|
|
4071
|
+
workingDirectory: [
|
|
4072
|
+
{kind: 'literal', value: 'packages/'},
|
|
4073
|
+
{
|
|
4074
|
+
kind: 'deferred',
|
|
4075
|
+
expression: {source: 'inputs.topic'},
|
|
4076
|
+
roots: ['inputs'],
|
|
4077
|
+
},
|
|
4078
|
+
],
|
|
3640
4079
|
prompt: [
|
|
3641
4080
|
{kind: 'literal', value: 'Review '},
|
|
3642
4081
|
{
|
|
@@ -3678,7 +4117,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3678
4117
|
expect(model).not.toHaveProperty('templates');
|
|
3679
4118
|
expect(model.jobs[0]?.steps[0]).not.toHaveProperty('templates');
|
|
3680
4119
|
expect(model.env).toEqual({VALUE: '$${{ event.ref }}'});
|
|
3681
|
-
expect(model.jobs[0]?.name).
|
|
4120
|
+
expect(model.jobs[0]?.name).toBe('Build app');
|
|
3682
4121
|
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
3683
4122
|
kind: 'run',
|
|
3684
4123
|
command: {value: 'echo $${{ event.ref }}'},
|
|
@@ -3737,30 +4176,32 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3737
4176
|
|
|
3738
4177
|
it.each([
|
|
3739
4178
|
['event payload', 'event.pull_request.title', ['event']],
|
|
3740
|
-
['job
|
|
3741
|
-
] as const)('
|
|
4179
|
+
['job status', 'jobs.build.status', ['jobs']],
|
|
4180
|
+
] as const)('allows %s context in run commands', (_label, source, roots) => {
|
|
3742
4181
|
const document: WorkflowDocument = {
|
|
3743
4182
|
name: 'unsafe run',
|
|
3744
4183
|
jobs: {
|
|
3745
4184
|
build: {
|
|
4185
|
+
steps: [{run: 'echo build'}],
|
|
4186
|
+
},
|
|
4187
|
+
deploy: {
|
|
4188
|
+
needs: 'build',
|
|
3746
4189
|
steps: [{run: `echo ${interpolation(source)}`}],
|
|
3747
4190
|
},
|
|
3748
4191
|
},
|
|
3749
4192
|
};
|
|
3750
4193
|
|
|
3751
|
-
const
|
|
4194
|
+
const model = normalizeWorkflowDocument(document);
|
|
3752
4195
|
|
|
3753
|
-
expect(
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
}),
|
|
3763
|
-
]);
|
|
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
|
+
});
|
|
3764
4205
|
});
|
|
3765
4206
|
|
|
3766
4207
|
it('rejects secrets in agent fields', () => {
|
|
@@ -3832,8 +4273,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3832
4273
|
it.each([
|
|
3833
4274
|
'execution.events[0].data.body',
|
|
3834
4275
|
'execution["events"][0].data.body',
|
|
3835
|
-
|
|
3836
|
-
])('rejects untrusted execution sub-paths in run commands: %s', (source) => {
|
|
4276
|
+
])('allows execution event sub-paths in run commands: %s', (source) => {
|
|
3837
4277
|
const document: WorkflowDocument = {
|
|
3838
4278
|
name: 'unsafe execution run',
|
|
3839
4279
|
jobs: {
|
|
@@ -3843,42 +4283,17 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3843
4283
|
},
|
|
3844
4284
|
};
|
|
3845
4285
|
|
|
3846
|
-
const
|
|
3847
|
-
|
|
3848
|
-
expect(error.issues).toEqual([
|
|
3849
|
-
expect.objectContaining({
|
|
3850
|
-
code: 'untrusted-context-in-field',
|
|
3851
|
-
path: ['jobs', 'build', 'steps', 0, 'run'],
|
|
3852
|
-
details: expect.objectContaining({
|
|
3853
|
-
field: 'run',
|
|
3854
|
-
rejectedRoots: ['execution'],
|
|
3855
|
-
}),
|
|
3856
|
-
}),
|
|
3857
|
-
]);
|
|
3858
|
-
});
|
|
4286
|
+
const model = normalizeWorkflowDocument(document);
|
|
3859
4287
|
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
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
|
+
],
|
|
3867
4295
|
},
|
|
3868
|
-
};
|
|
3869
|
-
|
|
3870
|
-
const error = expectInvalid(document);
|
|
3871
|
-
|
|
3872
|
-
expect(error.issues).toEqual([
|
|
3873
|
-
expect.objectContaining({
|
|
3874
|
-
code: 'untrusted-context-in-field',
|
|
3875
|
-
path: ['jobs', 'build', 'steps', 0, 'run'],
|
|
3876
|
-
details: expect.objectContaining({
|
|
3877
|
-
field: 'run',
|
|
3878
|
-
rejectedRoots: ['executions'],
|
|
3879
|
-
}),
|
|
3880
|
-
}),
|
|
3881
|
-
]);
|
|
4296
|
+
});
|
|
3882
4297
|
});
|
|
3883
4298
|
|
|
3884
4299
|
it.each([
|
|
@@ -3994,7 +4409,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3994
4409
|
});
|
|
3995
4410
|
});
|
|
3996
4411
|
|
|
3997
|
-
it('
|
|
4412
|
+
it('rejects dynamic job names in favor of execution_name', () => {
|
|
3998
4413
|
const document: WorkflowDocument = {
|
|
3999
4414
|
name: 'job display context',
|
|
4000
4415
|
jobs: {
|
|
@@ -4010,16 +4425,19 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4010
4425
|
},
|
|
4011
4426
|
};
|
|
4012
4427
|
|
|
4013
|
-
const
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
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
|
+
]);
|
|
4023
4441
|
});
|
|
4024
4442
|
|
|
4025
4443
|
it('does not apply availability checks to workflow-level or job-level env', () => {
|
|
@@ -4149,7 +4567,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4149
4567
|
});
|
|
4150
4568
|
});
|
|
4151
4569
|
|
|
4152
|
-
it('
|
|
4570
|
+
it('rejects execution events in static job names', () => {
|
|
4153
4571
|
const document: WorkflowDocument = {
|
|
4154
4572
|
name: 'execution events allowed',
|
|
4155
4573
|
jobs: {
|
|
@@ -4161,25 +4579,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4161
4579
|
},
|
|
4162
4580
|
};
|
|
4163
4581
|
|
|
4164
|
-
const
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4172
|
-
kind: 'agent',
|
|
4173
|
-
templates: {
|
|
4174
|
-
prompt: [
|
|
4175
|
-
{
|
|
4176
|
-
kind: 'deferred',
|
|
4177
|
-
expression: {check: 'typed'},
|
|
4178
|
-
roots: ['execution'],
|
|
4179
|
-
},
|
|
4180
|
-
],
|
|
4181
|
-
},
|
|
4182
|
-
});
|
|
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
|
+
]);
|
|
4183
4589
|
});
|
|
4184
4590
|
|
|
4185
4591
|
it('allows untrusted context in env, prompt, and step names', () => {
|
|
@@ -4216,9 +4622,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4216
4622
|
it.each([
|
|
4217
4623
|
['model', {model: interpolation('event.model'), prompt: 'Fix it.'}, 'event'],
|
|
4218
4624
|
['provider', {provider: interpolation('inputs.provider'), prompt: 'Fix it.'}, 'inputs'],
|
|
4219
|
-
] as const)('
|
|
4625
|
+
] as const)('allows external context in agent %s interpolation', (field, step, root) => {
|
|
4220
4626
|
const document: WorkflowDocument = {
|
|
4221
|
-
name: '
|
|
4627
|
+
name: 'external agent field',
|
|
4222
4628
|
jobs: {
|
|
4223
4629
|
fix: {
|
|
4224
4630
|
steps: [step],
|
|
@@ -4226,14 +4632,77 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4226
4632
|
},
|
|
4227
4633
|
};
|
|
4228
4634
|
|
|
4229
|
-
const
|
|
4635
|
+
const model = normalizeWorkflowDocument(document);
|
|
4230
4636
|
|
|
4231
|
-
expect(
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
}
|
|
4236
|
-
|
|
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
|
+
});
|
|
4237
4706
|
});
|
|
4238
4707
|
|
|
4239
4708
|
it.each([
|
|
@@ -4298,6 +4767,77 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4298
4767
|
});
|
|
4299
4768
|
});
|
|
4300
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
|
+
|
|
4301
4841
|
it('still validates literal providers through the catalog', () => {
|
|
4302
4842
|
const document: WorkflowDocument = {
|
|
4303
4843
|
name: 'literal provider',
|
|
@@ -4427,7 +4967,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4427
4967
|
]);
|
|
4428
4968
|
});
|
|
4429
4969
|
|
|
4430
|
-
it('uses syntax mode for mixed open contexts
|
|
4970
|
+
it('uses syntax mode for mixed open contexts without a trust restriction', () => {
|
|
4431
4971
|
const envDocument: WorkflowDocument = {
|
|
4432
4972
|
name: 'mixed env',
|
|
4433
4973
|
env: {MIXED: interpolation('run.nope + event.x')},
|
|
@@ -4447,22 +4987,25 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4447
4987
|
};
|
|
4448
4988
|
|
|
4449
4989
|
const model = normalizeWorkflowDocument(envDocument);
|
|
4450
|
-
const
|
|
4990
|
+
const runModel = normalizeWorkflowDocument(runDocument);
|
|
4451
4991
|
|
|
4452
4992
|
expect(model.templates?.env?.MIXED?.[0]).toMatchObject({
|
|
4453
4993
|
kind: 'deferred',
|
|
4454
4994
|
expression: {check: 'syntax'},
|
|
4455
4995
|
roots: expect.arrayContaining(['run', 'event']),
|
|
4456
4996
|
});
|
|
4457
|
-
expect(
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
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
|
+
});
|
|
4463
5006
|
});
|
|
4464
5007
|
|
|
4465
|
-
it('
|
|
5008
|
+
it('allows multi-segment run fields with external context', () => {
|
|
4466
5009
|
const document: WorkflowDocument = {
|
|
4467
5010
|
name: 'multi segment run',
|
|
4468
5011
|
jobs: {
|
|
@@ -4472,14 +5015,18 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4472
5015
|
},
|
|
4473
5016
|
};
|
|
4474
5017
|
|
|
4475
|
-
const
|
|
5018
|
+
const model = normalizeWorkflowDocument(document);
|
|
4476
5019
|
|
|
4477
|
-
expect(
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
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
|
+
});
|
|
4483
5030
|
});
|
|
4484
5031
|
|
|
4485
5032
|
it('does not parse templates in non-string env values', () => {
|