@shipfox/api-workflows 16.0.0 → 17.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 +4 -4
- package/CHANGELOG.md +47 -0
- package/dist/core/agent-tools.js +8 -3
- package/dist/core/agent-tools.js.map +1 -1
- package/dist/core/entities/step.d.ts +1 -1
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +2 -1
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/step-config/materialize-job-execution-steps.js +2 -0
- package/dist/core/step-config/materialize-job-execution-steps.js.map +1 -1
- package/dist/core/step-config/resolve-step-config.js +3 -1
- package/dist/core/step-config/resolve-step-config.js.map +1 -1
- package/dist/db/db.d.ts +8 -8
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/steps.d.ts +2 -2
- package/dist/db/schema/workflow-run-attempts.d.ts +2 -2
- package/dist/db/workflow-runs/outbox.d.ts +1 -0
- package/dist/db/workflow-runs/outbox.d.ts.map +1 -1
- package/dist/db/workflow-runs/outbox.js +2 -1
- package/dist/db/workflow-runs/outbox.js.map +1 -1
- package/dist/db/workflow-runs/run-create.d.ts.map +1 -1
- package/dist/db/workflow-runs/run-create.js +49 -1
- package/dist/db/workflow-runs/run-create.js.map +1 -1
- package/dist/db/workflow-runs/steps.d.ts +6 -0
- package/dist/db/workflow-runs/steps.d.ts.map +1 -1
- package/dist/db/workflow-runs/steps.js +14 -0
- package/dist/db/workflow-runs/steps.js.map +1 -1
- package/dist/db/workflow-runs.d.ts +1 -1
- package/dist/db/workflow-runs.d.ts.map +1 -1
- package/dist/db/workflow-runs.js +1 -1
- package/dist/db/workflow-runs.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +85 -19
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +11 -11
- package/src/core/agent-tools.test.ts +85 -0
- package/src/core/agent-tools.ts +11 -3
- package/src/core/entities/step.ts +1 -1
- package/src/core/errors.ts +2 -0
- package/src/core/job-execution.test.ts +1 -0
- package/src/core/run-workflow.test.ts +4 -0
- package/src/core/step-config/materialize-job-execution-steps.ts +2 -0
- package/src/core/step-config/resolve-step-config.ts +4 -1
- package/src/db/index.ts +1 -0
- package/src/db/workflow-runs/outbox.ts +2 -0
- package/src/db/workflow-runs/run-create.test.ts +41 -0
- package/src/db/workflow-runs/run-create.ts +81 -3
- package/src/db/workflow-runs/steps.ts +19 -0
- package/src/db/workflow-runs.ts +1 -0
- package/src/presentation/inter-module.test.ts +223 -0
- package/src/presentation/inter-module.ts +106 -24
- package/src/presentation/routes/agent-runtime-config.test.ts +2 -0
- package/test/factories/workflow-model.ts +74 -2
- package/test/fixtures/agent-inter-module.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
const mocks = vi.hoisted(() => ({
|
|
22
22
|
deliverEventToListener: vi.fn(),
|
|
23
23
|
getJobScope: vi.fn(),
|
|
24
|
+
getStepAttemptDetail: vi.fn(),
|
|
24
25
|
getStepById: vi.fn(),
|
|
25
26
|
getStepByIdForJobExecution: vi.fn(),
|
|
27
|
+
listStepAttemptIdsByJobId: vi.fn(),
|
|
26
28
|
}));
|
|
27
29
|
|
|
28
30
|
vi.mock('#db/index.js', () => mocks);
|
|
@@ -47,12 +49,38 @@ const input = {
|
|
|
47
49
|
describe('Workflows inter-module presentation', () => {
|
|
48
50
|
beforeEach(() => {
|
|
49
51
|
mocks.getJobScope.mockReset();
|
|
52
|
+
mocks.listStepAttemptIdsByJobId.mockReset();
|
|
53
|
+
mocks.getStepAttemptDetail.mockReset();
|
|
50
54
|
mocks.getStepById.mockReset();
|
|
51
55
|
mocks.getStepByIdForJobExecution.mockReset();
|
|
52
56
|
mocks.deliverEventToListener.mockReset();
|
|
53
57
|
mocks.deliverEventToListener.mockResolvedValue({buffered: true, skipped: false});
|
|
54
58
|
});
|
|
55
59
|
|
|
60
|
+
it('returns the step attempt ids of a job for the session release sweep', async () => {
|
|
61
|
+
const stepAttemptId = '00000000-0000-4000-8000-000000000010';
|
|
62
|
+
mocks.listStepAttemptIdsByJobId.mockResolvedValue([stepAttemptId]);
|
|
63
|
+
const presentation = createWorkflowsInterModulePresentation({
|
|
64
|
+
agent: {} as never,
|
|
65
|
+
definitions: {} as never,
|
|
66
|
+
integrations: {} as never,
|
|
67
|
+
projects: {} as never,
|
|
68
|
+
runners: {} as never,
|
|
69
|
+
secrets: {} as never,
|
|
70
|
+
workspaces: {getWorkspaceOperatingState: vi.fn()} as never,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await presentation.handlers.listJobStepAttempts(
|
|
74
|
+
{jobId: '00000000-0000-4000-8000-000000000006'},
|
|
75
|
+
{signal: new AbortController().signal},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(mocks.listStepAttemptIdsByJobId).toHaveBeenCalledWith(
|
|
79
|
+
'00000000-0000-4000-8000-000000000006',
|
|
80
|
+
);
|
|
81
|
+
expect(result).toEqual({stepAttemptIds: [stepAttemptId]});
|
|
82
|
+
});
|
|
83
|
+
|
|
56
84
|
it('returns only the resolved harness for Logs', async () => {
|
|
57
85
|
mocks.getStepById.mockResolvedValue({config: {harness: 'claude'}});
|
|
58
86
|
const presentation = createWorkflowsInterModulePresentation({
|
|
@@ -136,6 +164,201 @@ describe('Workflows inter-module presentation', () => {
|
|
|
136
164
|
});
|
|
137
165
|
});
|
|
138
166
|
|
|
167
|
+
describe('getLeasedAgentSessionContext', () => {
|
|
168
|
+
const input = {
|
|
169
|
+
jobId: '00000000-0000-4000-8000-000000000006',
|
|
170
|
+
jobExecutionId: '00000000-0000-4000-8000-000000000007',
|
|
171
|
+
runnerSessionId: '00000000-0000-4000-8000-000000000008',
|
|
172
|
+
stepId: '00000000-0000-4000-8000-000000000009',
|
|
173
|
+
attempt: 1,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const method = workflowsInterModuleContract.methods.getLeasedAgentSessionContext;
|
|
177
|
+
|
|
178
|
+
function presentation(runners: {getLeaseState: ReturnType<typeof vi.fn>}) {
|
|
179
|
+
return createWorkflowsInterModulePresentation({
|
|
180
|
+
agent: {} as never,
|
|
181
|
+
definitions: {} as never,
|
|
182
|
+
integrations: {} as never,
|
|
183
|
+
projects: {} as never,
|
|
184
|
+
runners: runners as never,
|
|
185
|
+
secrets: {} as never,
|
|
186
|
+
workspaces: {getWorkspaceOperatingState: vi.fn()} as never,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function expectKnownError(call: Promise<unknown> | unknown, code: string): Promise<void> {
|
|
191
|
+
try {
|
|
192
|
+
await call;
|
|
193
|
+
throw new Error(`Expected contract error ${code}, but the call succeeded`);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
expect(isInterModuleKnownError(method, error) && (error as {code: string}).code).toBe(code);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function arrangeRunningAgentStep() {
|
|
200
|
+
mocks.getStepByIdForJobExecution.mockResolvedValue({
|
|
201
|
+
currentAttempt: input.attempt,
|
|
202
|
+
status: 'running',
|
|
203
|
+
type: 'agent',
|
|
204
|
+
config: {},
|
|
205
|
+
});
|
|
206
|
+
mocks.getJobScope.mockResolvedValue({
|
|
207
|
+
workspaceId: '00000000-0000-4000-8000-000000000010',
|
|
208
|
+
projectId: '00000000-0000-4000-8000-000000000011',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
it('returns the recorded session descriptor with the resolved scope', async () => {
|
|
213
|
+
const stepAttemptId = '00000000-0000-4000-8000-000000000012';
|
|
214
|
+
const sessionId = '00000000-0000-4000-8000-000000000013';
|
|
215
|
+
arrangeRunningAgentStep();
|
|
216
|
+
mocks.getStepAttemptDetail.mockResolvedValue({
|
|
217
|
+
workflowRunId: '00000000-0000-4000-8000-000000000014',
|
|
218
|
+
workflowRunAttemptId: '00000000-0000-4000-8000-000000000015',
|
|
219
|
+
step: {},
|
|
220
|
+
attempt: {
|
|
221
|
+
id: stepAttemptId,
|
|
222
|
+
config: {
|
|
223
|
+
session: {id: sessionId, key: 'main', mode: 'resume', segment: 3},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const runners = {getLeaseState: vi.fn().mockResolvedValue({active: true})};
|
|
228
|
+
|
|
229
|
+
const result = await presentation(runners).handlers.getLeasedAgentSessionContext(input, {
|
|
230
|
+
signal: new AbortController().signal,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(result).toEqual({
|
|
234
|
+
workspaceId: '00000000-0000-4000-8000-000000000010',
|
|
235
|
+
projectId: '00000000-0000-4000-8000-000000000011',
|
|
236
|
+
workflowRunAttemptId: '00000000-0000-4000-8000-000000000015',
|
|
237
|
+
stepAttemptId,
|
|
238
|
+
session: {id: sessionId, key: 'main', mode: 'resume', segment: 3},
|
|
239
|
+
});
|
|
240
|
+
expect(mocks.getStepAttemptDetail).toHaveBeenCalledWith({
|
|
241
|
+
stepId: input.stepId,
|
|
242
|
+
attempt: input.attempt,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('returns a null session when the step has no recorded descriptor', async () => {
|
|
247
|
+
arrangeRunningAgentStep();
|
|
248
|
+
mocks.getStepAttemptDetail.mockResolvedValue({
|
|
249
|
+
workflowRunId: '00000000-0000-4000-8000-000000000014',
|
|
250
|
+
workflowRunAttemptId: '00000000-0000-4000-8000-000000000015',
|
|
251
|
+
step: {},
|
|
252
|
+
attempt: {id: '00000000-0000-4000-8000-000000000012', config: null},
|
|
253
|
+
});
|
|
254
|
+
const runners = {getLeaseState: vi.fn().mockResolvedValue({active: true})};
|
|
255
|
+
|
|
256
|
+
const result = await presentation(runners).handlers.getLeasedAgentSessionContext(input, {
|
|
257
|
+
signal: new AbortController().signal,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(result.session).toBeNull();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('fails fast when the lease is not active', async () => {
|
|
264
|
+
arrangeRunningAgentStep();
|
|
265
|
+
const runners = {getLeaseState: vi.fn().mockResolvedValue({active: false})};
|
|
266
|
+
|
|
267
|
+
await expectKnownError(
|
|
268
|
+
presentation(runners).handlers.getLeasedAgentSessionContext(input, {
|
|
269
|
+
signal: new AbortController().signal,
|
|
270
|
+
}),
|
|
271
|
+
'lease-not-active',
|
|
272
|
+
);
|
|
273
|
+
expect(mocks.getStepAttemptDetail).not.toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('rejects a malformed recorded descriptor', async () => {
|
|
277
|
+
arrangeRunningAgentStep();
|
|
278
|
+
mocks.getStepAttemptDetail.mockResolvedValue({
|
|
279
|
+
workflowRunId: '00000000-0000-4000-8000-000000000014',
|
|
280
|
+
workflowRunAttemptId: '00000000-0000-4000-8000-000000000015',
|
|
281
|
+
step: {},
|
|
282
|
+
attempt: {
|
|
283
|
+
id: '00000000-0000-4000-8000-000000000012',
|
|
284
|
+
config: {session: {id: 'not-a-uuid', key: 'main', mode: 'resume', segment: 1}},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
const runners = {getLeaseState: vi.fn().mockResolvedValue({active: true})};
|
|
288
|
+
|
|
289
|
+
await expectKnownError(
|
|
290
|
+
presentation(runners).handlers.getLeasedAgentSessionContext(input, {
|
|
291
|
+
signal: new AbortController().signal,
|
|
292
|
+
}),
|
|
293
|
+
'step-session-config-invalid',
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test.each([
|
|
298
|
+
['step-not-found', undefined],
|
|
299
|
+
['step-attempt-mismatch', {currentAttempt: 2, status: 'running', type: 'agent'}],
|
|
300
|
+
['step-not-running', {currentAttempt: 1, status: 'succeeded', type: 'agent'}],
|
|
301
|
+
['leased-step-not-agent', {currentAttempt: 1, status: 'running', type: 'run'}],
|
|
302
|
+
] as const)('maps a %s step to the published contract error', async (code, step) => {
|
|
303
|
+
const runners = {getLeaseState: vi.fn().mockResolvedValue({active: true})};
|
|
304
|
+
mocks.getStepByIdForJobExecution.mockResolvedValue(step);
|
|
305
|
+
if (step !== undefined) {
|
|
306
|
+
mocks.getJobScope.mockResolvedValue({
|
|
307
|
+
workspaceId: '00000000-0000-4000-8000-000000000010',
|
|
308
|
+
projectId: '00000000-0000-4000-8000-000000000011',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await expectKnownError(
|
|
313
|
+
presentation(runners).handlers.getLeasedAgentSessionContext(input, {
|
|
314
|
+
signal: new AbortController().signal,
|
|
315
|
+
}),
|
|
316
|
+
code,
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('maps a missing job scope to job-not-found', async () => {
|
|
321
|
+
mocks.getStepByIdForJobExecution.mockResolvedValue({
|
|
322
|
+
currentAttempt: input.attempt,
|
|
323
|
+
status: 'running',
|
|
324
|
+
type: 'agent',
|
|
325
|
+
config: {},
|
|
326
|
+
});
|
|
327
|
+
mocks.getJobScope.mockResolvedValue(undefined);
|
|
328
|
+
const runners = {getLeaseState: vi.fn().mockResolvedValue({active: true})};
|
|
329
|
+
|
|
330
|
+
await expectKnownError(
|
|
331
|
+
presentation(runners).handlers.getLeasedAgentSessionContext(input, {
|
|
332
|
+
signal: new AbortController().signal,
|
|
333
|
+
}),
|
|
334
|
+
'job-not-found',
|
|
335
|
+
);
|
|
336
|
+
expect(mocks.getStepAttemptDetail).not.toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('maps a missing step attempt detail to step-attempt-mismatch', async () => {
|
|
340
|
+
mocks.getStepByIdForJobExecution.mockResolvedValue({
|
|
341
|
+
currentAttempt: input.attempt,
|
|
342
|
+
status: 'running',
|
|
343
|
+
type: 'agent',
|
|
344
|
+
config: {},
|
|
345
|
+
});
|
|
346
|
+
mocks.getJobScope.mockResolvedValue({
|
|
347
|
+
workspaceId: '00000000-0000-4000-8000-000000000010',
|
|
348
|
+
projectId: '00000000-0000-4000-8000-000000000011',
|
|
349
|
+
});
|
|
350
|
+
mocks.getStepAttemptDetail.mockResolvedValue(undefined);
|
|
351
|
+
const runners = {getLeaseState: vi.fn().mockResolvedValue({active: true})};
|
|
352
|
+
|
|
353
|
+
await expectKnownError(
|
|
354
|
+
presentation(runners).handlers.getLeasedAgentSessionContext(input, {
|
|
355
|
+
signal: new AbortController().signal,
|
|
356
|
+
}),
|
|
357
|
+
'step-attempt-mismatch',
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
139
362
|
test.each([
|
|
140
363
|
['definition-not-found', () => new DefinitionNotFoundError(input.definitionId)],
|
|
141
364
|
['project-mismatch', () => new ProjectMismatchError(input.projectId, input.definitionId)],
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
agentSessionDescriptorSchema,
|
|
3
|
+
materializedAgentStepConfigSchema,
|
|
4
|
+
} from '@shipfox/api-agent-dto';
|
|
2
5
|
import type {AgentInterModuleClient} from '@shipfox/api-agent-dto/inter-module';
|
|
3
6
|
import type {DefinitionsInterModuleClient} from '@shipfox/api-definitions-dto/inter-module';
|
|
4
7
|
import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
type InterModulePresentation,
|
|
15
18
|
} from '@shipfox/inter-module';
|
|
16
19
|
import {DEFAULT_HARNESS, harnessSchema} from '@shipfox/workflow-document';
|
|
20
|
+
import type {Step} from '#core/entities/step.js';
|
|
17
21
|
import type {WorkflowRunTriggerReference} from '#core/entities/workflow-run.js';
|
|
18
22
|
import {
|
|
19
23
|
InvalidJobRunnerLabelsError,
|
|
@@ -32,7 +36,13 @@ import {
|
|
|
32
36
|
} from '#core/index.js';
|
|
33
37
|
import {resolveWorkflowRunTriggerReference} from '#core/resolve-trigger-reference.js';
|
|
34
38
|
import {assertWorkspaceAdmitsNewJobs} from '#core/workspace-admission.js';
|
|
35
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
getJobScope,
|
|
41
|
+
getStepAttemptDetail,
|
|
42
|
+
getStepById,
|
|
43
|
+
getStepByIdForJobExecution,
|
|
44
|
+
listStepAttemptIdsByJobId,
|
|
45
|
+
} from '#db/index.js';
|
|
36
46
|
import {deliverEventToListener} from '#db/job-listener-events.js';
|
|
37
47
|
|
|
38
48
|
type WorkspaceAdmissionKnownError = InterModuleKnownErrorFor<
|
|
@@ -48,6 +58,65 @@ export function createWorkflowsInterModulePresentation(params: {
|
|
|
48
58
|
integrations: IntegrationsModuleClient;
|
|
49
59
|
projects: ProjectsModuleClient;
|
|
50
60
|
}): InterModulePresentation<typeof workflowsInterModuleContract> {
|
|
61
|
+
/**
|
|
62
|
+
* Shared lease + running-agent-step resolution for the lease-authed
|
|
63
|
+
* inter-module methods (`getLeasedAgentToolContext` and
|
|
64
|
+
* `getLeasedAgentSessionContext`): verifies the runner lease, resolves the
|
|
65
|
+
* step for the job execution, then validates the job scope, attempt, status,
|
|
66
|
+
* and agent type in one canonical order. The two seams previously
|
|
67
|
+
* copy-pasted this chain with diverging check orders (the tool method
|
|
68
|
+
* resolved the job scope before the attempt/status checks, the session
|
|
69
|
+
* method after), so the same underlying state surfaced different errors
|
|
70
|
+
* depending on the method; a single helper keeps error codes and check
|
|
71
|
+
* ordering from drifting.
|
|
72
|
+
*
|
|
73
|
+
* `loadRunningLeasedStep` (routes) deliberately keeps its own copy: it
|
|
74
|
+
* returns the run scope (trigger reference/origin state), skips the
|
|
75
|
+
* agent-type check, and speaks HTTP `ClientError`s instead of contract
|
|
76
|
+
* known errors.
|
|
77
|
+
*/
|
|
78
|
+
async function resolveLeasedAgentStep(resolution: {
|
|
79
|
+
method:
|
|
80
|
+
| typeof workflowsInterModuleContract.methods.getLeasedAgentToolContext
|
|
81
|
+
| typeof workflowsInterModuleContract.methods.getLeasedAgentSessionContext;
|
|
82
|
+
input: {
|
|
83
|
+
jobId: string;
|
|
84
|
+
jobExecutionId: string;
|
|
85
|
+
runnerSessionId: string;
|
|
86
|
+
stepId: string;
|
|
87
|
+
attempt: number;
|
|
88
|
+
};
|
|
89
|
+
}): Promise<{
|
|
90
|
+
step: Step;
|
|
91
|
+
scope: NonNullable<Awaited<ReturnType<typeof getJobScope>>>;
|
|
92
|
+
}> {
|
|
93
|
+
const {active: leaseIsActive} = await params.runners.getLeaseState({
|
|
94
|
+
jobId: resolution.input.jobId,
|
|
95
|
+
jobExecutionId: resolution.input.jobExecutionId,
|
|
96
|
+
runnerSessionId: resolution.input.runnerSessionId,
|
|
97
|
+
});
|
|
98
|
+
if (!leaseIsActive)
|
|
99
|
+
throw createInterModuleKnownError(resolution.method, 'lease-not-active', {});
|
|
100
|
+
|
|
101
|
+
const step = await getStepByIdForJobExecution({
|
|
102
|
+
stepId: resolution.input.stepId,
|
|
103
|
+
jobExecutionId: resolution.input.jobExecutionId,
|
|
104
|
+
});
|
|
105
|
+
if (!step) throw createInterModuleKnownError(resolution.method, 'step-not-found', {});
|
|
106
|
+
|
|
107
|
+
const scope = await getJobScope(resolution.input.jobId);
|
|
108
|
+
if (!scope) throw createInterModuleKnownError(resolution.method, 'job-not-found', {});
|
|
109
|
+
if (step.currentAttempt !== resolution.input.attempt) {
|
|
110
|
+
throw createInterModuleKnownError(resolution.method, 'step-attempt-mismatch', {});
|
|
111
|
+
}
|
|
112
|
+
if (step.status !== 'running')
|
|
113
|
+
throw createInterModuleKnownError(resolution.method, 'step-not-running', {});
|
|
114
|
+
if (step.type !== 'agent')
|
|
115
|
+
throw createInterModuleKnownError(resolution.method, 'leased-step-not-agent', {});
|
|
116
|
+
|
|
117
|
+
return {step, scope};
|
|
118
|
+
}
|
|
119
|
+
|
|
51
120
|
return defineInterModulePresentation(workflowsInterModuleContract, {
|
|
52
121
|
startRunFromTrigger: async (input) => {
|
|
53
122
|
try {
|
|
@@ -151,30 +220,12 @@ export function createWorkflowsInterModulePresentation(params: {
|
|
|
151
220
|
const parsed = harnessSchema.safeParse(step?.config.harness);
|
|
152
221
|
return {harness: parsed.success ? parsed.data : DEFAULT_HARNESS};
|
|
153
222
|
},
|
|
223
|
+
listJobStepAttempts: async ({jobId}) => {
|
|
224
|
+
return {stepAttemptIds: await listStepAttemptIdsByJobId(jobId)};
|
|
225
|
+
},
|
|
154
226
|
getLeasedAgentToolContext: async (input) => {
|
|
155
227
|
const method = workflowsInterModuleContract.methods.getLeasedAgentToolContext;
|
|
156
|
-
const {
|
|
157
|
-
jobId: input.jobId,
|
|
158
|
-
jobExecutionId: input.jobExecutionId,
|
|
159
|
-
runnerSessionId: input.runnerSessionId,
|
|
160
|
-
});
|
|
161
|
-
if (!leaseIsActive) throw createInterModuleKnownError(method, 'lease-not-active', {});
|
|
162
|
-
|
|
163
|
-
const step = await getStepByIdForJobExecution({
|
|
164
|
-
stepId: input.stepId,
|
|
165
|
-
jobExecutionId: input.jobExecutionId,
|
|
166
|
-
});
|
|
167
|
-
if (!step) throw createInterModuleKnownError(method, 'step-not-found', {});
|
|
168
|
-
|
|
169
|
-
const scope = await getJobScope(input.jobId);
|
|
170
|
-
if (!scope) throw createInterModuleKnownError(method, 'job-not-found', {});
|
|
171
|
-
if (step.currentAttempt !== input.attempt) {
|
|
172
|
-
throw createInterModuleKnownError(method, 'step-attempt-mismatch', {});
|
|
173
|
-
}
|
|
174
|
-
if (step.status !== 'running')
|
|
175
|
-
throw createInterModuleKnownError(method, 'step-not-running', {});
|
|
176
|
-
if (step.type !== 'agent')
|
|
177
|
-
throw createInterModuleKnownError(method, 'leased-step-not-agent', {});
|
|
228
|
+
const {step, scope} = await resolveLeasedAgentStep({method, input});
|
|
178
229
|
|
|
179
230
|
const config = materializedAgentStepConfigSchema.safeParse(step.config);
|
|
180
231
|
if (!config.success) {
|
|
@@ -182,6 +233,37 @@ export function createWorkflowsInterModulePresentation(params: {
|
|
|
182
233
|
}
|
|
183
234
|
return {workspaceId: scope.workspaceId, integrations: config.data.integrations ?? []};
|
|
184
235
|
},
|
|
236
|
+
getLeasedAgentSessionContext: async (input) => {
|
|
237
|
+
const method = workflowsInterModuleContract.methods.getLeasedAgentSessionContext;
|
|
238
|
+
const {scope} = await resolveLeasedAgentStep({method, input});
|
|
239
|
+
|
|
240
|
+
const detail = await getStepAttemptDetail({stepId: input.stepId, attempt: input.attempt});
|
|
241
|
+
if (!detail) throw createInterModuleKnownError(method, 'step-attempt-mismatch', {});
|
|
242
|
+
|
|
243
|
+
// The dispatch integration records the resolved descriptor on the step
|
|
244
|
+
// attempt's config; anything else is a recording we cannot trust.
|
|
245
|
+
const recorded = detail.attempt.config?.session;
|
|
246
|
+
if (recorded === undefined || recorded === null) {
|
|
247
|
+
return {
|
|
248
|
+
workspaceId: scope.workspaceId,
|
|
249
|
+
projectId: scope.projectId,
|
|
250
|
+
workflowRunAttemptId: detail.workflowRunAttemptId,
|
|
251
|
+
stepAttemptId: detail.attempt.id,
|
|
252
|
+
session: null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const parsed = agentSessionDescriptorSchema.safeParse(recorded);
|
|
256
|
+
if (!parsed.success) {
|
|
257
|
+
throw createInterModuleKnownError(method, 'step-session-config-invalid', {});
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
workspaceId: scope.workspaceId,
|
|
261
|
+
projectId: scope.projectId,
|
|
262
|
+
workflowRunAttemptId: detail.workflowRunAttemptId,
|
|
263
|
+
stepAttemptId: detail.attempt.id,
|
|
264
|
+
session: parsed.data,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
185
267
|
});
|
|
186
268
|
}
|
|
187
269
|
|
|
@@ -54,6 +54,8 @@ const agentTestClient: AgentInterModuleClient = {
|
|
|
54
54
|
getValidationCatalog: vi.fn(),
|
|
55
55
|
resolveAgentConfig: vi.fn(),
|
|
56
56
|
resolveRuntimeCredentials,
|
|
57
|
+
claimSession: vi.fn(),
|
|
58
|
+
carryOverSessions: vi.fn(),
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_JOB_CHECKOUT,
|
|
3
|
+
type WorkflowJsonTemplateTree,
|
|
4
|
+
type WorkflowJsonValue,
|
|
5
|
+
type WorkflowModel,
|
|
6
|
+
} from '@shipfox/api-definitions-dto';
|
|
2
7
|
import {
|
|
3
8
|
createWorkflowExpression,
|
|
4
9
|
parseWorkflowTemplate,
|
|
@@ -41,7 +46,14 @@ interface TestCheckoutStep extends TestWorkflowStepBase {
|
|
|
41
46
|
readonly checkout: Checkout;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
interface TestToolStep extends TestWorkflowStepBase {
|
|
50
|
+
readonly tool: string;
|
|
51
|
+
readonly connection?: string | undefined;
|
|
52
|
+
readonly with?: WorkflowJsonValue | undefined;
|
|
53
|
+
readonly outputs?: Readonly<Record<string, string>> | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type TestWorkflowStep = TestRunStep | TestAgentStep | TestCheckoutStep | TestToolStep;
|
|
45
57
|
|
|
46
58
|
const DEFAULT_RUNNER_LABELS = ['ubuntu-latest'] as const;
|
|
47
59
|
|
|
@@ -171,6 +183,18 @@ function normalizeStep(step: TestWorkflowStep, jobId: string, stepIndex: number)
|
|
|
171
183
|
};
|
|
172
184
|
}
|
|
173
185
|
|
|
186
|
+
if ('tool' in step) {
|
|
187
|
+
return {
|
|
188
|
+
...base,
|
|
189
|
+
kind: 'tool',
|
|
190
|
+
tool: splitToolId(step.tool),
|
|
191
|
+
...(step.connection === undefined ? {} : {connection: step.connection}),
|
|
192
|
+
...(step.with === undefined ? {} : {with: step.with}),
|
|
193
|
+
...(step.outputs === undefined ? {} : {outputMappings: outputMappings(step.outputs)}),
|
|
194
|
+
...optionalToolTemplates(step),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
return {
|
|
175
199
|
...base,
|
|
176
200
|
kind: 'checkout',
|
|
@@ -274,6 +298,54 @@ function optionalAgentTemplates(step: TestAgentStep) {
|
|
|
274
298
|
};
|
|
275
299
|
}
|
|
276
300
|
|
|
301
|
+
function optionalToolTemplates(step: TestToolStep) {
|
|
302
|
+
const withTree = step.with === undefined ? undefined : withTemplateTree(step.with);
|
|
303
|
+
const name = step.name === undefined ? undefined : fieldTemplate('step.name', step.name);
|
|
304
|
+
if (withTree === undefined && name === undefined) return {};
|
|
305
|
+
return {
|
|
306
|
+
templates: {
|
|
307
|
+
...(withTree === undefined ? {} : {with: withTree}),
|
|
308
|
+
...(name === undefined ? {} : {name}),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function withTemplateTree(value: WorkflowJsonValue): WorkflowJsonTemplateTree | undefined {
|
|
314
|
+
if (Array.isArray(value)) {
|
|
315
|
+
const trees = value.map((child) => withTemplateTree(child));
|
|
316
|
+
return trees.every((tree) => tree === undefined) ? undefined : trees;
|
|
317
|
+
}
|
|
318
|
+
if (typeof value === 'object' && value !== null) {
|
|
319
|
+
const entries = Object.entries(value).flatMap(([key, child]) => {
|
|
320
|
+
const tree = withTemplateTree(child);
|
|
321
|
+
return tree === undefined ? [] : [[key, tree] as const];
|
|
322
|
+
});
|
|
323
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
324
|
+
}
|
|
325
|
+
if (typeof value !== 'string') return undefined;
|
|
326
|
+
return fieldTemplate('tool.with', value);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function outputMappings(outputs: Readonly<Record<string, string>>) {
|
|
330
|
+
return Object.fromEntries(
|
|
331
|
+
Object.entries(outputs).map(([key, source]) => {
|
|
332
|
+
const template = fieldTemplate('tool.outputs', source);
|
|
333
|
+
if (template === undefined || template.length !== 1 || template[0]?.kind !== 'deferred') {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Expected test tool output mapping to be exactly one expression for ${key}: ${source}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return [key, template[0].expression];
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function splitToolId(source: string): {readonly id: string; readonly method?: string} {
|
|
344
|
+
const dotIndex = source.indexOf('.');
|
|
345
|
+
if (dotIndex < 1 || dotIndex === source.length - 1) return {id: source};
|
|
346
|
+
return {id: source.slice(0, dotIndex), method: source.slice(dotIndex + 1)};
|
|
347
|
+
}
|
|
348
|
+
|
|
277
349
|
function envTemplates(env: WorkflowModel['env'] | undefined): WorkflowEnvTemplates | undefined {
|
|
278
350
|
if (env === undefined) return undefined;
|
|
279
351
|
|
|
@@ -46,6 +46,8 @@ export const agentTestClient: AgentInterModuleClient = {
|
|
|
46
46
|
credentials: {api_key: 'test-agent-credential'},
|
|
47
47
|
});
|
|
48
48
|
},
|
|
49
|
+
claimSession: vi.fn(),
|
|
50
|
+
carryOverSessions: vi.fn(),
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
export const resolveTestAgentDefaults: AgentDefaultsResolver = (config) => {
|