@hexabot-ai/agentic 3.0.1-alpha.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.
Files changed (176) hide show
  1. package/LICENSE.md +120 -0
  2. package/README.md +231 -0
  3. package/dist/cjs/action/abstract-action.js +141 -0
  4. package/dist/cjs/action/action.js +47 -0
  5. package/dist/cjs/action/action.types.js +7 -0
  6. package/dist/cjs/bindings/base-binding.js +278 -0
  7. package/dist/cjs/context.js +79 -0
  8. package/dist/cjs/dsl.types.js +318 -0
  9. package/dist/cjs/index.js +52 -0
  10. package/dist/cjs/runner-runtime-control.js +343 -0
  11. package/dist/cjs/step-executors/conditional-executor.js +63 -0
  12. package/dist/cjs/step-executors/loop-executor.js +187 -0
  13. package/dist/cjs/step-executors/parallel-executor.js +51 -0
  14. package/dist/cjs/step-executors/skip-helpers.js +29 -0
  15. package/dist/cjs/step-executors/task-executor.js +155 -0
  16. package/dist/cjs/step-executors/types.js +7 -0
  17. package/dist/cjs/suspension-rebuilder.js +301 -0
  18. package/dist/cjs/utils/deferred.js +17 -0
  19. package/dist/cjs/utils/naming.js +48 -0
  20. package/dist/cjs/utils/timeout.js +42 -0
  21. package/dist/cjs/utils/workflow-definition.js +102 -0
  22. package/dist/cjs/workflow-compiler.js +212 -0
  23. package/dist/cjs/workflow-event-emitter.js +40 -0
  24. package/dist/cjs/workflow-runner.js +448 -0
  25. package/dist/cjs/workflow-types.js +7 -0
  26. package/dist/cjs/workflow-values.js +99 -0
  27. package/dist/cjs/workflow.js +243 -0
  28. package/dist/esm/action/abstract-action.js +137 -0
  29. package/dist/esm/action/action.js +44 -0
  30. package/dist/esm/action/action.types.js +6 -0
  31. package/dist/esm/bindings/base-binding.js +273 -0
  32. package/dist/esm/context.js +75 -0
  33. package/dist/esm/dsl.types.js +309 -0
  34. package/dist/esm/index.js +16 -0
  35. package/dist/esm/runner-runtime-control.js +338 -0
  36. package/dist/esm/step-executors/conditional-executor.js +60 -0
  37. package/dist/esm/step-executors/loop-executor.js +182 -0
  38. package/dist/esm/step-executors/parallel-executor.js +48 -0
  39. package/dist/esm/step-executors/skip-helpers.js +25 -0
  40. package/dist/esm/step-executors/task-executor.js +152 -0
  41. package/dist/esm/step-executors/types.js +6 -0
  42. package/dist/esm/suspension-rebuilder.js +296 -0
  43. package/dist/esm/utils/deferred.js +14 -0
  44. package/dist/esm/utils/naming.js +42 -0
  45. package/dist/esm/utils/timeout.js +37 -0
  46. package/dist/esm/utils/workflow-definition.js +98 -0
  47. package/dist/esm/workflow-compiler.js +208 -0
  48. package/dist/esm/workflow-event-emitter.js +36 -0
  49. package/dist/esm/workflow-runner.js +444 -0
  50. package/dist/esm/workflow-types.js +6 -0
  51. package/dist/esm/workflow-values.js +89 -0
  52. package/dist/esm/workflow.js +236 -0
  53. package/dist/types/action/abstract-action.d.ts +54 -0
  54. package/dist/types/action/abstract-action.d.ts.map +1 -0
  55. package/dist/types/action/action.d.ts +23 -0
  56. package/dist/types/action/action.d.ts.map +1 -0
  57. package/dist/types/action/action.types.d.ts +55 -0
  58. package/dist/types/action/action.types.d.ts.map +1 -0
  59. package/dist/types/bindings/base-binding.d.ts +52 -0
  60. package/dist/types/bindings/base-binding.d.ts.map +1 -0
  61. package/dist/types/context.d.ts +105 -0
  62. package/dist/types/context.d.ts.map +1 -0
  63. package/dist/types/dsl.types.d.ts +220 -0
  64. package/dist/types/dsl.types.d.ts.map +1 -0
  65. package/dist/types/index.d.ts +18 -0
  66. package/dist/types/index.d.ts.map +1 -0
  67. package/dist/types/runner-runtime-control.d.ts +64 -0
  68. package/dist/types/runner-runtime-control.d.ts.map +1 -0
  69. package/dist/types/step-executors/conditional-executor.d.ts +13 -0
  70. package/dist/types/step-executors/conditional-executor.d.ts.map +1 -0
  71. package/dist/types/step-executors/loop-executor.d.ts +33 -0
  72. package/dist/types/step-executors/loop-executor.d.ts.map +1 -0
  73. package/dist/types/step-executors/parallel-executor.d.ts +14 -0
  74. package/dist/types/step-executors/parallel-executor.d.ts.map +1 -0
  75. package/dist/types/step-executors/skip-helpers.d.ts +12 -0
  76. package/dist/types/step-executors/skip-helpers.d.ts.map +1 -0
  77. package/dist/types/step-executors/task-executor.d.ts +13 -0
  78. package/dist/types/step-executors/task-executor.d.ts.map +1 -0
  79. package/dist/types/step-executors/types.d.ts +24 -0
  80. package/dist/types/step-executors/types.d.ts.map +1 -0
  81. package/dist/types/suspension-rebuilder.d.ts +67 -0
  82. package/dist/types/suspension-rebuilder.d.ts.map +1 -0
  83. package/dist/types/utils/deferred.d.ts +7 -0
  84. package/dist/types/utils/deferred.d.ts.map +1 -0
  85. package/dist/types/utils/naming.d.ts +24 -0
  86. package/dist/types/utils/naming.d.ts.map +1 -0
  87. package/dist/types/utils/timeout.d.ts +17 -0
  88. package/dist/types/utils/timeout.d.ts.map +1 -0
  89. package/dist/types/utils/workflow-definition.d.ts +3 -0
  90. package/dist/types/utils/workflow-definition.d.ts.map +1 -0
  91. package/dist/types/workflow-compiler.d.ts +12 -0
  92. package/dist/types/workflow-compiler.d.ts.map +1 -0
  93. package/dist/types/workflow-event-emitter.d.ts +70 -0
  94. package/dist/types/workflow-event-emitter.d.ts.map +1 -0
  95. package/dist/types/workflow-runner.d.ts +179 -0
  96. package/dist/types/workflow-runner.d.ts.map +1 -0
  97. package/dist/types/workflow-types.d.ts +195 -0
  98. package/dist/types/workflow-types.d.ts.map +1 -0
  99. package/dist/types/workflow-values.d.ts +34 -0
  100. package/dist/types/workflow-values.d.ts.map +1 -0
  101. package/dist/types/workflow.d.ts +75 -0
  102. package/dist/types/workflow.d.ts.map +1 -0
  103. package/examples/defs-bindings-agent/actions/ai-agent.ts +110 -0
  104. package/examples/defs-bindings-agent/actions/caculate-score.ts +49 -0
  105. package/examples/defs-bindings-agent/actions/index.ts +15 -0
  106. package/examples/defs-bindings-agent/bindings.ts +20 -0
  107. package/examples/defs-bindings-agent/context.ts +32 -0
  108. package/examples/defs-bindings-agent/workflow.ts +67 -0
  109. package/examples/defs-bindings-agent/workflow.yml +28 -0
  110. package/examples/full/actions/await-user-input.ts +46 -0
  111. package/examples/full/actions/call-llm.ts +133 -0
  112. package/examples/full/actions/create-ticket.ts +46 -0
  113. package/examples/full/actions/decision-router.ts +83 -0
  114. package/examples/full/actions/get-calendar-events.ts +55 -0
  115. package/examples/full/actions/get-user-profile.ts +72 -0
  116. package/examples/full/actions/index.ts +33 -0
  117. package/examples/full/actions/query-memory.ts +45 -0
  118. package/examples/full/actions/search-web.ts +53 -0
  119. package/examples/full/actions/send-email.ts +50 -0
  120. package/examples/full/context.ts +41 -0
  121. package/examples/full/workflow.ts +75 -0
  122. package/examples/full/workflow.yml +359 -0
  123. package/examples/loop/actions/await-reply.ts +47 -0
  124. package/examples/loop/actions/index.ts +19 -0
  125. package/examples/loop/actions/send-text-message.ts +40 -0
  126. package/examples/loop/context.ts +32 -0
  127. package/examples/loop/workflow.ts +66 -0
  128. package/examples/loop/workflow.yml +66 -0
  129. package/examples/suspend-resume/actions/format-reply.ts +43 -0
  130. package/examples/suspend-resume/actions/index.ts +13 -0
  131. package/examples/suspend-resume/actions/wait-for-user.ts +51 -0
  132. package/examples/suspend-resume/context.ts +32 -0
  133. package/examples/suspend-resume/workflow.ts +79 -0
  134. package/examples/suspend-resume/workflow.yml +29 -0
  135. package/package.json +60 -0
  136. package/src/__tests__/parser.test.ts +95 -0
  137. package/src/__tests__/suspension-rebuilder.test.ts +361 -0
  138. package/src/__tests__/test-helpers.ts +36 -0
  139. package/src/__tests__/validation.test.ts +526 -0
  140. package/src/__tests__/workflow-compiler.test.ts +715 -0
  141. package/src/__tests__/workflow-definition-path.test.ts +381 -0
  142. package/src/__tests__/workflow-event-emitter.test.ts +50 -0
  143. package/src/__tests__/workflow-runner.test.ts +1397 -0
  144. package/src/__tests__/workflow-values.test.ts +132 -0
  145. package/src/__tests__/workflow.test.ts +320 -0
  146. package/src/action/__tests__/abstract-action-timing.test.ts +183 -0
  147. package/src/action/__tests__/action.test.ts +473 -0
  148. package/src/action/abstract-action.ts +215 -0
  149. package/src/action/action.ts +83 -0
  150. package/src/action/action.types.ts +93 -0
  151. package/src/bindings/base-binding.ts +508 -0
  152. package/src/context.ts +179 -0
  153. package/src/dsl.types.ts +473 -0
  154. package/src/index.ts +114 -0
  155. package/src/runner-runtime-control.ts +544 -0
  156. package/src/step-executors/conditional-executor.test.ts +192 -0
  157. package/src/step-executors/conditional-executor.ts +83 -0
  158. package/src/step-executors/loop-executor.test.ts +303 -0
  159. package/src/step-executors/loop-executor.ts +299 -0
  160. package/src/step-executors/parallel-executor.test.ts +182 -0
  161. package/src/step-executors/parallel-executor.ts +77 -0
  162. package/src/step-executors/skip-helpers.ts +68 -0
  163. package/src/step-executors/task-executor.test.ts +257 -0
  164. package/src/step-executors/task-executor.ts +248 -0
  165. package/src/step-executors/types.ts +70 -0
  166. package/src/suspension-rebuilder.ts +504 -0
  167. package/src/utils/deferred.ts +23 -0
  168. package/src/utils/naming.ts +55 -0
  169. package/src/utils/timeout.ts +48 -0
  170. package/src/utils/workflow-definition.ts +173 -0
  171. package/src/workflow-compiler.ts +317 -0
  172. package/src/workflow-event-emitter.ts +86 -0
  173. package/src/workflow-runner.ts +603 -0
  174. package/src/workflow-types.ts +207 -0
  175. package/src/workflow-values.ts +159 -0
  176. package/src/workflow.ts +387 -0
