@shipfox/api-definitions 11.0.0 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +13 -12
- package/CHANGELOG.md +106 -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 +5 -1
- 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/errors.d.ts +2 -2
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/integrations.d.ts +1 -1
- 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 +315 -74
- 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/core/workflow-yaml/invalid-workflow-yaml-error.d.ts.map +1 -1
- package/dist/db/db.d.ts +48 -10
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/definitions.d.ts +1 -1
- package/dist/db/schema/outbox.d.ts +1 -1
- package/dist/db/schema/sync-states.d.ts +22 -3
- 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/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.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/presentation/subscribers/index.d.ts +1 -0
- package/dist/presentation/subscribers/index.d.ts.map +1 -1
- package/dist/presentation/subscribers/index.js +1 -0
- package/dist/presentation/subscribers/index.js.map +1 -1
- package/dist/presentation/subscribers/on-integration-connection-available.d.ts +5 -0
- package/dist/presentation/subscribers/on-integration-connection-available.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-integration-connection-available.js +32 -0
- package/dist/presentation/subscribers/on-integration-connection-available.js.map +1 -0
- package/dist/presentation/subscribers/start-definition-sync.d.ts +1 -0
- package/dist/presentation/subscribers/start-definition-sync.d.ts.map +1 -1
- package/dist/presentation/subscribers/start-definition-sync.js +30 -17
- package/dist/presentation/subscribers/start-definition-sync.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 +17 -17
- 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 -3
- 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 +335 -90
- 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 +876 -202
- 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/index.ts +12 -1
- 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/presentation/subscribers/index.ts +1 -0
- package/src/presentation/subscribers/on-integration-connection-available.test.ts +132 -0
- package/src/presentation/subscribers/on-integration-connection-available.ts +44 -0
- package/src/presentation/subscribers/start-definition-sync.ts +35 -19
- 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
|
@@ -69,16 +69,219 @@ const integrationValidationContext = {
|
|
|
69
69
|
],
|
|
70
70
|
},
|
|
71
71
|
],
|
|
72
|
+
[
|
|
73
|
+
'jira',
|
|
74
|
+
{
|
|
75
|
+
selectors: [
|
|
76
|
+
{token: 'get_issue', kind: 'standalone', sensitivity: 'read', sensitive: false},
|
|
77
|
+
{token: 'add_comment', kind: 'standalone', sensitivity: 'write', sensitive: false},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
72
81
|
]),
|
|
73
82
|
workspaceConnectionSnapshot: new Map([
|
|
74
83
|
['github-main', {id: 'conn_1', provider: 'github', capabilities: ['agent_tools']}],
|
|
75
84
|
['sentry-main', {id: 'conn_2', provider: 'sentry', capabilities: []}],
|
|
76
85
|
['linear-main', {id: 'conn_3', provider: 'linear', capabilities: ['agent_tools']}],
|
|
86
|
+
['jira-main', {id: 'conn_4', provider: 'jira', capabilities: ['agent_tools']}],
|
|
77
87
|
]),
|
|
78
88
|
defaultConnectionSlug: 'github-main',
|
|
79
89
|
} satisfies IntegrationValidationContext;
|
|
80
90
|
|
|
81
91
|
describe('normalizeWorkflowDocument', () => {
|
|
92
|
+
it('rejects interpolated workflow names in favor of run_name', () => {
|
|
93
|
+
const error = expectInvalid({
|
|
94
|
+
name: interpolation('inputs.environment'),
|
|
95
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(error.issues).toEqual([
|
|
99
|
+
expect.objectContaining({
|
|
100
|
+
code: 'invalid-interpolation-template',
|
|
101
|
+
message: 'Workflow name must be literal. Move runtime interpolation to run_name.',
|
|
102
|
+
path: ['name'],
|
|
103
|
+
}),
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects unterminated workflow-name templates with the migration guidance', () => {
|
|
108
|
+
const error = expectInvalid({
|
|
109
|
+
name: '$' + '{{ unterminated',
|
|
110
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(error.issues).toEqual([
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
code: 'invalid-interpolation-template',
|
|
116
|
+
message: 'Workflow name must be literal. Move runtime interpolation to run_name.',
|
|
117
|
+
path: ['name'],
|
|
118
|
+
details: expect.objectContaining({reason: 'unterminated opener'}),
|
|
119
|
+
}),
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects unterminated job-name templates with the migration guidance', () => {
|
|
124
|
+
const error = expectInvalid({
|
|
125
|
+
name: 'Deploy',
|
|
126
|
+
jobs: {
|
|
127
|
+
build: {name: '$' + '{{ unterminated', steps: [{run: 'build'}]},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(error.issues).toEqual([
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
code: 'invalid-interpolation-template',
|
|
134
|
+
message: 'Job name must be literal. Move runtime interpolation to execution_name.',
|
|
135
|
+
path: ['jobs', 'build', 'name'],
|
|
136
|
+
details: expect.objectContaining({reason: 'unterminated opener'}),
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('normalizes workflow run and job execution name templates', () => {
|
|
142
|
+
const model = normalizeWorkflowDocument({
|
|
143
|
+
name: 'Deploy application',
|
|
144
|
+
run_name: `Deploy ${interpolation('inputs.environment')}`,
|
|
145
|
+
jobs: {
|
|
146
|
+
deploy: {
|
|
147
|
+
name: 'Deploy',
|
|
148
|
+
execution_name: `Deploy ${interpolation('event.environment')}`,
|
|
149
|
+
steps: [{run: 'deploy'}],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(model.runName).toMatchObject([
|
|
155
|
+
{kind: 'literal', value: 'Deploy '},
|
|
156
|
+
{kind: 'deferred', roots: ['inputs'], fillTarget: 'run-creation'},
|
|
157
|
+
]);
|
|
158
|
+
expect(model.jobs[0]?.executionName).toMatchObject([
|
|
159
|
+
{kind: 'literal', value: 'Deploy '},
|
|
160
|
+
{kind: 'deferred', roots: ['event'], fillTarget: 'execution-creation'},
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('preserves literal workflow run and job execution names', () => {
|
|
165
|
+
const model = normalizeWorkflowDocument({
|
|
166
|
+
name: 'Workflow',
|
|
167
|
+
run_name: 'Static run name',
|
|
168
|
+
jobs: {
|
|
169
|
+
build: {
|
|
170
|
+
execution_name: 'Static execution name',
|
|
171
|
+
steps: [{run: 'build'}],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(model.runName).toEqual([{kind: 'literal', value: 'Static run name'}]);
|
|
177
|
+
expect(model.jobs[0]?.executionName).toEqual([
|
|
178
|
+
{kind: 'literal', value: 'Static execution name'},
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('unescapes literal workflow and job names', () => {
|
|
183
|
+
const escapedName = `$${interpolation('inputs.environment')}`;
|
|
184
|
+
const model = normalizeWorkflowDocument({
|
|
185
|
+
name: escapedName,
|
|
186
|
+
jobs: {
|
|
187
|
+
build: {name: escapedName, steps: [{run: 'build'}]},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(model.name).toBe(interpolation('inputs.environment'));
|
|
192
|
+
expect(model.jobs[0]?.name).toBe(interpolation('inputs.environment'));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it.each([
|
|
196
|
+
[
|
|
197
|
+
'run_name',
|
|
198
|
+
{
|
|
199
|
+
name: 'Workflow',
|
|
200
|
+
run_name: interpolation('steps.build.outputs.name'),
|
|
201
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
'execution_name',
|
|
206
|
+
{
|
|
207
|
+
name: 'Workflow',
|
|
208
|
+
jobs: {
|
|
209
|
+
build: {
|
|
210
|
+
execution_name: interpolation('steps.build.outputs.name'),
|
|
211
|
+
steps: [{run: 'build'}],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
] as const)('rejects unavailable context in %s', (_field, document) => {
|
|
217
|
+
const error = expectInvalid(document as unknown as WorkflowDocument);
|
|
218
|
+
expect(error.issues).toEqual([
|
|
219
|
+
expect.objectContaining({code: 'context-unavailable-at-fill-site'}),
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('allows computed access to unrelated dynamic-name context fields', () => {
|
|
224
|
+
const model = normalizeWorkflowDocument({
|
|
225
|
+
name: 'Workflow',
|
|
226
|
+
run_name: interpolation('run["id"]'),
|
|
227
|
+
jobs: {
|
|
228
|
+
build: {
|
|
229
|
+
execution_name: interpolation('execution["status"]'),
|
|
230
|
+
steps: [{run: 'build'}],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(model.runName).toMatchObject([{kind: 'deferred', roots: ['run']}]);
|
|
236
|
+
expect(model.jobs[0]?.executionName).toMatchObject([{kind: 'deferred', roots: ['execution']}]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it.each([
|
|
240
|
+
[
|
|
241
|
+
'run_name',
|
|
242
|
+
{
|
|
243
|
+
name: 'Workflow',
|
|
244
|
+
run_name: interpolation('run.name'),
|
|
245
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
'run_name bracket access',
|
|
250
|
+
{
|
|
251
|
+
name: 'Workflow',
|
|
252
|
+
run_name: interpolation('run["name"]'),
|
|
253
|
+
jobs: {build: {steps: [{run: 'build'}]}},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
'execution_name',
|
|
258
|
+
{
|
|
259
|
+
name: 'Workflow',
|
|
260
|
+
jobs: {
|
|
261
|
+
build: {
|
|
262
|
+
execution_name: interpolation('execution.name'),
|
|
263
|
+
steps: [{run: 'build'}],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
[
|
|
269
|
+
'execution_name bracket access',
|
|
270
|
+
{
|
|
271
|
+
name: 'Workflow',
|
|
272
|
+
jobs: {
|
|
273
|
+
build: {
|
|
274
|
+
execution_name: interpolation('execution["name"]'),
|
|
275
|
+
steps: [{run: 'build'}],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
] as const)('rejects dynamic-name self-reference in %s', (_field, document) => {
|
|
281
|
+
const error = expectInvalid(document as unknown as WorkflowDocument);
|
|
282
|
+
expect(error.issues).toEqual([expect.objectContaining({code: 'dynamic-name-self-reference'})]);
|
|
283
|
+
});
|
|
284
|
+
|
|
82
285
|
it('normalizes a workflow document into a WorkflowModel', () => {
|
|
83
286
|
const document: WorkflowDocument = {
|
|
84
287
|
name: 'simple build',
|
|
@@ -154,7 +357,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
154
357
|
{
|
|
155
358
|
key: 'review',
|
|
156
359
|
harness: 'claude',
|
|
157
|
-
model: '
|
|
360
|
+
model: 'claude-opus-4-8',
|
|
158
361
|
provider: 'anthropic',
|
|
159
362
|
prompt: 'Review the fix.',
|
|
160
363
|
thinking: 'low',
|
|
@@ -454,6 +657,56 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
454
657
|
});
|
|
455
658
|
});
|
|
456
659
|
|
|
660
|
+
it('requires allow_write for Jira write tools', () => {
|
|
661
|
+
const document: WorkflowDocument = {
|
|
662
|
+
name: 'jira write integrations',
|
|
663
|
+
jobs: {
|
|
664
|
+
fix: {
|
|
665
|
+
steps: [
|
|
666
|
+
{
|
|
667
|
+
prompt: 'Comment on the issue.',
|
|
668
|
+
integrations: [{connection: 'jira-main', include: ['add_comment']}],
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const error = expectInvalid(document, {integrationValidationContext});
|
|
676
|
+
|
|
677
|
+
expect(error.issues).toMatchObject([
|
|
678
|
+
{
|
|
679
|
+
code: 'integration-write-not-allowed',
|
|
680
|
+
details: {tokens: ['add_comment']},
|
|
681
|
+
},
|
|
682
|
+
]);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('accepts Jira write tools when allow_write is true', () => {
|
|
686
|
+
const document: WorkflowDocument = {
|
|
687
|
+
name: 'jira write integrations',
|
|
688
|
+
jobs: {
|
|
689
|
+
fix: {
|
|
690
|
+
steps: [
|
|
691
|
+
{
|
|
692
|
+
prompt: 'Comment on the issue.',
|
|
693
|
+
integrations: [
|
|
694
|
+
{connection: 'jira-main', include: ['add_comment'], allow_write: true},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const model = normalizeWorkflowDocument(document, {integrationValidationContext});
|
|
703
|
+
|
|
704
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
705
|
+
kind: 'agent',
|
|
706
|
+
integrations: [{connection: 'jira-main', include: ['add_comment'], allowWrite: true}],
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
457
710
|
it('reports connection failures before token checks', () => {
|
|
458
711
|
const document: WorkflowDocument = {
|
|
459
712
|
name: 'missing connection',
|
|
@@ -538,6 +791,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
538
791
|
{
|
|
539
792
|
harness: 'pi',
|
|
540
793
|
provider: 'workspace-openai-compatible',
|
|
794
|
+
model: 'workspace-model',
|
|
541
795
|
prompt: 'Fix it.',
|
|
542
796
|
},
|
|
543
797
|
],
|
|
@@ -551,6 +805,80 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
551
805
|
kind: 'agent',
|
|
552
806
|
harness: 'pi',
|
|
553
807
|
provider: 'workspace-openai-compatible',
|
|
808
|
+
model: 'workspace-model',
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('reports unsupported literal models', () => {
|
|
813
|
+
const document: WorkflowDocument = {
|
|
814
|
+
name: 'agent build',
|
|
815
|
+
jobs: {
|
|
816
|
+
fix: {
|
|
817
|
+
steps: [{harness: 'pi', provider: 'anthropic', model: 'not-a-model', prompt: 'Fix it.'}],
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const error = expectInvalid(document);
|
|
823
|
+
|
|
824
|
+
expect(error.issues).toEqual([
|
|
825
|
+
{
|
|
826
|
+
code: 'invalid-model',
|
|
827
|
+
message:
|
|
828
|
+
'Agent model "not-a-model" is not available for harness "pi" and provider "anthropic".',
|
|
829
|
+
path: ['jobs', 'fix', 'steps', 0, 'model'],
|
|
830
|
+
details: {harness: 'pi', provider: 'anthropic', model: 'not-a-model'},
|
|
831
|
+
},
|
|
832
|
+
]);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('allows custom providers without an explicit harness', () => {
|
|
836
|
+
const document: WorkflowDocument = {
|
|
837
|
+
name: 'agent build',
|
|
838
|
+
jobs: {
|
|
839
|
+
fix: {
|
|
840
|
+
steps: [
|
|
841
|
+
{
|
|
842
|
+
provider: 'workspace-openai-compatible',
|
|
843
|
+
model: 'workspace-model',
|
|
844
|
+
prompt: 'Fix it.',
|
|
845
|
+
},
|
|
846
|
+
],
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const model = normalizeWorkflowDocument(document);
|
|
852
|
+
|
|
853
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
854
|
+
kind: 'agent',
|
|
855
|
+
provider: 'workspace-openai-compatible',
|
|
856
|
+
model: 'workspace-model',
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('defers harness-specific validation when harness is omitted', () => {
|
|
861
|
+
const document: WorkflowDocument = {
|
|
862
|
+
name: 'workspace-default harness',
|
|
863
|
+
jobs: {
|
|
864
|
+
fix: {
|
|
865
|
+
steps: [
|
|
866
|
+
{
|
|
867
|
+
provider: 'anthropic',
|
|
868
|
+
model: 'claude-sonnet-4-6',
|
|
869
|
+
prompt: 'Fix it.',
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const model = normalizeWorkflowDocument(document);
|
|
877
|
+
|
|
878
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
879
|
+
kind: 'agent',
|
|
880
|
+
provider: 'anthropic',
|
|
881
|
+
model: 'claude-sonnet-4-6',
|
|
554
882
|
});
|
|
555
883
|
});
|
|
556
884
|
|
|
@@ -743,6 +1071,29 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
743
1071
|
});
|
|
744
1072
|
});
|
|
745
1073
|
|
|
1074
|
+
it('defers an interpolated thinking level to dispatch', () => {
|
|
1075
|
+
const document: WorkflowDocument = {
|
|
1076
|
+
name: 'agent build',
|
|
1077
|
+
jobs: {
|
|
1078
|
+
fix: {
|
|
1079
|
+
steps: [
|
|
1080
|
+
{
|
|
1081
|
+
harness: 'claude',
|
|
1082
|
+
prompt: 'Fix it.',
|
|
1083
|
+
thinking: workflowInterpolation('vars.THINKING'),
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const model = normalizeWorkflowDocument(document);
|
|
1091
|
+
const step = model.jobs[0]?.steps[0];
|
|
1092
|
+
|
|
1093
|
+
if (step?.kind !== 'agent') throw new Error('Expected an agent step');
|
|
1094
|
+
expect(step.templates?.thinking).toMatchObject([{kind: 'deferred', roots: ['vars']}]);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
746
1097
|
it('normalizes job success expressions and execution timeouts', () => {
|
|
747
1098
|
const document: WorkflowDocument = {
|
|
748
1099
|
name: 'job controls',
|
|
@@ -831,6 +1182,144 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
831
1182
|
expect(model.jobs[0]?.checkout).toEqual(DEFAULT_JOB_CHECKOUT);
|
|
832
1183
|
});
|
|
833
1184
|
|
|
1185
|
+
it('preserves the job checkout opt-out in the model', () => {
|
|
1186
|
+
const model = normalizeWorkflowDocument({
|
|
1187
|
+
name: 'checkout disabled',
|
|
1188
|
+
jobs: {
|
|
1189
|
+
build: {
|
|
1190
|
+
checkout: false,
|
|
1191
|
+
steps: [{run: 'npm test'}],
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
expect(model.jobs[0]?.checkout).toBe(false);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('normalizes checkout-step targets from earlier step outputs', () => {
|
|
1200
|
+
const model = normalizeWorkflowDocument({
|
|
1201
|
+
name: 'templated checkout step',
|
|
1202
|
+
jobs: {
|
|
1203
|
+
build: {
|
|
1204
|
+
steps: [
|
|
1205
|
+
{
|
|
1206
|
+
key: 'route',
|
|
1207
|
+
run: 'echo route',
|
|
1208
|
+
outputs: {
|
|
1209
|
+
connection: {type: 'string'},
|
|
1210
|
+
repository: {type: 'string'},
|
|
1211
|
+
ref: {type: 'string'},
|
|
1212
|
+
path: {type: 'string'},
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
key: 'checkout',
|
|
1217
|
+
checkout: {
|
|
1218
|
+
connection: interpolation('inputs.connection'),
|
|
1219
|
+
repository: interpolation('steps.route.outputs.repository'),
|
|
1220
|
+
ref: interpolation('steps.route.outputs.ref'),
|
|
1221
|
+
path: interpolation('steps.route.outputs.path'),
|
|
1222
|
+
},
|
|
1223
|
+
},
|
|
1224
|
+
],
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
1230
|
+
kind: 'checkout',
|
|
1231
|
+
checkout: {
|
|
1232
|
+
connection: interpolation('inputs.connection'),
|
|
1233
|
+
repository: interpolation('steps.route.outputs.repository'),
|
|
1234
|
+
ref: interpolation('steps.route.outputs.ref'),
|
|
1235
|
+
path: interpolation('steps.route.outputs.path'),
|
|
1236
|
+
fetchDepth: 1,
|
|
1237
|
+
permissions: {contents: 'read'},
|
|
1238
|
+
persistCredentials: true,
|
|
1239
|
+
templates: {
|
|
1240
|
+
connection: [{kind: 'deferred', roots: ['inputs'], fillTarget: 'run-creation'}],
|
|
1241
|
+
repository: [{kind: 'deferred', roots: ['steps'], fillTarget: 'step-dispatch'}],
|
|
1242
|
+
ref: [{kind: 'deferred', roots: ['steps'], fillTarget: 'step-dispatch'}],
|
|
1243
|
+
path: [{kind: 'deferred', roots: ['steps'], fillTarget: 'step-dispatch'}],
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
it('reports invalid checkout target combinations from model normalization', () => {
|
|
1250
|
+
const error = expectInvalid({
|
|
1251
|
+
name: 'invalid checkout target',
|
|
1252
|
+
jobs: {
|
|
1253
|
+
build: {
|
|
1254
|
+
steps: [{checkout: {project: 'project-id', repository: 'acme/api'}}],
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
expect(error.issues).toContainEqual(
|
|
1260
|
+
expect.objectContaining({
|
|
1261
|
+
code: 'checkout-target-invalid',
|
|
1262
|
+
path: ['jobs', 'build', 'steps', 0, 'checkout', 'repository'],
|
|
1263
|
+
}),
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
const missingRepository = expectInvalid({
|
|
1267
|
+
name: 'missing checkout repository',
|
|
1268
|
+
jobs: {
|
|
1269
|
+
build: {
|
|
1270
|
+
steps: [{checkout: {connection: 'github'}}],
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
expect(missingRepository.issues).toContainEqual(
|
|
1276
|
+
expect.objectContaining({
|
|
1277
|
+
code: 'checkout-target-invalid',
|
|
1278
|
+
path: ['jobs', 'build', 'steps', 0, 'checkout', 'repository'],
|
|
1279
|
+
details: {fields: ['connection', 'repository']},
|
|
1280
|
+
}),
|
|
1281
|
+
);
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
it('normalizes checkout steps and their static defaults', () => {
|
|
1285
|
+
const model = normalizeWorkflowDocument({
|
|
1286
|
+
name: 'checkout step',
|
|
1287
|
+
jobs: {
|
|
1288
|
+
build: {
|
|
1289
|
+
steps: [
|
|
1290
|
+
{
|
|
1291
|
+
key: 'fetch-target',
|
|
1292
|
+
checkout: {
|
|
1293
|
+
repository: 'acme/api',
|
|
1294
|
+
ref: 'refs/heads/main',
|
|
1295
|
+
'fetch-depth': 0,
|
|
1296
|
+
path: 'target',
|
|
1297
|
+
permissions: {contents: 'write'},
|
|
1298
|
+
'persist-credentials': false,
|
|
1299
|
+
force: true,
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
],
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
1308
|
+
id: expect.stringContaining('fetch-target'),
|
|
1309
|
+
key: 'fetch-target',
|
|
1310
|
+
kind: 'checkout',
|
|
1311
|
+
checkout: {
|
|
1312
|
+
repository: 'acme/api',
|
|
1313
|
+
ref: 'refs/heads/main',
|
|
1314
|
+
fetchDepth: 0,
|
|
1315
|
+
path: 'target',
|
|
1316
|
+
permissions: {contents: 'write'},
|
|
1317
|
+
persistCredentials: false,
|
|
1318
|
+
force: true,
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1322
|
+
|
|
834
1323
|
it('normalizes checkout contents write and defaults persisted credentials', () => {
|
|
835
1324
|
const document: WorkflowDocument = {
|
|
836
1325
|
name: 'write checkout',
|
|
@@ -880,7 +1369,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
880
1369
|
name: 'listen for reviews',
|
|
881
1370
|
jobs: {
|
|
882
1371
|
review: {
|
|
883
|
-
|
|
1372
|
+
execution_name: displayName,
|
|
884
1373
|
listening: {
|
|
885
1374
|
on: [
|
|
886
1375
|
{
|
|
@@ -915,7 +1404,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
915
1404
|
onResolve: 'cancel',
|
|
916
1405
|
},
|
|
917
1406
|
});
|
|
918
|
-
expect(model.jobs[0]?.
|
|
1407
|
+
expect(model.jobs[0]?.executionName?.[1]).toMatchObject({
|
|
919
1408
|
kind: 'deferred',
|
|
920
1409
|
expression: {source: 'execution.index', check: 'typed'},
|
|
921
1410
|
});
|
|
@@ -968,7 +1457,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
968
1457
|
]);
|
|
969
1458
|
});
|
|
970
1459
|
|
|
971
|
-
it('validates listener filters against
|
|
1460
|
+
it('validates listener filters against their persisted snapshot roots', () => {
|
|
972
1461
|
const document: WorkflowDocument = {
|
|
973
1462
|
name: 'listener filter roots',
|
|
974
1463
|
jobs: {
|
|
@@ -983,7 +1472,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
983
1472
|
source: 'github',
|
|
984
1473
|
event: 'pull_request_review',
|
|
985
1474
|
filter:
|
|
986
|
-
'run.id != "" && inputs.target == event.issue.number &&
|
|
1475
|
+
'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
1476
|
},
|
|
988
1477
|
],
|
|
989
1478
|
until: [
|
|
@@ -1002,7 +1491,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1002
1491
|
const model = normalizeWorkflowDocument(document);
|
|
1003
1492
|
|
|
1004
1493
|
expect(model.jobs[1]?.listening?.on[0]?.filter).toBe(
|
|
1005
|
-
'run.id != "" && inputs.target == event.issue.number &&
|
|
1494
|
+
'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
1495
|
);
|
|
1007
1496
|
expect(model.jobs[1]?.listening?.until?.[0]?.filter).toBe('event.action == "closed"');
|
|
1008
1497
|
});
|
|
@@ -1010,6 +1499,8 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1010
1499
|
it.each([
|
|
1011
1500
|
['step root', 'step.status == "succeeded"', 'context-unavailable-at-predicate-site'],
|
|
1012
1501
|
['steps root', 'steps.build.outputs.sha == "abc"', 'context-unavailable-at-predicate-site'],
|
|
1502
|
+
['executions root', 'executions.size() > 0', 'context-unavailable-at-predicate-site'],
|
|
1503
|
+
['matrix root', 'matrix.os == "linux"', 'context-unavailable-at-predicate-site'],
|
|
1013
1504
|
['runner root', 'runner.os == "linux"', 'runner-context-in-server-predicate'],
|
|
1014
1505
|
['non-boolean source', 'event.action', 'invalid-listener-filter'],
|
|
1015
1506
|
] as const)('reports invalid listener on filters for %s', (_label, filter, code) => {
|
|
@@ -1114,12 +1605,12 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1114
1605
|
]);
|
|
1115
1606
|
});
|
|
1116
1607
|
|
|
1117
|
-
it('
|
|
1608
|
+
it('accepts explicit model ids present in the harness catalog', () => {
|
|
1118
1609
|
const document: WorkflowDocument = {
|
|
1119
1610
|
name: 'agent build',
|
|
1120
1611
|
jobs: {
|
|
1121
1612
|
fix: {
|
|
1122
|
-
steps: [{provider: 'openai', model: 'gpt-4.1', prompt: 'Fix it.'}],
|
|
1613
|
+
steps: [{harness: 'pi', provider: 'openai', model: 'gpt-4.1', prompt: 'Fix it.'}],
|
|
1123
1614
|
},
|
|
1124
1615
|
},
|
|
1125
1616
|
};
|
|
@@ -1602,7 +2093,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1602
2093
|
});
|
|
1603
2094
|
});
|
|
1604
2095
|
|
|
1605
|
-
it('
|
|
2096
|
+
it('rejects server roots omitted from the gate success context', () => {
|
|
1606
2097
|
const document: WorkflowDocument = {
|
|
1607
2098
|
name: 'server-context gate',
|
|
1608
2099
|
jobs: {
|
|
@@ -1610,14 +2101,21 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1610
2101
|
},
|
|
1611
2102
|
};
|
|
1612
2103
|
|
|
1613
|
-
const
|
|
2104
|
+
const error = expectInvalid(document);
|
|
1614
2105
|
|
|
1615
|
-
expect(
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2106
|
+
expect(error.issues).toEqual([
|
|
2107
|
+
expect.objectContaining({
|
|
2108
|
+
code: 'context-unavailable-at-predicate-site',
|
|
2109
|
+
message: expect.stringContaining('Step gate success references context "run"'),
|
|
2110
|
+
path: ['jobs', 'build', 'steps', 0, 'gate', 'success'],
|
|
2111
|
+
details: expect.objectContaining({
|
|
2112
|
+
field: 'step.success',
|
|
2113
|
+
source: 'run.id != ""',
|
|
2114
|
+
unavailableRoots: ['run'],
|
|
2115
|
+
site: 'step-report',
|
|
2116
|
+
}),
|
|
2117
|
+
}),
|
|
2118
|
+
]);
|
|
1621
2119
|
});
|
|
1622
2120
|
|
|
1623
2121
|
it('rejects runner-host roots in gate success with a server-predicate issue', () => {
|
|
@@ -1647,7 +2145,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1647
2145
|
]);
|
|
1648
2146
|
});
|
|
1649
2147
|
|
|
1650
|
-
it('
|
|
2148
|
+
it('accepts vars in gate success', () => {
|
|
1651
2149
|
const document: WorkflowDocument = {
|
|
1652
2150
|
name: 'vars-context gate',
|
|
1653
2151
|
jobs: {
|
|
@@ -1657,24 +2155,15 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1657
2155
|
},
|
|
1658
2156
|
};
|
|
1659
2157
|
|
|
1660
|
-
const
|
|
2158
|
+
const model = normalizeWorkflowDocument(document);
|
|
1661
2159
|
|
|
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
|
-
]);
|
|
2160
|
+
expect(model.jobs[0]?.steps[0]?.gate?.success).toMatchObject({
|
|
2161
|
+
source: 'vars.REQUIRED == "true"',
|
|
2162
|
+
check: 'syntax',
|
|
2163
|
+
});
|
|
1675
2164
|
});
|
|
1676
2165
|
|
|
1677
|
-
it('
|
|
2166
|
+
it('rejects jobs root references omitted from the gate success context', () => {
|
|
1678
2167
|
const document: WorkflowDocument = {
|
|
1679
2168
|
name: 'jobs-context gate',
|
|
1680
2169
|
jobs: {
|
|
@@ -1688,13 +2177,21 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1688
2177
|
},
|
|
1689
2178
|
};
|
|
1690
2179
|
|
|
1691
|
-
const
|
|
2180
|
+
const error = expectInvalid(document);
|
|
1692
2181
|
|
|
1693
|
-
expect(
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2182
|
+
expect(error.issues).toEqual([
|
|
2183
|
+
expect.objectContaining({
|
|
2184
|
+
code: 'context-unavailable-at-predicate-site',
|
|
2185
|
+
message: expect.stringContaining('Step gate success references context "jobs"'),
|
|
2186
|
+
path: ['jobs', 'deploy', 'steps', 0, 'gate', 'success'],
|
|
2187
|
+
details: expect.objectContaining({
|
|
2188
|
+
field: 'step.success',
|
|
2189
|
+
source: 'jobs.build.status == "succeeded"',
|
|
2190
|
+
unavailableRoots: ['jobs'],
|
|
2191
|
+
site: 'step-report',
|
|
2192
|
+
}),
|
|
2193
|
+
}),
|
|
2194
|
+
]);
|
|
1698
2195
|
});
|
|
1699
2196
|
|
|
1700
2197
|
it('parses job output mappings at execution resolution', () => {
|
|
@@ -1981,7 +2478,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1981
2478
|
});
|
|
1982
2479
|
});
|
|
1983
2480
|
|
|
1984
|
-
it('
|
|
2481
|
+
it('allows step outputs in run fields', () => {
|
|
1985
2482
|
const document: WorkflowDocument = {
|
|
1986
2483
|
name: 'untrusted step output',
|
|
1987
2484
|
jobs: {
|
|
@@ -1994,15 +2491,17 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
1994
2491
|
},
|
|
1995
2492
|
};
|
|
1996
2493
|
|
|
1997
|
-
const
|
|
2494
|
+
const model = normalizeWorkflowDocument(document);
|
|
1998
2495
|
|
|
1999
|
-
expect(
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2496
|
+
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
2497
|
+
kind: 'run',
|
|
2498
|
+
templates: {
|
|
2499
|
+
command: [
|
|
2500
|
+
{kind: 'literal', value: 'echo '},
|
|
2501
|
+
{kind: 'deferred', roots: ['steps']},
|
|
2502
|
+
],
|
|
2503
|
+
},
|
|
2504
|
+
});
|
|
2006
2505
|
});
|
|
2007
2506
|
|
|
2008
2507
|
it('infers typed job output scalars for downstream job overlays', () => {
|
|
@@ -2119,9 +2618,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2119
2618
|
});
|
|
2120
2619
|
});
|
|
2121
2620
|
|
|
2122
|
-
it('
|
|
2621
|
+
it('infers structured typed job output mappings', () => {
|
|
2123
2622
|
const document: WorkflowDocument = {
|
|
2124
|
-
name: '
|
|
2623
|
+
name: 'structured job output type',
|
|
2125
2624
|
jobs: {
|
|
2126
2625
|
build: {
|
|
2127
2626
|
steps: [
|
|
@@ -2132,10 +2631,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2132
2631
|
metadata: {
|
|
2133
2632
|
type: 'json',
|
|
2134
2633
|
schema: {
|
|
2135
|
-
type: '
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2634
|
+
type: 'array',
|
|
2635
|
+
items: {
|
|
2636
|
+
type: 'object',
|
|
2637
|
+
additionalProperties: false,
|
|
2638
|
+
required: ['severity'],
|
|
2639
|
+
properties: {severity: {type: 'string'}},
|
|
2640
|
+
},
|
|
2139
2641
|
},
|
|
2140
2642
|
},
|
|
2141
2643
|
},
|
|
@@ -2144,18 +2646,82 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2144
2646
|
outputs: {
|
|
2145
2647
|
metadata: interpolation('steps.collect.outputs.metadata'),
|
|
2146
2648
|
},
|
|
2147
|
-
},
|
|
2649
|
+
},
|
|
2650
|
+
},
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
const model = normalizeWorkflowDocument(document);
|
|
2654
|
+
|
|
2655
|
+
expect(model.jobs[0]?.outputTypes).toEqual({
|
|
2656
|
+
metadata: {
|
|
2657
|
+
kind: 'list',
|
|
2658
|
+
element: {
|
|
2659
|
+
kind: 'object',
|
|
2660
|
+
fields: {severity: 'string'},
|
|
2661
|
+
},
|
|
2662
|
+
},
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
it('authorizes indexed reads from structured job outputs', () => {
|
|
2667
|
+
const document: WorkflowDocument = {
|
|
2668
|
+
name: 'indexed structured job output',
|
|
2669
|
+
jobs: {
|
|
2670
|
+
review: {
|
|
2671
|
+
steps: [
|
|
2672
|
+
{
|
|
2673
|
+
key: 'inspect',
|
|
2674
|
+
run: 'npm run inspect',
|
|
2675
|
+
outputs: {
|
|
2676
|
+
findings: {
|
|
2677
|
+
type: 'json',
|
|
2678
|
+
schema: {
|
|
2679
|
+
type: 'array',
|
|
2680
|
+
items: {
|
|
2681
|
+
type: 'object',
|
|
2682
|
+
additionalProperties: false,
|
|
2683
|
+
required: ['severity'],
|
|
2684
|
+
properties: {severity: {type: 'string'}},
|
|
2685
|
+
},
|
|
2686
|
+
},
|
|
2687
|
+
},
|
|
2688
|
+
},
|
|
2689
|
+
},
|
|
2690
|
+
],
|
|
2691
|
+
outputs: {
|
|
2692
|
+
findings: interpolation('steps.inspect.outputs.findings'),
|
|
2693
|
+
},
|
|
2694
|
+
},
|
|
2695
|
+
summarize: {
|
|
2696
|
+
needs: ['review'],
|
|
2697
|
+
steps: [
|
|
2698
|
+
{
|
|
2699
|
+
run: `printf '%s\\n' ${interpolation('jobs.review.outputs.findings[0].severity')}`,
|
|
2700
|
+
},
|
|
2701
|
+
],
|
|
2702
|
+
},
|
|
2703
|
+
},
|
|
2704
|
+
};
|
|
2705
|
+
|
|
2706
|
+
const model = normalizeWorkflowDocument(document);
|
|
2707
|
+
const summarize = model.jobs.find((job) => job.key === 'summarize');
|
|
2708
|
+
|
|
2709
|
+
expect(summarize?.steps[0]).toMatchObject({
|
|
2710
|
+
kind: 'run',
|
|
2711
|
+
templates: {
|
|
2712
|
+
command: [
|
|
2713
|
+
{kind: 'literal', value: "printf '%s\\n' "},
|
|
2714
|
+
{
|
|
2715
|
+
kind: 'deferred',
|
|
2716
|
+
expression: {
|
|
2717
|
+
source: 'jobs.review.outputs.findings[0].severity',
|
|
2718
|
+
check: 'typed',
|
|
2719
|
+
},
|
|
2720
|
+
roots: ['jobs'],
|
|
2721
|
+
},
|
|
2722
|
+
],
|
|
2148
2723
|
},
|
|
2149
|
-
};
|
|
2150
|
-
|
|
2151
|
-
const error = expectInvalid(document);
|
|
2152
|
-
|
|
2153
|
-
expect(error.issues).toEqual([
|
|
2154
|
-
expect.objectContaining({
|
|
2155
|
-
code: 'invalid-job-output',
|
|
2156
|
-
path: ['jobs', 'build', 'outputs', 'metadata'],
|
|
2157
|
-
}),
|
|
2158
|
-
]);
|
|
2724
|
+
});
|
|
2159
2725
|
});
|
|
2160
2726
|
|
|
2161
2727
|
it('rejects job output references without a direct needs edge', () => {
|
|
@@ -2318,29 +2884,29 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2318
2884
|
);
|
|
2319
2885
|
});
|
|
2320
2886
|
|
|
2321
|
-
it('
|
|
2887
|
+
it('accepts vars in job and step if predicates', () => {
|
|
2322
2888
|
const document: WorkflowDocument = {
|
|
2323
2889
|
name: 'vars condition',
|
|
2324
2890
|
jobs: {
|
|
2325
2891
|
build: {
|
|
2326
2892
|
if: interpolation('vars.REQUIRED == "true"'),
|
|
2327
|
-
steps: [{run: 'npm run build'}],
|
|
2893
|
+
steps: [{if: interpolation('vars.REQUIRED == "true"'), run: 'npm run build'}],
|
|
2328
2894
|
},
|
|
2329
2895
|
},
|
|
2330
2896
|
};
|
|
2331
2897
|
|
|
2332
|
-
const
|
|
2898
|
+
const model = normalizeWorkflowDocument(document);
|
|
2333
2899
|
|
|
2334
|
-
expect(
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2900
|
+
expect(model.jobs[0]?.if).toMatchObject({
|
|
2901
|
+
source: 'vars.REQUIRED == "true"',
|
|
2902
|
+
check: 'typed',
|
|
2903
|
+
resultType: 'bool',
|
|
2904
|
+
});
|
|
2905
|
+
expect(model.jobs[0]?.steps[0]?.if).toMatchObject({
|
|
2906
|
+
source: 'vars.REQUIRED == "true"',
|
|
2907
|
+
check: 'typed',
|
|
2908
|
+
resultType: 'bool',
|
|
2909
|
+
});
|
|
2344
2910
|
});
|
|
2345
2911
|
|
|
2346
2912
|
it('rejects if job references without a direct needs edge', () => {
|
|
@@ -2442,7 +3008,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2442
3008
|
path: ['jobs', 'build', 'if'],
|
|
2443
3009
|
details: expect.objectContaining({
|
|
2444
3010
|
field: 'job.if',
|
|
2445
|
-
unavailableRoots: ['execution
|
|
3011
|
+
unavailableRoots: ['execution'],
|
|
2446
3012
|
}),
|
|
2447
3013
|
}),
|
|
2448
3014
|
]);
|
|
@@ -2573,7 +3139,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2573
3139
|
]);
|
|
2574
3140
|
});
|
|
2575
3141
|
|
|
2576
|
-
it('
|
|
3142
|
+
it('accepts vars in job success', () => {
|
|
2577
3143
|
const document: WorkflowDocument = {
|
|
2578
3144
|
name: 'vars-context job success',
|
|
2579
3145
|
jobs: {
|
|
@@ -2584,21 +3150,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
2584
3150
|
},
|
|
2585
3151
|
};
|
|
2586
3152
|
|
|
2587
|
-
const
|
|
3153
|
+
const model = normalizeWorkflowDocument(document);
|
|
2588
3154
|
|
|
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
|
-
]);
|
|
3155
|
+
expect(model.jobs[0]?.success).toBe('vars.ENVIRONMENT == "prod"');
|
|
2602
3156
|
});
|
|
2603
3157
|
|
|
2604
3158
|
it('reports malformed job execution timeouts', () => {
|
|
@@ -3111,12 +3665,12 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3111
3665
|
[
|
|
3112
3666
|
'vars root',
|
|
3113
3667
|
'vars.environment == "prod"',
|
|
3114
|
-
'
|
|
3668
|
+
'context-unavailable-at-predicate-site',
|
|
3115
3669
|
{
|
|
3116
3670
|
field: 'trigger.filter',
|
|
3117
3671
|
source: 'vars.environment == "prod"',
|
|
3118
3672
|
contextRoots: ['vars'],
|
|
3119
|
-
|
|
3673
|
+
unavailableRoots: ['vars'],
|
|
3120
3674
|
site: 'ingest',
|
|
3121
3675
|
},
|
|
3122
3676
|
],
|
|
@@ -3551,11 +4105,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3551
4105
|
steps: [
|
|
3552
4106
|
{
|
|
3553
4107
|
name: `deploy ${interpolation('event.action')}`,
|
|
4108
|
+
working_directory: 'packages/api',
|
|
3554
4109
|
run: `deploy ${interpolation('run.id')}`,
|
|
3555
4110
|
env: {PR_TITLE: interpolation('event.pull_request.title'), DEBUG: false},
|
|
3556
4111
|
},
|
|
3557
4112
|
{
|
|
3558
4113
|
name: `review ${interpolation('inputs.topic')}`,
|
|
4114
|
+
working_directory: `packages/${interpolation('inputs.topic')}`,
|
|
3559
4115
|
provider: 'openai',
|
|
3560
4116
|
prompt: `Review ${interpolation('event.pull_request.title')}`,
|
|
3561
4117
|
},
|
|
@@ -3602,6 +4158,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3602
4158
|
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
3603
4159
|
kind: 'run',
|
|
3604
4160
|
command: {value: `deploy ${interpolation('run.id')}`},
|
|
4161
|
+
workingDirectory: 'packages/api',
|
|
3605
4162
|
templates: {
|
|
3606
4163
|
command: [
|
|
3607
4164
|
{kind: 'literal', value: 'deploy '},
|
|
@@ -3636,7 +4193,16 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3636
4193
|
});
|
|
3637
4194
|
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
3638
4195
|
kind: 'agent',
|
|
4196
|
+
workingDirectory: `packages/${interpolation('inputs.topic')}`,
|
|
3639
4197
|
templates: {
|
|
4198
|
+
workingDirectory: [
|
|
4199
|
+
{kind: 'literal', value: 'packages/'},
|
|
4200
|
+
{
|
|
4201
|
+
kind: 'deferred',
|
|
4202
|
+
expression: {source: 'inputs.topic'},
|
|
4203
|
+
roots: ['inputs'],
|
|
4204
|
+
},
|
|
4205
|
+
],
|
|
3640
4206
|
prompt: [
|
|
3641
4207
|
{kind: 'literal', value: 'Review '},
|
|
3642
4208
|
{
|
|
@@ -3678,7 +4244,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3678
4244
|
expect(model).not.toHaveProperty('templates');
|
|
3679
4245
|
expect(model.jobs[0]?.steps[0]).not.toHaveProperty('templates');
|
|
3680
4246
|
expect(model.env).toEqual({VALUE: '$${{ event.ref }}'});
|
|
3681
|
-
expect(model.jobs[0]?.name).
|
|
4247
|
+
expect(model.jobs[0]?.name).toBe('Build app');
|
|
3682
4248
|
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
3683
4249
|
kind: 'run',
|
|
3684
4250
|
command: {value: 'echo $${{ event.ref }}'},
|
|
@@ -3737,30 +4303,32 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3737
4303
|
|
|
3738
4304
|
it.each([
|
|
3739
4305
|
['event payload', 'event.pull_request.title', ['event']],
|
|
3740
|
-
['job
|
|
3741
|
-
] as const)('
|
|
4306
|
+
['job status', 'jobs.build.status', ['jobs']],
|
|
4307
|
+
] as const)('allows %s context in run commands', (_label, source, roots) => {
|
|
3742
4308
|
const document: WorkflowDocument = {
|
|
3743
4309
|
name: 'unsafe run',
|
|
3744
4310
|
jobs: {
|
|
3745
4311
|
build: {
|
|
4312
|
+
steps: [{run: 'echo build'}],
|
|
4313
|
+
},
|
|
4314
|
+
deploy: {
|
|
4315
|
+
needs: 'build',
|
|
3746
4316
|
steps: [{run: `echo ${interpolation(source)}`}],
|
|
3747
4317
|
},
|
|
3748
4318
|
},
|
|
3749
4319
|
};
|
|
3750
4320
|
|
|
3751
|
-
const
|
|
4321
|
+
const model = normalizeWorkflowDocument(document);
|
|
3752
4322
|
|
|
3753
|
-
expect(
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
}),
|
|
3763
|
-
]);
|
|
4323
|
+
expect(model.jobs[1]?.steps[0]).toMatchObject({
|
|
4324
|
+
kind: 'run',
|
|
4325
|
+
templates: {
|
|
4326
|
+
command: [
|
|
4327
|
+
{kind: 'literal', value: 'echo '},
|
|
4328
|
+
{kind: 'deferred', roots},
|
|
4329
|
+
],
|
|
4330
|
+
},
|
|
4331
|
+
});
|
|
3764
4332
|
});
|
|
3765
4333
|
|
|
3766
4334
|
it('rejects secrets in agent fields', () => {
|
|
@@ -3832,8 +4400,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3832
4400
|
it.each([
|
|
3833
4401
|
'execution.events[0].data.body',
|
|
3834
4402
|
'execution["events"][0].data.body',
|
|
3835
|
-
|
|
3836
|
-
])('rejects untrusted execution sub-paths in run commands: %s', (source) => {
|
|
4403
|
+
])('allows execution event sub-paths in run commands: %s', (source) => {
|
|
3837
4404
|
const document: WorkflowDocument = {
|
|
3838
4405
|
name: 'unsafe execution run',
|
|
3839
4406
|
jobs: {
|
|
@@ -3843,42 +4410,17 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3843
4410
|
},
|
|
3844
4411
|
};
|
|
3845
4412
|
|
|
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
|
-
});
|
|
4413
|
+
const model = normalizeWorkflowDocument(document);
|
|
3859
4414
|
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
4415
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4416
|
+
kind: 'run',
|
|
4417
|
+
templates: {
|
|
4418
|
+
command: [
|
|
4419
|
+
{kind: 'literal', value: 'echo '},
|
|
4420
|
+
{kind: 'deferred', roots: ['execution']},
|
|
4421
|
+
],
|
|
3867
4422
|
},
|
|
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
|
-
]);
|
|
4423
|
+
});
|
|
3882
4424
|
});
|
|
3883
4425
|
|
|
3884
4426
|
it.each([
|
|
@@ -3994,7 +4536,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
3994
4536
|
});
|
|
3995
4537
|
});
|
|
3996
4538
|
|
|
3997
|
-
it('
|
|
4539
|
+
it('rejects dynamic job names in favor of execution_name', () => {
|
|
3998
4540
|
const document: WorkflowDocument = {
|
|
3999
4541
|
name: 'job display context',
|
|
4000
4542
|
jobs: {
|
|
@@ -4010,16 +4552,19 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4010
4552
|
},
|
|
4011
4553
|
};
|
|
4012
4554
|
|
|
4013
|
-
const
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4555
|
+
const error = expectInvalid(document);
|
|
4556
|
+
expect(error.issues).toEqual([
|
|
4557
|
+
expect.objectContaining({
|
|
4558
|
+
code: 'invalid-interpolation-template',
|
|
4559
|
+
message: 'Job name must be literal. Move runtime interpolation to execution_name.',
|
|
4560
|
+
path: ['jobs', 'build', 'name'],
|
|
4561
|
+
}),
|
|
4562
|
+
expect.objectContaining({
|
|
4563
|
+
code: 'invalid-interpolation-template',
|
|
4564
|
+
message: 'Job name must be literal. Move runtime interpolation to execution_name.',
|
|
4565
|
+
path: ['jobs', 'review', 'name'],
|
|
4566
|
+
}),
|
|
4567
|
+
]);
|
|
4023
4568
|
});
|
|
4024
4569
|
|
|
4025
4570
|
it('does not apply availability checks to workflow-level or job-level env', () => {
|
|
@@ -4149,7 +4694,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4149
4694
|
});
|
|
4150
4695
|
});
|
|
4151
4696
|
|
|
4152
|
-
it('
|
|
4697
|
+
it('rejects execution events in static job names', () => {
|
|
4153
4698
|
const document: WorkflowDocument = {
|
|
4154
4699
|
name: 'execution events allowed',
|
|
4155
4700
|
jobs: {
|
|
@@ -4161,25 +4706,13 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4161
4706
|
},
|
|
4162
4707
|
};
|
|
4163
4708
|
|
|
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
|
-
});
|
|
4709
|
+
const error = expectInvalid(document);
|
|
4710
|
+
expect(error.issues).toEqual([
|
|
4711
|
+
expect.objectContaining({
|
|
4712
|
+
code: 'invalid-interpolation-template',
|
|
4713
|
+
path: ['jobs', 'build', 'name'],
|
|
4714
|
+
}),
|
|
4715
|
+
]);
|
|
4183
4716
|
});
|
|
4184
4717
|
|
|
4185
4718
|
it('allows untrusted context in env, prompt, and step names', () => {
|
|
@@ -4216,9 +4749,9 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4216
4749
|
it.each([
|
|
4217
4750
|
['model', {model: interpolation('event.model'), prompt: 'Fix it.'}, 'event'],
|
|
4218
4751
|
['provider', {provider: interpolation('inputs.provider'), prompt: 'Fix it.'}, 'inputs'],
|
|
4219
|
-
] as const)('
|
|
4752
|
+
] as const)('allows external context in agent %s interpolation', (field, step, root) => {
|
|
4220
4753
|
const document: WorkflowDocument = {
|
|
4221
|
-
name: '
|
|
4754
|
+
name: 'external agent field',
|
|
4222
4755
|
jobs: {
|
|
4223
4756
|
fix: {
|
|
4224
4757
|
steps: [step],
|
|
@@ -4226,14 +4759,77 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4226
4759
|
},
|
|
4227
4760
|
};
|
|
4228
4761
|
|
|
4229
|
-
const
|
|
4762
|
+
const model = normalizeWorkflowDocument(document);
|
|
4230
4763
|
|
|
4231
|
-
expect(
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
}
|
|
4236
|
-
|
|
4764
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4765
|
+
kind: 'agent',
|
|
4766
|
+
templates: {
|
|
4767
|
+
[field]: [{kind: 'deferred', roots: [root]}],
|
|
4768
|
+
},
|
|
4769
|
+
});
|
|
4770
|
+
});
|
|
4771
|
+
|
|
4772
|
+
it.each([
|
|
4773
|
+
['model', {model: interpolation('steps.collect.outputs.value'), prompt: 'Fix it.'}, 'steps'],
|
|
4774
|
+
[
|
|
4775
|
+
'provider',
|
|
4776
|
+
{provider: interpolation('steps.collect.outputs.value'), prompt: 'Fix it.'},
|
|
4777
|
+
'steps',
|
|
4778
|
+
],
|
|
4779
|
+
[
|
|
4780
|
+
'model',
|
|
4781
|
+
{
|
|
4782
|
+
model: interpolation('steps.filter(s, true).map(s, s.outputs.value)[0]'),
|
|
4783
|
+
prompt: 'Fix it.',
|
|
4784
|
+
},
|
|
4785
|
+
'steps',
|
|
4786
|
+
],
|
|
4787
|
+
[
|
|
4788
|
+
'provider',
|
|
4789
|
+
{
|
|
4790
|
+
provider: interpolation('steps.filter(s, true).map(s, s.outputs.value)[0]'),
|
|
4791
|
+
prompt: 'Fix it.',
|
|
4792
|
+
},
|
|
4793
|
+
'steps',
|
|
4794
|
+
],
|
|
4795
|
+
[
|
|
4796
|
+
'model',
|
|
4797
|
+
{model: interpolation('execution.events[0].data.body'), prompt: 'Fix it.'},
|
|
4798
|
+
'execution',
|
|
4799
|
+
],
|
|
4800
|
+
[
|
|
4801
|
+
'provider',
|
|
4802
|
+
{provider: interpolation('execution.events[0].data.body'), prompt: 'Fix it.'},
|
|
4803
|
+
'execution',
|
|
4804
|
+
],
|
|
4805
|
+
[
|
|
4806
|
+
'model',
|
|
4807
|
+
{model: interpolation('execution["events"][0].data.body'), prompt: 'Fix it.'},
|
|
4808
|
+
'execution',
|
|
4809
|
+
],
|
|
4810
|
+
[
|
|
4811
|
+
'provider',
|
|
4812
|
+
{provider: interpolation('execution["events"][0].data.body'), prompt: 'Fix it.'},
|
|
4813
|
+
'execution',
|
|
4814
|
+
],
|
|
4815
|
+
] as const)('allows path-specific external context in agent %s interpolation', (field, step, root) => {
|
|
4816
|
+
const document: WorkflowDocument = {
|
|
4817
|
+
name: 'path-specific external agent field',
|
|
4818
|
+
jobs: {
|
|
4819
|
+
fix: {
|
|
4820
|
+
steps: [{key: 'collect', run: 'collect'}, step],
|
|
4821
|
+
},
|
|
4822
|
+
},
|
|
4823
|
+
};
|
|
4824
|
+
|
|
4825
|
+
const model = normalizeWorkflowDocument(document);
|
|
4826
|
+
|
|
4827
|
+
expect(model.jobs[0]?.steps[1]).toMatchObject({
|
|
4828
|
+
kind: 'agent',
|
|
4829
|
+
templates: {
|
|
4830
|
+
[field]: [{kind: 'deferred', roots: [root]}],
|
|
4831
|
+
},
|
|
4832
|
+
});
|
|
4237
4833
|
});
|
|
4238
4834
|
|
|
4239
4835
|
it.each([
|
|
@@ -4298,6 +4894,77 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4298
4894
|
});
|
|
4299
4895
|
});
|
|
4300
4896
|
|
|
4897
|
+
it('does not add provider catalog issues for invalid provider interpolation', () => {
|
|
4898
|
+
const document: WorkflowDocument = {
|
|
4899
|
+
name: 'invalid provider interpolation',
|
|
4900
|
+
jobs: {
|
|
4901
|
+
fix: {
|
|
4902
|
+
steps: [{harness: 'claude', provider: interpolation('foo.bar'), prompt: 'Fix it.'}],
|
|
4903
|
+
},
|
|
4904
|
+
},
|
|
4905
|
+
};
|
|
4906
|
+
|
|
4907
|
+
const error = expectInvalid(document);
|
|
4908
|
+
|
|
4909
|
+
expect(error.issues).toEqual([
|
|
4910
|
+
expect.objectContaining({
|
|
4911
|
+
code: 'unknown-interpolation-context',
|
|
4912
|
+
path: ['jobs', 'fix', 'steps', 0, 'provider'],
|
|
4913
|
+
}),
|
|
4914
|
+
]);
|
|
4915
|
+
});
|
|
4916
|
+
|
|
4917
|
+
it('skips static model catalog validation when model is interpolated', () => {
|
|
4918
|
+
const document: WorkflowDocument = {
|
|
4919
|
+
name: 'templated model',
|
|
4920
|
+
jobs: {
|
|
4921
|
+
fix: {
|
|
4922
|
+
steps: [
|
|
4923
|
+
{
|
|
4924
|
+
provider: 'anthropic',
|
|
4925
|
+
model: interpolation('run.name'),
|
|
4926
|
+
prompt: 'Fix it.',
|
|
4927
|
+
},
|
|
4928
|
+
],
|
|
4929
|
+
},
|
|
4930
|
+
},
|
|
4931
|
+
};
|
|
4932
|
+
|
|
4933
|
+
const model = normalizeWorkflowDocument(document);
|
|
4934
|
+
|
|
4935
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
4936
|
+
kind: 'agent',
|
|
4937
|
+
templates: {model: [{kind: 'deferred', roots: ['run']}]},
|
|
4938
|
+
});
|
|
4939
|
+
});
|
|
4940
|
+
|
|
4941
|
+
it('does not add model catalog issues for invalid model interpolation', () => {
|
|
4942
|
+
const document: WorkflowDocument = {
|
|
4943
|
+
name: 'invalid model interpolation',
|
|
4944
|
+
jobs: {
|
|
4945
|
+
fix: {
|
|
4946
|
+
steps: [
|
|
4947
|
+
{
|
|
4948
|
+
harness: 'pi',
|
|
4949
|
+
provider: 'anthropic',
|
|
4950
|
+
model: interpolation('foo.bar'),
|
|
4951
|
+
prompt: 'Fix it.',
|
|
4952
|
+
},
|
|
4953
|
+
],
|
|
4954
|
+
},
|
|
4955
|
+
},
|
|
4956
|
+
};
|
|
4957
|
+
|
|
4958
|
+
const error = expectInvalid(document);
|
|
4959
|
+
|
|
4960
|
+
expect(error.issues).toEqual([
|
|
4961
|
+
expect.objectContaining({
|
|
4962
|
+
code: 'unknown-interpolation-context',
|
|
4963
|
+
path: ['jobs', 'fix', 'steps', 0, 'model'],
|
|
4964
|
+
}),
|
|
4965
|
+
]);
|
|
4966
|
+
});
|
|
4967
|
+
|
|
4301
4968
|
it('still validates literal providers through the catalog', () => {
|
|
4302
4969
|
const document: WorkflowDocument = {
|
|
4303
4970
|
name: 'literal provider',
|
|
@@ -4427,7 +5094,7 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4427
5094
|
]);
|
|
4428
5095
|
});
|
|
4429
5096
|
|
|
4430
|
-
it('uses syntax mode for mixed open contexts
|
|
5097
|
+
it('uses syntax mode for mixed open contexts without a trust restriction', () => {
|
|
4431
5098
|
const envDocument: WorkflowDocument = {
|
|
4432
5099
|
name: 'mixed env',
|
|
4433
5100
|
env: {MIXED: interpolation('run.nope + event.x')},
|
|
@@ -4447,22 +5114,25 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4447
5114
|
};
|
|
4448
5115
|
|
|
4449
5116
|
const model = normalizeWorkflowDocument(envDocument);
|
|
4450
|
-
const
|
|
5117
|
+
const runModel = normalizeWorkflowDocument(runDocument);
|
|
4451
5118
|
|
|
4452
5119
|
expect(model.templates?.env?.MIXED?.[0]).toMatchObject({
|
|
4453
5120
|
kind: 'deferred',
|
|
4454
5121
|
expression: {check: 'syntax'},
|
|
4455
5122
|
roots: expect.arrayContaining(['run', 'event']),
|
|
4456
5123
|
});
|
|
4457
|
-
expect(
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
5124
|
+
expect(runModel.jobs[0]?.steps[0]).toMatchObject({
|
|
5125
|
+
kind: 'run',
|
|
5126
|
+
templates: {
|
|
5127
|
+
command: [
|
|
5128
|
+
{kind: 'literal', value: 'echo '},
|
|
5129
|
+
{kind: 'deferred', roots: expect.arrayContaining(['run', 'event'])},
|
|
5130
|
+
],
|
|
5131
|
+
},
|
|
5132
|
+
});
|
|
4463
5133
|
});
|
|
4464
5134
|
|
|
4465
|
-
it('
|
|
5135
|
+
it('allows multi-segment run fields with external context', () => {
|
|
4466
5136
|
const document: WorkflowDocument = {
|
|
4467
5137
|
name: 'multi segment run',
|
|
4468
5138
|
jobs: {
|
|
@@ -4472,14 +5142,18 @@ describe('normalizeWorkflowDocument', () => {
|
|
|
4472
5142
|
},
|
|
4473
5143
|
};
|
|
4474
5144
|
|
|
4475
|
-
const
|
|
5145
|
+
const model = normalizeWorkflowDocument(document);
|
|
4476
5146
|
|
|
4477
|
-
expect(
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
5147
|
+
expect(model.jobs[0]?.steps[0]).toMatchObject({
|
|
5148
|
+
kind: 'run',
|
|
5149
|
+
templates: {
|
|
5150
|
+
command: [
|
|
5151
|
+
{kind: 'deferred', roots: ['run']},
|
|
5152
|
+
{kind: 'literal', value: '-'},
|
|
5153
|
+
{kind: 'deferred', roots: ['event']},
|
|
5154
|
+
],
|
|
5155
|
+
},
|
|
5156
|
+
});
|
|
4483
5157
|
});
|
|
4484
5158
|
|
|
4485
5159
|
it('does not parse templates in non-string env values', () => {
|