@hexabot-ai/agentic 3.1.2-alpha.9 → 3.1.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +13 -5
  2. package/dist/cjs/action/abstract-action.js +8 -3
  3. package/dist/cjs/dsl.types.js +2 -1
  4. package/dist/cjs/errors.js +41 -0
  5. package/dist/cjs/index.js +7 -1
  6. package/dist/cjs/step-executors/parallel-executor.js +238 -24
  7. package/dist/cjs/step-executors/skip-helpers.js +25 -1
  8. package/dist/cjs/step-executors/task-executor.js +77 -10
  9. package/dist/cjs/suspension-rebuilder.js +1 -24
  10. package/dist/cjs/utils/naming.js +4 -4
  11. package/dist/cjs/utils/timeout.js +63 -8
  12. package/dist/cjs/utils/workflow-definition-resources.js +73 -0
  13. package/dist/cjs/workflow-compiler.js +0 -2
  14. package/dist/cjs/workflow-runner.js +40 -17
  15. package/dist/esm/action/abstract-action.js +8 -3
  16. package/dist/esm/dsl.types.js +2 -1
  17. package/dist/esm/errors.js +33 -0
  18. package/dist/esm/index.js +2 -0
  19. package/dist/esm/step-executors/parallel-executor.js +239 -25
  20. package/dist/esm/step-executors/skip-helpers.js +23 -0
  21. package/dist/esm/step-executors/task-executor.js +77 -10
  22. package/dist/esm/suspension-rebuilder.js +1 -24
  23. package/dist/esm/utils/naming.js +1 -1
  24. package/dist/esm/utils/timeout.js +63 -8
  25. package/dist/esm/utils/workflow-definition-resources.js +68 -0
  26. package/dist/esm/workflow-compiler.js +0 -2
  27. package/dist/esm/workflow-runner.js +40 -17
  28. package/dist/types/action/abstract-action.d.ts +1 -1
  29. package/dist/types/action/abstract-action.d.ts.map +1 -1
  30. package/dist/types/action/action.types.d.ts +2 -1
  31. package/dist/types/action/action.types.d.ts.map +1 -1
  32. package/dist/types/context.d.ts +2 -1
  33. package/dist/types/context.d.ts.map +1 -1
  34. package/dist/types/dsl.types.d.ts.map +1 -1
  35. package/dist/types/errors.d.ts +10 -0
  36. package/dist/types/errors.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +2 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/step-executors/parallel-executor.d.ts +3 -4
  40. package/dist/types/step-executors/parallel-executor.d.ts.map +1 -1
  41. package/dist/types/step-executors/skip-helpers.d.ts +1 -0
  42. package/dist/types/step-executors/skip-helpers.d.ts.map +1 -1
  43. package/dist/types/step-executors/task-executor.d.ts.map +1 -1
  44. package/dist/types/step-executors/types.d.ts +9 -1
  45. package/dist/types/step-executors/types.d.ts.map +1 -1
  46. package/dist/types/suspension-rebuilder.d.ts.map +1 -1
  47. package/dist/types/utils/naming.d.ts +1 -0
  48. package/dist/types/utils/naming.d.ts.map +1 -1
  49. package/dist/types/utils/timeout.d.ts +4 -2
  50. package/dist/types/utils/timeout.d.ts.map +1 -1
  51. package/dist/types/utils/workflow-definition-resources.d.ts +16 -0
  52. package/dist/types/utils/workflow-definition-resources.d.ts.map +1 -0
  53. package/dist/types/workflow-compiler.d.ts.map +1 -1
  54. package/dist/types/workflow-event-emitter.d.ts +12 -13
  55. package/dist/types/workflow-event-emitter.d.ts.map +1 -1
  56. package/dist/types/workflow-runner.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/__tests__/suspension-rebuilder.test.ts +3 -42
  59. package/src/__tests__/workflow-definition-resources.test.ts +97 -0
  60. package/src/__tests__/workflow-runner.test.ts +58 -5
  61. package/src/action/__tests__/action.test.ts +2 -1
  62. package/src/action/abstract-action.ts +9 -1
  63. package/src/action/action.types.ts +2 -0
  64. package/src/context.ts +2 -0
  65. package/src/dsl.types.ts +5 -1
  66. package/src/errors.ts +43 -0
  67. package/src/index.ts +10 -0
  68. package/src/step-executors/conditional-executor.test.ts +8 -3
  69. package/src/step-executors/loop-executor.test.ts +8 -3
  70. package/src/step-executors/parallel-executor.test.ts +191 -82
  71. package/src/step-executors/parallel-executor.ts +402 -34
  72. package/src/step-executors/skip-helpers.ts +41 -0
  73. package/src/step-executors/task-executor.test.ts +85 -21
  74. package/src/step-executors/task-executor.ts +94 -10
  75. package/src/step-executors/types.ts +14 -1
  76. package/src/suspension-rebuilder.ts +1 -53
  77. package/src/utils/naming.ts +1 -1
  78. package/src/utils/timeout.ts +78 -8
  79. package/src/utils/workflow-definition-resources.ts +111 -0
  80. package/src/workflow-compiler.ts +0 -3
  81. package/src/workflow-event-emitter.ts +13 -6
  82. package/src/workflow-runner.ts +59 -19