@@ -0,0 +1,381 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2025 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+
7
+ import type { FlowStep, WorkflowDefinition } from '../dsl.types';
8
+ import { Workflow, type FlowStepPath } from '../workflow';
9
+
10
+ import { createTaskDefs } from './test-helpers';
11
+
12
+ const createDefinition = (): WorkflowDefinition => ({
13
+ defs: createTaskDefs({
14
+ task_one: { action: 'noop' },
15
+ task_two: { action: 'noop' },
16
+ task_three: { action: 'noop' },
17
+ task_four: { action: 'noop' },
18
+ task_five: { action: 'noop' },
19
+ task_six: { action: 'noop' },
20
+ }),
21
+ flow: [
22
+ { do: 'task_one' },
23
+ { parallel: { steps: [{ do: 'task_two' }, { do: 'task_three' }] } },
24
+ {
25
+ conditional: {
26
+ when: [
27
+ { condition: '=true', steps: [{ do: 'task_four' }] },
28
+ { else: true, steps: [{ do: 'task_five' }] },
29
+ ],
30
+ },
31
+ },
32
+ ],
33
+ outputs: { done: '=true' },
34
+ });
35
+ const getParallelSteps = (definition: WorkflowDefinition) =>
36
+ (definition.flow[1] as { parallel: { steps: FlowStep[] } }).parallel.steps;
37
+
38
+ describe('workflow definition path helpers', () => {
39
+ describe('getValueAtPath', () => {
40
+ it('returns nested values for object and array paths', () => {
41
+ const definition = createDefinition();
42
+ const path: FlowStepPath = ['flow', 1, 'parallel', 'steps', 0, 'do'];
43
+
44
+ expect(Workflow.getValueAtPath(definition, path)).toBe('task_two');
45
+ });
46
+
47
+ it('returns the root value for an empty path', () => {
48
+ const definition = createDefinition();
49
+
50
+ expect(Workflow.getValueAtPath(definition, [])).toBe(definition);
51
+ });
52
+
53
+ it('returns undefined for invalid array keys or nullish branches', () => {
54
+ const definition = createDefinition();
55
+
56
+ expect(
57
+ Workflow.getValueAtPath(definition, ['flow', '1']),
58
+ ).toBeUndefined();
59
+ expect(
60
+ Workflow.getValueAtPath({ flow: null }, ['flow', 0]),
61
+ ).toBeUndefined();
62
+ });
63
+
64
+ it('stringifies object keys when walking paths', () => {
65
+ const value = { 0: { label: 'zero' } };
66
+
67
+ expect(Workflow.getValueAtPath(value, [0, 'label'])).toBe('zero');
68
+ });
69
+ });
70
+
71
+ describe('setValueAtPath', () => {
72
+ it('updates nested object values immutably', () => {
73
+ const definition = createDefinition();
74
+ const updated = Workflow.setValueAtPath(
75
+ definition,
76
+ ['defs', 'task_one', 'action'],
77
+ 'updated_action',
78
+ );
79
+
80
+ expect(updated).not.toBe(definition);
81
+ expect(updated.defs.task_one.action).toBe('updated_action');
82
+ expect(updated.flow).toBe(definition.flow);
83
+ });
84
+
85
+ it('updates array elements immutably', () => {
86
+ const steps: FlowStep[] = [{ do: 'task_one' }, { do: 'task_two' }];
87
+ const updated = Workflow.setValueAtPath(steps, [1, 'do'], 'task_three');
88
+
89
+ expect(updated).not.toBe(steps);
90
+ expect((updated as FlowStep[])[1]).toEqual({ do: 'task_three' });
91
+ expect(steps[1]).toEqual({ do: 'task_two' });
92
+ });
93
+
94
+ it('returns the original value for invalid array keys or non-objects', () => {
95
+ const steps: FlowStep[] = [{ do: 'task_one' }];
96
+ const unchanged = Workflow.setValueAtPath(steps, ['0'], {
97
+ do: 'task_two',
98
+ });
99
+
100
+ expect(unchanged).toBe(steps);
101
+ expect(Workflow.setValueAtPath('value', ['next'], 'updated')).toBe(
102
+ 'value',
103
+ );
104
+ });
105
+ });
106
+
107
+ describe('removeStepAtPath', () => {
108
+ it('removes a step at the provided path', () => {
109
+ const definition = createDefinition();
110
+ const updated = Workflow.removeStepAtPath(definition, ['flow', 1]);
111
+
112
+ expect(updated).not.toBeNull();
113
+ const nextDefinition = updated as WorkflowDefinition;
114
+
115
+ expect(nextDefinition.flow).toHaveLength(2);
116
+ expect(nextDefinition.flow[1]).toEqual(definition.flow[2]);
117
+ expect(nextDefinition.defs.task_two).toEqual({
118
+ kind: 'task',
119
+ action: 'noop',
120
+ });
121
+ expect(nextDefinition.defs.task_three).toEqual({
122
+ kind: 'task',
123
+ action: 'noop',
124
+ });
125
+ expect(definition.flow).toHaveLength(3);
126
+ });
127
+
128
+ it('removes a nested step in a parallel block', () => {
129
+ const definition = createDefinition();
130
+ const updated = Workflow.removeStepAtPath(definition, [
131
+ 'flow',
132
+ 1,
133
+ 'parallel',
134
+ 'steps',
135
+ 0,
136
+ ]);
137
+
138
+ expect(updated).not.toBeNull();
139
+ const nextDefinition = updated as WorkflowDefinition;
140
+ const steps = getParallelSteps(nextDefinition);
141
+
142
+ expect(steps).toHaveLength(1);
143
+ expect(steps[0]).toEqual({ do: 'task_three' });
144
+ expect(nextDefinition.defs.task_two).toBeUndefined();
145
+ expect(nextDefinition.defs.task_three).toEqual({
146
+ kind: 'task',
147
+ action: 'noop',
148
+ });
149
+ });
150
+
151
+ it('removes an unreferenced task definition when deleting a task step', () => {
152
+ const definition = createDefinition();
153
+ const updated = Workflow.removeStepAtPath(definition, ['flow', 0]);
154
+
155
+ expect(updated).not.toBeNull();
156
+ const nextDefinition = updated as WorkflowDefinition;
157
+
158
+ expect(nextDefinition.defs.task_one).toBeUndefined();
159
+ expect(nextDefinition.flow).toEqual(definition.flow.slice(1));
160
+ });
161
+
162
+ it('keeps task definitions that are still referenced by other steps', () => {
163
+ const definition = createDefinition();
164
+ const definitionWithDuplicateTask: WorkflowDefinition = {
165
+ ...definition,
166
+ flow: [...definition.flow, { do: 'task_one' }],
167
+ };
168
+ const updated = Workflow.removeStepAtPath(definitionWithDuplicateTask, [
169
+ 'flow',
170
+ 0,
171
+ ]);
172
+
173
+ expect(updated).not.toBeNull();
174
+ const nextDefinition = updated as WorkflowDefinition;
175
+
176
+ expect(nextDefinition.defs.task_one).toEqual({
177
+ kind: 'task',
178
+ action: 'noop',
179
+ });
180
+ expect(nextDefinition.flow).toEqual([
181
+ ...definition.flow.slice(1),
182
+ { do: 'task_one' },
183
+ ]);
184
+ });
185
+
186
+ it('returns null for invalid paths', () => {
187
+ const definition = createDefinition();
188
+
189
+ expect(Workflow.removeStepAtPath(definition, [])).toBeNull();
190
+ expect(Workflow.removeStepAtPath(definition, ['flow', '1'])).toBeNull();
191
+ expect(
192
+ Workflow.removeStepAtPath(definition, ['defs', 'task_one', 0]),
193
+ ).toBeNull();
194
+ expect(Workflow.removeStepAtPath(definition, ['flow', 99])).toBeNull();
195
+ });
196
+ });
197
+
198
+ describe('insertStepAtPath', () => {
199
+ it('inserts a step at the provided path', () => {
200
+ const definition = createDefinition();
201
+ const step: FlowStep = { do: 'task_six' };
202
+ const updated = Workflow.insertStepAtPath(definition, ['flow', 1], step);
203
+
204
+ expect(updated).not.toBeNull();
205
+ const nextDefinition = updated as WorkflowDefinition;
206
+
207
+ expect(nextDefinition.flow).toHaveLength(4);
208
+ expect(nextDefinition.flow[1]).toEqual(step);
209
+ expect(definition.flow).toHaveLength(3);
210
+ });
211
+
212
+ it('clamps insertion indices to array bounds', () => {
213
+ const definition = createDefinition();
214
+ const step: FlowStep = { do: 'task_six' };
215
+ const insertAtStart = Workflow.insertStepAtPath(
216
+ definition,
217
+ ['flow', -2],
218
+ step,
219
+ );
220
+ const insertAtEnd = Workflow.insertStepAtPath(
221
+ definition,
222
+ ['flow', 99],
223
+ step,
224
+ );
225
+
226
+ expect(insertAtStart).not.toBeNull();
227
+ expect((insertAtStart as WorkflowDefinition).flow[0]).toEqual(step);
228
+
229
+ expect(insertAtEnd).not.toBeNull();
230
+ const endDefinition = insertAtEnd as WorkflowDefinition;
231
+ expect(endDefinition.flow[endDefinition.flow.length - 1]).toEqual(step);
232
+ });
233
+
234
+ it('inserts a step into a nested array', () => {
235
+ const definition = createDefinition();
236
+ const step: FlowStep = { do: 'task_six' };
237
+ const updated = Workflow.insertStepAtPath(
238
+ definition,
239
+ ['flow', 1, 'parallel', 'steps', 1],
240
+ step,
241
+ );
242
+
243
+ expect(updated).not.toBeNull();
244
+ const nextDefinition = updated as WorkflowDefinition;
245
+ const steps = getParallelSteps(nextDefinition);
246
+
247
+ expect(steps).toEqual([{ do: 'task_two' }, step, { do: 'task_three' }]);
248
+ });
249
+
250
+ it('returns null for invalid paths', () => {
251
+ const definition = createDefinition();
252
+ const step: FlowStep = { do: 'task_six' };
253
+
254
+ expect(Workflow.insertStepAtPath(definition, [], step)).toBeNull();
255
+ expect(
256
+ Workflow.insertStepAtPath(definition, ['flow', '1'], step),
257
+ ).toBeNull();
258
+ expect(
259
+ Workflow.insertStepAtPath(definition, ['defs', 'task_one', 0], step),
260
+ ).toBeNull();
261
+ });
262
+ });
263
+
264
+ describe('safeRenameTaskInDefinition', () => {
265
+ it('renames a task and updates flow and output references', () => {
266
+ const baseDefinition = createDefinition();
267
+ const definition: WorkflowDefinition = {
268
+ ...baseDefinition,
269
+ defs: {
270
+ ...baseDefinition.defs,
271
+ task_two: {
272
+ kind: 'task',
273
+ action: 'noop',
274
+ inputs: {
275
+ from_dot: '=$output.task_one.value',
276
+ from_single: "=$output['task_one']",
277
+ from_double: '=$output["task_one"]',
278
+ untouched: '=$output.task_one_extra.value',
279
+ },
280
+ },
281
+ },
282
+ flow: [
283
+ { do: 'task_one' },
284
+ {
285
+ conditional: {
286
+ when: [
287
+ { condition: '=true', steps: [{ do: 'task_one' }] },
288
+ {
289
+ else: true,
290
+ steps: [
291
+ {
292
+ loop: {
293
+ type: 'for_each',
294
+ for_each: {
295
+ item: 'item',
296
+ in: '=[]',
297
+ },
298
+ steps: [{ do: 'task_one' }],
299
+ },
300
+ },
301
+ ],
302
+ },
303
+ ],
304
+ },
305
+ },
306
+ ],
307
+ outputs: {
308
+ dot: '=$output.task_one.value',
309
+ single: "=$output['task_one'].value",
310
+ double: '=$output["task_one"].value',
311
+ untouched: '=$output.task_one_extra.value',
312
+ },
313
+ };
314
+ const nextDefinition = Workflow.safeRenameTaskInDefinition(
315
+ definition,
316
+ 'task_one',
317
+ 'renamed_task',
318
+ );
319
+
320
+ expect(nextDefinition.defs.task_one).toBeUndefined();
321
+ expect(nextDefinition.defs.renamed_task).toEqual({
322
+ kind: 'task',
323
+ action: 'noop',
324
+ });
325
+ expect(nextDefinition.flow).toEqual([
326
+ { do: 'renamed_task' },
327
+ {
328
+ conditional: {
329
+ when: [
330
+ { condition: '=true', steps: [{ do: 'renamed_task' }] },
331
+ {
332
+ else: true,
333
+ steps: [
334
+ {
335
+ loop: {
336
+ type: 'for_each',
337
+ for_each: {
338
+ item: 'item',
339
+ in: '=[]',
340
+ },
341
+ steps: [{ do: 'renamed_task' }],
342
+ },
343
+ },
344
+ ],
345
+ },
346
+ ],
347
+ },
348
+ },
349
+ ]);
350
+ expect(
351
+ (nextDefinition.defs.task_two as { inputs?: unknown }).inputs,
352
+ ).toEqual({
353
+ from_dot: '=$output.renamed_task.value',
354
+ from_single: "=$output['renamed_task']",
355
+ from_double: '=$output["renamed_task"]',
356
+ untouched: '=$output.task_one_extra.value',
357
+ });
358
+ expect(nextDefinition.outputs).toEqual({
359
+ dot: '=$output.renamed_task.value',
360
+ single: "=$output['renamed_task'].value",
361
+ double: '=$output["renamed_task"].value',
362
+ untouched: '=$output.task_one_extra.value',
363
+ });
364
+ });
365
+
366
+ it('returns the original definition when rename is a no-op', () => {
367
+ const definition = createDefinition();
368
+
369
+ expect(
370
+ Workflow.safeRenameTaskInDefinition(definition, 'task_one', 'task_one'),
371
+ ).toBe(definition);
372
+ expect(
373
+ Workflow.safeRenameTaskInDefinition(
374
+ definition,
375
+ 'missing_task',
376
+ 'renamed_task',
377
+ ),
378
+ ).toBe(definition);
379
+ });
380
+ });
381
+ });
@@ -0,0 +1,50 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2025 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+
7
+ import { StepType, WorkflowEventEmitter } from '../workflow-event-emitter';
8
+
9
+ const step = { id: 'step-1', name: 'sample', type: StepType.Task };
10
+
11
+ describe('WorkflowEventEmitter', () => {
12
+ it('invokes listeners with strongly typed payloads', () => {
13
+ const emitter = new WorkflowEventEmitter();
14
+ const events: string[] = [];
15
+
16
+ emitter.on('hook:workflow:start', ({ runId }) =>
17
+ events.push(`start:${runId}`),
18
+ );
19
+ emitter.on('hook:step:skipped', ({ step: skipped }) =>
20
+ events.push(`skipped:${skipped.id}`),
21
+ );
22
+ emitter.on('hook:workflow:finish', ({ output }) =>
23
+ events.push(`finish:${output.value}`),
24
+ );
25
+
26
+ expect(emitter.emit('hook:workflow:start', { runId: 'run-123' })).toBe(
27
+ true,
28
+ );
29
+ expect(
30
+ emitter.emit('hook:step:skipped', {
31
+ runId: 'run-123',
32
+ step,
33
+ reason: 'test',
34
+ }),
35
+ ).toBe(true);
36
+ expect(
37
+ emitter.emit('hook:workflow:finish', {
38
+ runId: 'run-123',
39
+ output: { value: 42 },
40
+ }),
41
+ ).toBe(true);
42
+
43
+ expect(events).toEqual(['start:run-123', 'skipped:step-1', 'finish:42']);
44
+ });
45
+
46
+ it('returns false when no listeners are attached', () => {
47
+ const emitter = new WorkflowEventEmitter();
48
+ expect(emitter.emit('hook:step:start', { runId: 'id', step })).toBe(false);
49
+ });
50
+ });