@@ -18,7 +18,6 @@ import {
18
18
  shouldStopLoop,
19
19
  updateAccumulator,
20
20
  } from '../step-executors/loop-executor';
21
- import { executeParallel as runParallelExecutor } from '../step-executors/parallel-executor';
22
21
  import type { StepExecutorEnv } from '../step-executors/types';
23
22
  import {
24
23
  parseSuspendedStepId,
@@ -40,11 +39,6 @@ jest.mock('../step-executors/loop-executor', () => ({
40
39
  shouldStopLoop: jest.fn(),
41
40
  }));
42
41
 
43
- jest.mock('../step-executors/parallel-executor', () => ({
44
- __esModule: true,
45
- executeParallel: jest.fn(),
46
- }));
47
-
48
42
  const mockedRunLoopExecutor = runLoopExecutor as jest.MockedFunction<
49
43
  typeof runLoopExecutor
50
44
  >;
@@ -54,9 +48,6 @@ const mockedUpdateAccumulator = updateAccumulator as jest.MockedFunction<
54
48
  const mockedShouldStopLoop = shouldStopLoop as jest.MockedFunction<
55
49
  typeof shouldStopLoop
56
50
  >;
57
- const mockedRunParallelExecutor = runParallelExecutor as jest.MockedFunction<
58
- typeof runParallelExecutor
59
- >;
60
51
 
61
52
  class TestContext extends BaseWorkflowContext {
62
53
  public eventEmitter: EventEmitterLike = { emit: jest.fn(), on: jest.fn() };
@@ -268,7 +259,7 @@ describe('rebuildSuspension', () => {
268
259
  expect(deps.executeFlow).toHaveBeenCalledWith(compiled.flow, state, [], 1);
269
260
  });
270
261
 
271
- it('rebuilds a parallel suspension and continues remaining siblings when needed', async () => {
262
+ it('does not rebuild suspensions inside parallel blocks', () => {
272
263
  const firstTask = createTask('child_a');
273
264
  const secondTask = createTask('child_b');
274
265
  const childA: CompiledStep = {
@@ -308,39 +299,9 @@ describe('rebuildSuspension', () => {
308
299
  state,
309
300
  stepId: '0.parallel.1:child_b',
310
301
  });
311
- const nextSuspension = {
312
- step: {
313
- id: '0.parallel.2:child_c',
314
- name: 'child_c',
315
- type: StepType.Task,
316
- },
317
- continue: jest.fn().mockResolvedValue(undefined),
318
- };
319
- mockedRunParallelExecutor.mockResolvedValue(nextSuspension);
320
302
 
321
- const result = await suspension?.continue({ payload: true });
322
-
323
- expect(deps.captureTaskOutput).toHaveBeenCalledWith(secondTask, state, {
324
- payload: true,
325
- });
326
- expect(mockedRunParallelExecutor).toHaveBeenCalledWith(
327
- expect.anything(),
328
- parallelStep,
329
- state,
330
- [0],
331
- 2,
332
- );
333
- expect(result?.step.id).toBe(nextSuspension.step.id);
334
-
335
- await result?.continue({ done: true });
336
-
337
- expect(nextSuspension.continue).toHaveBeenCalledWith({ done: true });
338
- expect(deps.executeFlow).toHaveBeenLastCalledWith(
339
- compiled.flow,
340
- state,
341
- [],
342
- 1,
343
- );
303
+ expect(suspension).toBeNull();
304
+ expect(deps.captureTaskOutput).not.toHaveBeenCalled();
344
305
  });
345
306
 
346
307
  it('resumes a loop suspension, updating accumulators and continuing execution', async () => {
@@ -0,0 +1,97 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2026 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+
7
+ import type { WorkflowDefinition } from '../dsl.types';
8
+ import {
9
+ collectWorkflowDefinitionResourceRefs,
10
+ remapWorkflowDefinitionResourceRefs,
11
+ type WorkflowDefinitionResourceDescriptor,
12
+ } from '../utils/workflow-definition-resources';
13
+
14
+ const descriptors: WorkflowDefinitionResourceDescriptor[] = [
15
+ { kind: 'memory', settingsKey: 'definition_id' },
16
+ { kind: 'mcp', settingsKey: 'server_id' },
17
+ ];
18
+ const definition: WorkflowDefinition = {
19
+ defs: {
20
+ profile_memory: {
21
+ kind: 'memory',
22
+ settings: {
23
+ definition_id: 'memory-1',
24
+ },
25
+ },
26
+ duplicate_profile_memory: {
27
+ kind: 'memory',
28
+ settings: {
29
+ definition_id: 'memory-1',
30
+ },
31
+ },
32
+ search_mcp: {
33
+ kind: 'mcp',
34
+ settings: {
35
+ server_id: 'mcp-1',
36
+ },
37
+ },
38
+ greet: {
39
+ kind: 'task',
40
+ action: 'send_message',
41
+ bindings: {
42
+ memory: ['profile_memory'],
43
+ mcp: ['search_mcp'],
44
+ },
45
+ },
46
+ },
47
+ flow: [{ do: 'greet' }],
48
+ outputs: { result: '=$output.greet' },
49
+ };
50
+
51
+ describe('workflow definition resource helpers', () => {
52
+ it('collects unique resource references by descriptor kind', () => {
53
+ expect(
54
+ collectWorkflowDefinitionResourceRefs(definition, descriptors),
55
+ ).toEqual({
56
+ memory: ['memory-1'],
57
+ mcp: ['mcp-1'],
58
+ });
59
+ });
60
+
61
+ it('remaps resource references without mutating the original definition', () => {
62
+ const updated = remapWorkflowDefinitionResourceRefs(
63
+ definition,
64
+ descriptors,
65
+ {
66
+ memory: { 'memory-1': 'memory-local' },
67
+ mcp: { 'mcp-1': 'mcp-local' },
68
+ },
69
+ );
70
+
71
+ expect(updated).not.toBe(definition);
72
+ expect(updated.defs.profile_memory.settings).toEqual({
73
+ definition_id: 'memory-local',
74
+ });
75
+ expect(updated.defs.duplicate_profile_memory.settings).toEqual({
76
+ definition_id: 'memory-local',
77
+ });
78
+ expect(updated.defs.search_mcp.settings).toEqual({
79
+ server_id: 'mcp-local',
80
+ });
81
+ expect(definition.defs.profile_memory.settings).toEqual({
82
+ definition_id: 'memory-1',
83
+ });
84
+ expect(definition.defs.search_mcp.settings).toEqual({
85
+ server_id: 'mcp-1',
86
+ });
87
+ });
88
+
89
+ it('returns the original definition when no refs change', () => {
90
+ expect(
91
+ remapWorkflowDefinitionResourceRefs(definition, descriptors, {
92
+ memory: {},
93
+ mcp: {},
94
+ }),
95
+ ).toBe(definition);
96
+ });
97
+ });
@@ -9,6 +9,7 @@ import { z } from 'zod';
9
9
  import { defineAction } from '../action/action';
10
10
  import { BaseWorkflowContext } from '../context';
11
11
  import type { Settings, WorkflowDefinition } from '../dsl.types';
12
+ import { ParallelSuspensionError } from '../errors';
12
13
  import { compileWorkflow } from '../workflow-compiler';
13
14
  import {
14
15
  StepType,
@@ -298,6 +299,7 @@ describe('WorkflowRunner', () => {
298
299
  });
299
300
  const runner = new WorkflowRunner(compiled, { runId: 'run-1' });
300
301
  const context = new TestContext({});
302
+ context.eventEmitter = emitter;
301
303
  const result = await runner.start({
302
304
  inputData: { items: [10, 20] },
303
305
  context,
@@ -313,15 +315,13 @@ describe('WorkflowRunner', () => {
313
315
  }
314
316
 
315
317
  expect(firstExecute).toHaveBeenCalledTimes(1);
316
- expect(secondExecute).not.toHaveBeenCalled();
318
+ expect(secondExecute).toHaveBeenCalledTimes(1);
317
319
  expect(echoExecute).toHaveBeenCalledTimes(3);
318
- expect(
319
- eventLog.filter((entry) => entry.includes('second_task')),
320
- ).toHaveLength(0);
320
+ expect(eventLog).toContain('start:second_task');
321
321
  expect(runner.getSnapshot().status).toBe('finished');
322
322
  const snapshots = runner.getSnapshot().actions;
323
323
  expect(snapshots['0.parallel.0:first_task']?.status).toBe('completed');
324
- expect(snapshots['0.parallel.1:second_task']?.status).toBe('skipped');
324
+ expect(snapshots['0.parallel.1:second_task']?.status).toBe('cancelled');
325
325
  expect(snapshots['1.branch.0.0:branch_task']?.status).toBe('completed');
326
326
  expect(snapshots['1.branch.1.0:second_task']?.status).toBe('skipped');
327
327
  });
@@ -741,6 +741,59 @@ describe('WorkflowRunner', () => {
741
741
  expect(() => context.workflow).toThrow();
742
742
  });
743
743
 
744
+ it('fails when a parallel branch attempts to suspend', async () => {
745
+ const suspendAction = defineAction<
746
+ unknown,
747
+ { reply: string },
748
+ TestContext,
749
+ Settings
750
+ >({
751
+ name: 'parallel_suspend_action',
752
+ inputSchema: z.any(),
753
+ outputSchema: z.object({ reply: z.string() }),
754
+ execute: async ({ context }) => {
755
+ await context.workflow.suspend({ reason: 'not_allowed' });
756
+
757
+ return { reply: 'unreachable' };
758
+ },
759
+ });
760
+ const definition: WorkflowDefinition = {
761
+ defs: createTaskDefs({
762
+ wait_step: { action: 'parallel_suspend_action' },
763
+ }),
764
+ flow: [
765
+ {
766
+ parallel: {
767
+ strategy: 'wait_all',
768
+ steps: [{ do: 'wait_step' }],
769
+ },
770
+ },
771
+ ],
772
+ outputs: { reply: '=$output.wait_step.reply' },
773
+ };
774
+ const compiled = compileWorkflow(definition, {
775
+ actions: { parallel_suspend_action: suspendAction },
776
+ });
777
+ const runner = new WorkflowRunner(compiled, {
778
+ runId: 'run-parallel-suspend',
779
+ });
780
+ const result = await runner.start({
781
+ inputData: {},
782
+ context: new TestContext({}),
783
+ });
784
+
785
+ expect(result.status).toBe('failed');
786
+ if (result.status === 'failed') {
787
+ expect(result.error).toBeInstanceOf(ParallelSuspensionError);
788
+ }
789
+ expect(
790
+ runner.getSnapshot().actions['0.parallel.0:wait_step'],
791
+ ).toMatchObject({
792
+ status: 'failed',
793
+ reason: 'workflow.suspend() is not supported inside a parallel block.',
794
+ });
795
+ });
796
+
744
797
  it('accepts emitter-like implementations without Node EventEmitter', async () => {
745
798
  const pingAction = defineAction<
746
799
  unknown,
@@ -189,6 +189,7 @@ class FlakyDoubleAction extends DoubleAction {
189
189
  context,
190
190
  settings,
191
191
  bindings,
192
+ signal,
192
193
  }: ActionExecutionArgs<
193
194
  z.infer<typeof InputSchema>,
194
195
  DoubleContext,
@@ -199,7 +200,7 @@ class FlakyDoubleAction extends DoubleAction {
199
200
  throw new Error('Intermittent failure');
200
201
  }
201
202
 
202
- return super.execute({ input, context, settings, bindings });
203
+ return super.execute({ input, context, settings, bindings, signal });
203
204
  }
204
205
  }
205
206
 
@@ -8,6 +8,7 @@ import { z, ZodType } from 'zod';
8
8
 
9
9
  import { BaseWorkflowContext } from '../context';
10
10
  import { BaseSettingsSchema } from '../dsl.types';
11
+ import { throwIfAborted } from '../errors';
11
12
  import { assertSnakeCaseName } from '../utils/naming';
12
13
  import { sleep, withTimeout } from '../utils/timeout';
13
14
 
@@ -112,6 +113,7 @@ export abstract class AbstractAction<
112
113
  context: C,
113
114
  settings?: Partial<RuntimeSettings<S>>,
114
115
  bindings?: B,
116
+ signal?: AbortSignal,
115
117
  ): Promise<O> {
116
118
  const input = this.parseInput(payload);
117
119
  const parsedSettings = this.parseSettings(settings);
@@ -136,6 +138,10 @@ export abstract class AbstractAction<
136
138
  const multiplier = retrySettings.multiplier;
137
139
 
138
140
  while (attempt < maxAttempts) {
141
+ if (signal) {
142
+ throwIfAborted(signal);
143
+ }
144
+
139
145
  try {
140
146
  const result = await withTimeout(
141
147
  this.execute({
@@ -143,8 +149,10 @@ export abstract class AbstractAction<
143
149
  context,
144
150
  settings: parsedSettings,
145
151
  bindings: parsedBindings,
152
+ signal: signal ?? new AbortController().signal,
146
153
  }),
147
154
  timeoutMs,
155
+ signal,
148
156
  );
149
157
 
150
158
  return this.parseOutput(result);
@@ -166,7 +174,7 @@ export abstract class AbstractAction<
166
174
  const jitteredDelay = Math.max(0, Math.round(delay * jitterFactor));
167
175
 
168
176
  if (jitteredDelay > 0) {
169
- await sleep(jitteredDelay);
177
+ await sleep(jitteredDelay, signal);
170
178
  }
171
179
  }
172
180
 
@@ -34,6 +34,7 @@ export interface ActionExecutionArgs<
34
34
  context: C;
35
35
  settings: RuntimeSettings<S>;
36
36
  bindings: B;
37
+ signal: AbortSignal;
37
38
  }
38
39
 
39
40
  export interface Action<
@@ -59,6 +60,7 @@ export interface Action<
59
60
  context: C,
60
61
  settings?: Partial<RuntimeSettings<S>>,
61
62
  bindings?: B,
63
+ signal?: AbortSignal,
62
64
  ): Promise<O>;
63
65
  }
64
66
 
package/src/context.ts CHANGED
@@ -42,6 +42,7 @@ export const WORKFLOW_RUN_STATUSES: WorkflowRunStatus[] = [
42
42
  * - suspended: action paused via `workflow.suspend`; resumes to completed.
43
43
  * - completed: action resolved (either immediately or after resume).
44
44
  * - failed: action threw an error.
45
+ * - cancelled: action was aborted by the workflow scheduler.
45
46
  * - skipped: control flow bypassed the step (e.g., alternate branch or wait_any short-circuit).
46
47
  */
47
48
  export type ActionStatus =
@@ -50,6 +51,7 @@ export type ActionStatus =
50
51
  | 'suspended'
51
52
  | 'completed'
52
53
  | 'failed'
54
+ | 'cancelled'
53
55
  | 'skipped';
54
56
 
55
57
  export interface SuspensionOptions {
package/src/dsl.types.ts CHANGED
@@ -10,6 +10,7 @@ import { z } from 'zod';
10
10
 
11
11
  import type { BindingKindSchemas } from './bindings/base-binding';
12
12
  import { validateAndResolveBindings } from './bindings/base-binding';
13
+ import { SNAKE_CASE_REGEX } from './utils/naming';
13
14
 
14
15
  export const ExpressionStringSchema = z
15
16
  .string()
@@ -330,7 +331,10 @@ export const WorkflowDefinitionSchema = z.strictObject({
330
331
  inputs: InputsSchema.optional(),
331
332
  context: z.record(z.string(), JsonValueSchema).optional(),
332
333
  defaults: DefaultsSchema.optional(),
333
- defs: z.record(z.string(), DefDefinitionSchema),
334
+ defs: z.record(
335
+ z.string().regex(SNAKE_CASE_REGEX, 'Name must be snake_case'),
336
+ DefDefinitionSchema,
337
+ ),
334
338
  flow: z.array(FlowStepSchema),
335
339
  outputs: z.record(z.string(), ExpressionStringSchema),
336
340
  });
package/src/errors.ts ADDED
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2025 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+
7
+ export class WorkflowCancellationError extends Error {
8
+ constructor(message = 'Workflow execution was cancelled.') {
9
+ super(message);
10
+ this.name = 'WorkflowCancellationError';
11
+ }
12
+ }
13
+
14
+ export class ParallelSuspensionError extends Error {
15
+ constructor() {
16
+ super('workflow.suspend() is not supported inside a parallel block.');
17
+ this.name = 'ParallelSuspensionError';
18
+ }
19
+ }
20
+
21
+ export const isWorkflowCancellationError = (
22
+ error: unknown,
23
+ ): error is WorkflowCancellationError =>
24
+ error instanceof WorkflowCancellationError ||
25
+ (error instanceof Error && error.name === 'WorkflowCancellationError');
26
+
27
+ export const getAbortReason = (signal: AbortSignal): Error => {
28
+ if (signal.reason instanceof Error) {
29
+ return signal.reason;
30
+ }
31
+
32
+ if (signal.reason !== undefined) {
33
+ return new WorkflowCancellationError(String(signal.reason));
34
+ }
35
+
36
+ return new WorkflowCancellationError();
37
+ };
38
+
39
+ export const throwIfAborted = (signal: AbortSignal): void => {
40
+ if (signal.aborted) {
41
+ throw getAbortReason(signal);
42
+ }
43
+ };
package/src/index.ts CHANGED
@@ -92,6 +92,8 @@ export type {
92
92
 
93
93
  export { NonDeterministicWorkflowError } from './runner-runtime-control';
94
94
 
95
+ export { ParallelSuspensionError, WorkflowCancellationError } from './errors';
96
+
95
97
  export {
96
98
  compileValue,
97
99
  evaluateMapping,
@@ -114,3 +116,11 @@ export {
114
116
  } from './utils/naming';
115
117
 
116
118
  export { sleep, withTimeout } from './utils/timeout';
119
+
120
+ export {
121
+ collectWorkflowDefinitionResourceRefs,
122
+ remapWorkflowDefinitionResourceRefs,
123
+ type WorkflowDefinitionResourceDescriptor,
124
+ type WorkflowDefinitionResourceIdMaps,
125
+ type WorkflowDefinitionResourceRefs,
126
+ } from './utils/workflow-definition-resources';
@@ -48,10 +48,10 @@ const createEnv = (): StepExecutorEnv => {
48
48
  outputMapping: {},
49
49
  inputParser: { parse: (value: unknown) => value } as any,
50
50
  } as any;
51
-
52
- return {
51
+ const env = {
53
52
  compiled,
54
53
  context: new TestContext(),
54
+ signal: new AbortController().signal,
55
55
  runId: 'run-1',
56
56
  buildInstanceStepInfo: jest.fn(),
57
57
  markSnapshot: jest.fn(),
@@ -68,7 +68,12 @@ const createEnv = (): StepExecutorEnv => {
68
68
  captureTaskOutput: jest.fn(),
69
69
  executeFlow: jest.fn(),
70
70
  executeStep: jest.fn(),
71
- };
71
+ fork: jest.fn(),
72
+ } as StepExecutorEnv;
73
+
74
+ env.fork = jest.fn((overrides) => ({ ...env, ...overrides }));
75
+
76
+ return env;
72
77
  };
73
78
 
74
79
  describe('executeConditional', () => {
@@ -47,10 +47,10 @@ const createEnv = (executeFlow: jest.Mock): StepExecutorEnv => {
47
47
  outputMapping: {},
48
48
  inputParser: { parse: (value: unknown) => value } as any,
49
49
  } as any;
50
-
51
- return {
50
+ const env = {
52
51
  compiled,
53
52
  context: new TestContext(),
53
+ signal: new AbortController().signal,
54
54
  runId: 'run-loop',
55
55
  buildInstanceStepInfo: jest.fn(),
56
56
  markSnapshot: jest.fn(),
@@ -67,7 +67,12 @@ const createEnv = (executeFlow: jest.Mock): StepExecutorEnv => {
67
67
  captureTaskOutput: jest.fn(),
68
68
  executeFlow,
69
69
  executeStep: jest.fn(),
70
- };
70
+ fork: jest.fn(),
71
+ } as StepExecutorEnv;
72
+
73
+ env.fork = jest.fn((overrides) => ({ ...env, ...overrides }));
74
+
75
+ return env;
71
76
  };
72
77
  const createTaskStep = (id: string): CompiledStep => ({
73
78
  id,