@renseiai/agentfactory 0.8.8 → 0.8.9
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/dist/src/config/index.d.ts +1 -1
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/config/repository-config.d.ts +23 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +27 -0
- package/dist/src/config/repository-config.test.js +140 -1
- package/dist/src/governor/decision-engine.d.ts +3 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +11 -0
- package/dist/src/governor/decision-engine.test.js +33 -0
- package/dist/src/governor/governor-types.d.ts +1 -1
- package/dist/src/governor/governor-types.d.ts.map +1 -1
- package/dist/src/governor/governor.d.ts +17 -1
- package/dist/src/governor/governor.d.ts.map +1 -1
- package/dist/src/governor/governor.js +112 -1
- package/dist/src/governor/governor.test.js +155 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +24 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +6 -0
- package/dist/src/orchestrator/parse-work-result.test.js +19 -0
- package/dist/src/providers/index.d.ts +64 -1
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +132 -1
- package/dist/src/providers/index.test.js +340 -2
- package/dist/src/routing/index.d.ts +7 -0
- package/dist/src/routing/index.d.ts.map +1 -0
- package/dist/src/routing/index.js +6 -0
- package/dist/src/routing/observation-recorder.d.ts +19 -0
- package/dist/src/routing/observation-recorder.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.js +73 -0
- package/dist/src/routing/observation-recorder.test.d.ts +2 -0
- package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.test.js +322 -0
- package/dist/src/routing/observation-store.d.ts +40 -0
- package/dist/src/routing/observation-store.d.ts.map +1 -0
- package/dist/src/routing/observation-store.js +1 -0
- package/dist/src/routing/observation-store.test.d.ts +2 -0
- package/dist/src/routing/observation-store.test.d.ts.map +1 -0
- package/dist/src/routing/observation-store.test.js +138 -0
- package/dist/src/routing/posterior-store.d.ts +12 -0
- package/dist/src/routing/posterior-store.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.js +13 -0
- package/dist/src/routing/posterior-store.test.d.ts +2 -0
- package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.test.js +37 -0
- package/dist/src/routing/reward.d.ts +16 -0
- package/dist/src/routing/reward.d.ts.map +1 -0
- package/dist/src/routing/reward.js +29 -0
- package/dist/src/routing/reward.test.d.ts +2 -0
- package/dist/src/routing/reward.test.d.ts.map +1 -0
- package/dist/src/routing/reward.test.js +210 -0
- package/dist/src/routing/routing-engine.d.ts +20 -0
- package/dist/src/routing/routing-engine.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.js +113 -0
- package/dist/src/routing/routing-engine.test.d.ts +2 -0
- package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.test.js +310 -0
- package/dist/src/routing/types.d.ts +157 -0
- package/dist/src/routing/types.d.ts.map +1 -0
- package/dist/src/routing/types.js +68 -0
- package/dist/src/routing/types.test.d.ts +2 -0
- package/dist/src/routing/types.test.d.ts.map +1 -0
- package/dist/src/routing/types.test.js +184 -0
- package/dist/src/templates/types.d.ts +3 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +2 -0
- package/dist/src/workflow/agent-cancellation.d.ts +37 -0
- package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.js +41 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.test.js +86 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.js +46 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
- package/dist/src/workflow/gate-state.d.ts +115 -0
- package/dist/src/workflow/gate-state.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.js +185 -0
- package/dist/src/workflow/gate-state.test.d.ts +2 -0
- package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.test.js +251 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.js +243 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
- package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
- package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.js +216 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.test.js +199 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.js +162 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
- package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
- package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.js +381 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.test.js +211 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.js +216 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
- package/dist/src/workflow/index.d.ts +23 -2
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +15 -1
- package/dist/src/workflow/parallelism-executor.d.ts +25 -0
- package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.js +53 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.test.js +191 -0
- package/dist/src/workflow/parallelism-types.d.ts +80 -0
- package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-types.js +8 -0
- package/dist/src/workflow/phase-context-injector.d.ts +29 -0
- package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.js +43 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.test.js +123 -0
- package/dist/src/workflow/phase-output-collector.d.ts +39 -0
- package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.js +141 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.test.js +179 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
- package/dist/src/workflow/strategies/index.d.ts +4 -0
- package/dist/src/workflow/strategies/index.d.ts.map +1 -0
- package/dist/src/workflow/strategies/index.js +3 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.js +92 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
- package/dist/src/workflow/transition-engine.d.ts.map +1 -1
- package/dist/src/workflow/transition-engine.js +12 -0
- package/dist/src/workflow/transition-engine.test.js +92 -0
- package/dist/src/workflow/workflow-registry.d.ts +5 -1
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
- package/dist/src/workflow/workflow-registry.js +8 -0
- package/dist/src/workflow/workflow-registry.test.js +54 -0
- package/dist/src/workflow/workflow-types.d.ts +151 -6
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +71 -1
- package/dist/src/workflow/workflow-types.test.js +293 -2
- package/package.json +2 -2
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { RaceStrategy } from './race-strategy.js';
|
|
3
|
+
import { InMemoryAgentCancellation } from '../agent-cancellation.js';
|
|
4
|
+
/** Helper to create a task */
|
|
5
|
+
function makeTask(id, issueId) {
|
|
6
|
+
return { id, issueId: issueId ?? id, phaseName: 'test-phase' };
|
|
7
|
+
}
|
|
8
|
+
/** Helper to create a successful result */
|
|
9
|
+
function makeResult(task, outputs, durationMs) {
|
|
10
|
+
return {
|
|
11
|
+
id: task.id,
|
|
12
|
+
issueId: task.issueId,
|
|
13
|
+
success: true,
|
|
14
|
+
outputs,
|
|
15
|
+
durationMs,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Helper to create a failed result */
|
|
19
|
+
function makeFailedResult(task) {
|
|
20
|
+
return {
|
|
21
|
+
id: task.id,
|
|
22
|
+
issueId: task.issueId,
|
|
23
|
+
success: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe('RaceStrategy', () => {
|
|
27
|
+
it('sets strategy to "race"', async () => {
|
|
28
|
+
const strategy = new RaceStrategy();
|
|
29
|
+
const result = await strategy.execute([], {
|
|
30
|
+
dispatch: async () => ({ id: '', issueId: '', success: true }),
|
|
31
|
+
});
|
|
32
|
+
expect(result.strategy).toBe('race');
|
|
33
|
+
});
|
|
34
|
+
describe('empty task list', () => {
|
|
35
|
+
it('returns empty result', async () => {
|
|
36
|
+
const strategy = new RaceStrategy();
|
|
37
|
+
const result = await strategy.execute([], {
|
|
38
|
+
dispatch: async () => ({ id: '', issueId: '', success: true }),
|
|
39
|
+
});
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
strategy: 'race',
|
|
42
|
+
completed: [],
|
|
43
|
+
cancelled: [],
|
|
44
|
+
failed: [],
|
|
45
|
+
outputs: {},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('single task', () => {
|
|
50
|
+
it('returns that task result with no cancellation', async () => {
|
|
51
|
+
const cancellation = new InMemoryAgentCancellation();
|
|
52
|
+
const strategy = new RaceStrategy(cancellation);
|
|
53
|
+
const task = makeTask('t1', 'SUP-100');
|
|
54
|
+
const dispatch = async (t) => makeResult(t, { code: 'done' }, 100);
|
|
55
|
+
const result = await strategy.execute([task], { dispatch });
|
|
56
|
+
expect(result.completed).toHaveLength(1);
|
|
57
|
+
expect(result.completed[0].id).toBe('t1');
|
|
58
|
+
expect(result.completed[0].success).toBe(true);
|
|
59
|
+
expect(result.cancelled).toEqual([]);
|
|
60
|
+
expect(result.failed).toEqual([]);
|
|
61
|
+
expect(result.outputs).toEqual({ 'SUP-100': { code: 'done' } });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('race with 3 agents, agent 2 wins first', () => {
|
|
65
|
+
it('collects winner output and cancels others', async () => {
|
|
66
|
+
const cancellation = new InMemoryAgentCancellation();
|
|
67
|
+
const strategy = new RaceStrategy(cancellation);
|
|
68
|
+
const t1 = makeTask('t1', 'SUP-101');
|
|
69
|
+
const t2 = makeTask('t2', 'SUP-102');
|
|
70
|
+
const t3 = makeTask('t3', 'SUP-103');
|
|
71
|
+
const dispatch = async (task) => {
|
|
72
|
+
// Agent 2 resolves fastest
|
|
73
|
+
if (task.id === 't2') {
|
|
74
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
75
|
+
return makeResult(task, { winner: true }, 10);
|
|
76
|
+
}
|
|
77
|
+
// Agent 1 and 3 take longer
|
|
78
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
79
|
+
return makeResult(task, { winner: false }, 50);
|
|
80
|
+
};
|
|
81
|
+
const result = await strategy.execute([t1, t2, t3], { dispatch });
|
|
82
|
+
expect(result.strategy).toBe('race');
|
|
83
|
+
// All tasks should eventually complete (they all resolve successfully)
|
|
84
|
+
expect(result.completed.length).toBeGreaterThanOrEqual(1);
|
|
85
|
+
// Winner's outputs should be collected
|
|
86
|
+
expect(result.outputs['SUP-102']).toEqual({ winner: true });
|
|
87
|
+
// t1 and t3 should be cancelled
|
|
88
|
+
expect(result.cancelled).toContain('t1');
|
|
89
|
+
expect(result.cancelled).toContain('t3');
|
|
90
|
+
expect(result.cancelled).not.toContain('t2');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('all agents fail', () => {
|
|
94
|
+
it('returns aggregated errors and no cancellation', async () => {
|
|
95
|
+
const cancellation = new InMemoryAgentCancellation();
|
|
96
|
+
const strategy = new RaceStrategy(cancellation);
|
|
97
|
+
const t1 = makeTask('t1', 'SUP-201');
|
|
98
|
+
const t2 = makeTask('t2', 'SUP-202');
|
|
99
|
+
const t3 = makeTask('t3', 'SUP-203');
|
|
100
|
+
const dispatch = async (task) => {
|
|
101
|
+
throw new Error(`Agent ${task.id} failed`);
|
|
102
|
+
};
|
|
103
|
+
const result = await strategy.execute([t1, t2, t3], { dispatch });
|
|
104
|
+
expect(result.completed).toEqual([]);
|
|
105
|
+
expect(result.cancelled).toEqual([]);
|
|
106
|
+
expect(result.failed).toHaveLength(3);
|
|
107
|
+
const failedIds = result.failed.map((f) => f.id);
|
|
108
|
+
expect(failedIds).toContain('t1');
|
|
109
|
+
expect(failedIds).toContain('t2');
|
|
110
|
+
expect(failedIds).toContain('t3');
|
|
111
|
+
for (const f of result.failed) {
|
|
112
|
+
expect(f.error).toMatch(/Agent .+ failed/);
|
|
113
|
+
}
|
|
114
|
+
expect(result.outputs).toEqual({});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('cancellation tracking', () => {
|
|
118
|
+
it('cancelled IDs are reported in result', async () => {
|
|
119
|
+
const cancellation = new InMemoryAgentCancellation();
|
|
120
|
+
const strategy = new RaceStrategy(cancellation);
|
|
121
|
+
const t1 = makeTask('t1');
|
|
122
|
+
const t2 = makeTask('t2');
|
|
123
|
+
const t3 = makeTask('t3');
|
|
124
|
+
const dispatch = async (task) => {
|
|
125
|
+
if (task.id === 't1') {
|
|
126
|
+
// t1 wins immediately
|
|
127
|
+
return makeResult(task, { fast: true }, 1);
|
|
128
|
+
}
|
|
129
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
130
|
+
return makeResult(task, {}, 30);
|
|
131
|
+
};
|
|
132
|
+
const result = await strategy.execute([t1, t2, t3], { dispatch });
|
|
133
|
+
// t2 and t3 should be in cancelled
|
|
134
|
+
expect(result.cancelled).toContain('t2');
|
|
135
|
+
expect(result.cancelled).toContain('t3');
|
|
136
|
+
expect(result.cancelled).toHaveLength(2);
|
|
137
|
+
// Cancellation state matches
|
|
138
|
+
expect(cancellation.isCancelled('t2')).toBe(true);
|
|
139
|
+
expect(cancellation.isCancelled('t3')).toBe(true);
|
|
140
|
+
expect(cancellation.isCancelled('t1')).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('winner outputs collected, non-winner outputs excluded', () => {
|
|
144
|
+
it('only includes the winner outputs in result.outputs', async () => {
|
|
145
|
+
const strategy = new RaceStrategy();
|
|
146
|
+
const t1 = makeTask('t1', 'SUP-301');
|
|
147
|
+
const t2 = makeTask('t2', 'SUP-302');
|
|
148
|
+
const dispatch = async (task) => {
|
|
149
|
+
if (task.id === 't1') {
|
|
150
|
+
// t1 wins
|
|
151
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
152
|
+
return makeResult(task, { result: 'alpha' }, 5);
|
|
153
|
+
}
|
|
154
|
+
// t2 is slower and also succeeds
|
|
155
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
156
|
+
return makeResult(task, { result: 'beta' }, 40);
|
|
157
|
+
};
|
|
158
|
+
const result = await strategy.execute([t1, t2], { dispatch });
|
|
159
|
+
// Winner's outputs should be in result.outputs
|
|
160
|
+
expect(result.outputs['SUP-301']).toEqual({ result: 'alpha' });
|
|
161
|
+
// Non-winner that completed after cancellation: since it still succeeds,
|
|
162
|
+
// its outputs appear only if success is true (per implementation)
|
|
163
|
+
// The key point: the winner's outputs are definitely there
|
|
164
|
+
expect(result.outputs).toHaveProperty('SUP-301');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('agent resolves after winner still appears in completed', () => {
|
|
168
|
+
it('late-finishing agents appear in completed list', async () => {
|
|
169
|
+
const strategy = new RaceStrategy();
|
|
170
|
+
const t1 = makeTask('t1', 'SUP-401');
|
|
171
|
+
const t2 = makeTask('t2', 'SUP-402');
|
|
172
|
+
const dispatch = async (task) => {
|
|
173
|
+
if (task.id === 't1') {
|
|
174
|
+
// t1 wins fast
|
|
175
|
+
return makeResult(task, { first: true }, 1);
|
|
176
|
+
}
|
|
177
|
+
// t2 completes later but still successfully
|
|
178
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
179
|
+
return makeResult(task, { second: true }, 30);
|
|
180
|
+
};
|
|
181
|
+
const result = await strategy.execute([t1, t2], { dispatch });
|
|
182
|
+
// Both should appear in completed since both resolved successfully
|
|
183
|
+
const completedIds = result.completed.map((c) => c.id);
|
|
184
|
+
expect(completedIds).toContain('t1');
|
|
185
|
+
expect(completedIds).toContain('t2');
|
|
186
|
+
// t2 should still be marked as cancelled
|
|
187
|
+
expect(result.cancelled).toContain('t2');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('mixed success and failure', () => {
|
|
191
|
+
it('handles a mix of failures and successes correctly', async () => {
|
|
192
|
+
const strategy = new RaceStrategy();
|
|
193
|
+
const t1 = makeTask('t1', 'SUP-501');
|
|
194
|
+
const t2 = makeTask('t2', 'SUP-502');
|
|
195
|
+
const t3 = makeTask('t3', 'SUP-503');
|
|
196
|
+
const dispatch = async (task) => {
|
|
197
|
+
if (task.id === 't1') {
|
|
198
|
+
// t1 fails fast
|
|
199
|
+
throw new Error('Agent t1 crashed');
|
|
200
|
+
}
|
|
201
|
+
if (task.id === 't2') {
|
|
202
|
+
// t2 succeeds and becomes winner
|
|
203
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
204
|
+
return makeResult(task, { answer: 42 }, 15);
|
|
205
|
+
}
|
|
206
|
+
// t3 succeeds later
|
|
207
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
208
|
+
return makeResult(task, { answer: 99 }, 40);
|
|
209
|
+
};
|
|
210
|
+
const result = await strategy.execute([t1, t2, t3], { dispatch });
|
|
211
|
+
// t1 should be in failed
|
|
212
|
+
expect(result.failed).toHaveLength(1);
|
|
213
|
+
expect(result.failed[0].id).toBe('t1');
|
|
214
|
+
expect(result.failed[0].error).toBe('Agent t1 crashed');
|
|
215
|
+
// t2 should be the winner
|
|
216
|
+
expect(result.outputs['SUP-502']).toEqual({ answer: 42 });
|
|
217
|
+
// t3 should be cancelled
|
|
218
|
+
expect(result.cancelled).toContain('t3');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('non-Error rejection', () => {
|
|
222
|
+
it('handles non-Error rejection values', async () => {
|
|
223
|
+
const strategy = new RaceStrategy();
|
|
224
|
+
const t1 = makeTask('t1', 'SUP-601');
|
|
225
|
+
const dispatch = async (_task) => {
|
|
226
|
+
throw 'string error';
|
|
227
|
+
};
|
|
228
|
+
const result = await strategy.execute([t1], { dispatch });
|
|
229
|
+
expect(result.failed).toHaveLength(1);
|
|
230
|
+
expect(result.failed[0].error).toBe('string error');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
describe('cancellation timeout', () => {
|
|
234
|
+
it('resolves after timeout if agent does not acknowledge cancellation', async () => {
|
|
235
|
+
const cancellation = new InMemoryAgentCancellation();
|
|
236
|
+
// Use a very short timeout (100ms) for test speed
|
|
237
|
+
const strategy = new RaceStrategy(cancellation, 100);
|
|
238
|
+
const t1 = makeTask('t1', 'SUP-701');
|
|
239
|
+
const t2 = makeTask('t2', 'SUP-702');
|
|
240
|
+
const dispatch = async (task) => {
|
|
241
|
+
if (task.id === 't1') {
|
|
242
|
+
// t1 wins immediately
|
|
243
|
+
return makeResult(task, { fast: true }, 1);
|
|
244
|
+
}
|
|
245
|
+
// t2 never completes — simulates an agent that never checks isCancelled()
|
|
246
|
+
return new Promise(() => {
|
|
247
|
+
// intentionally never resolves
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
const startTime = Date.now();
|
|
251
|
+
const result = await strategy.execute([t1, t2], { dispatch });
|
|
252
|
+
const elapsed = Date.now() - startTime;
|
|
253
|
+
// Should resolve within a reasonable time (timeout + buffer)
|
|
254
|
+
expect(elapsed).toBeLessThan(1000);
|
|
255
|
+
// Winner should still be collected
|
|
256
|
+
expect(result.completed).toHaveLength(1);
|
|
257
|
+
expect(result.completed[0].id).toBe('t1');
|
|
258
|
+
expect(result.outputs['SUP-701']).toEqual({ fast: true });
|
|
259
|
+
// t2 should be marked as cancelled
|
|
260
|
+
expect(result.cancelled).toContain('t2');
|
|
261
|
+
});
|
|
262
|
+
it('respects configurable timeout value', async () => {
|
|
263
|
+
const cancellation = new InMemoryAgentCancellation();
|
|
264
|
+
// Use a 200ms timeout
|
|
265
|
+
const strategy = new RaceStrategy(cancellation, 200);
|
|
266
|
+
const t1 = makeTask('t1', 'SUP-801');
|
|
267
|
+
const t2 = makeTask('t2', 'SUP-802');
|
|
268
|
+
const dispatch = async (task) => {
|
|
269
|
+
if (task.id === 't1') {
|
|
270
|
+
return makeResult(task, { winner: true }, 1);
|
|
271
|
+
}
|
|
272
|
+
// t2 hangs forever
|
|
273
|
+
return new Promise(() => { });
|
|
274
|
+
};
|
|
275
|
+
const startTime = Date.now();
|
|
276
|
+
await strategy.execute([t1, t2], { dispatch });
|
|
277
|
+
const elapsed = Date.now() - startTime;
|
|
278
|
+
// Should resolve after ~200ms, not immediately and not at 30s default
|
|
279
|
+
expect(elapsed).toBeGreaterThanOrEqual(150); // allow for timer imprecision
|
|
280
|
+
expect(elapsed).toBeLessThan(1000);
|
|
281
|
+
});
|
|
282
|
+
it('force-resolves with hanging agents after timeout expiry', async () => {
|
|
283
|
+
const cancellation = new InMemoryAgentCancellation();
|
|
284
|
+
const strategy = new RaceStrategy(cancellation, 100);
|
|
285
|
+
const t1 = makeTask('t1', 'SUP-901');
|
|
286
|
+
const t2 = makeTask('t2', 'SUP-902');
|
|
287
|
+
const t3 = makeTask('t3', 'SUP-903');
|
|
288
|
+
const dispatch = async (task) => {
|
|
289
|
+
if (task.id === 't1') {
|
|
290
|
+
// t1 wins fast
|
|
291
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
292
|
+
return makeResult(task, { answer: 'first' }, 5);
|
|
293
|
+
}
|
|
294
|
+
// t2 and t3 hang forever — simulate agents that never check isCancelled()
|
|
295
|
+
return new Promise(() => { });
|
|
296
|
+
};
|
|
297
|
+
const result = await strategy.execute([t1, t2, t3], { dispatch });
|
|
298
|
+
// The strategy should have resolved despite hanging agents
|
|
299
|
+
expect(result.strategy).toBe('race');
|
|
300
|
+
expect(result.completed).toHaveLength(1);
|
|
301
|
+
expect(result.completed[0].id).toBe('t1');
|
|
302
|
+
expect(result.outputs['SUP-901']).toEqual({ answer: 'first' });
|
|
303
|
+
// Both hanging agents should be in the cancelled list
|
|
304
|
+
expect(result.cancelled).toContain('t2');
|
|
305
|
+
expect(result.cancelled).toContain('t3');
|
|
306
|
+
// They should NOT appear in failed since they didn't error
|
|
307
|
+
expect(result.failed).toHaveLength(0);
|
|
308
|
+
});
|
|
309
|
+
it('uses default 30s timeout when none is specified', () => {
|
|
310
|
+
// Verify the constructor default — we test this indirectly by confirming
|
|
311
|
+
// the strategy can be constructed without a timeout parameter
|
|
312
|
+
const strategy = new RaceStrategy();
|
|
313
|
+
// If this compiles and runs, the default is applied internally.
|
|
314
|
+
// We can't directly access the private field, but we verify it doesn't throw.
|
|
315
|
+
expect(strategy).toBeDefined();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transition-engine.d.ts","sourceRoot":"","sources":["../../../src/workflow/transition-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAElF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAO9D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,aAAa,CAAA;IACpB,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qDAAqD;IACrD,aAAa,EAAE,OAAO,CAAA;IACtB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACrC;AAMD,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,MAAM,EAAE,cAAc,CAAA;IACtB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;CACf;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"transition-engine.d.ts","sourceRoot":"","sources":["../../../src/workflow/transition-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAElF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAO9D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,aAAa,CAAA;IACpB,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qDAAqD;IACrD,aAAa,EAAE,OAAO,CAAA;IACtB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACrC;AAMD,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,MAAM,EAAE,cAAc,CAAA;IACtB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;CACf;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,GAAG,gBAAgB,CAqF5E"}
|
|
@@ -105,6 +105,18 @@ export function evaluateTransitions(ctx) {
|
|
|
105
105
|
reason: `Phase '${match.to}' does not map to a known GovernorAction`,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
// Check if the target phase belongs to a parallelism group.
|
|
109
|
+
// Only parent issues trigger parallel dispatch — sub-issues are dispatched
|
|
110
|
+
// individually by the ParallelismExecutor within the Governor.
|
|
111
|
+
if (isParentIssue) {
|
|
112
|
+
const group = registry.getParallelismGroup(match.to);
|
|
113
|
+
if (group) {
|
|
114
|
+
return {
|
|
115
|
+
action: 'trigger-parallel-group',
|
|
116
|
+
reason: `Issue ${issue.identifier} in '${issue.status}' → parallel group '${group.name}' (strategy: ${group.strategy})`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
108
120
|
const parentSuffix = isParentIssue ? ' (parent — uses coordination template)' : '';
|
|
109
121
|
return {
|
|
110
122
|
action,
|
|
@@ -319,6 +319,98 @@ describe('evaluateTransitions', () => {
|
|
|
319
319
|
expect(result.reason).not.toContain('coordination template');
|
|
320
320
|
});
|
|
321
321
|
});
|
|
322
|
+
// --- Parallelism group detection ---
|
|
323
|
+
describe('parallelism group detection', () => {
|
|
324
|
+
it('parent issue with phase in parallelism group returns trigger-parallel-group', () => {
|
|
325
|
+
const workflow = makeWorkflow({
|
|
326
|
+
parallelism: [
|
|
327
|
+
{
|
|
328
|
+
name: 'dev-parallel',
|
|
329
|
+
phases: ['development'],
|
|
330
|
+
strategy: 'fan-out',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
const ctx = makeContext({
|
|
335
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
336
|
+
registry: registryWith(workflow),
|
|
337
|
+
isParentIssue: true,
|
|
338
|
+
});
|
|
339
|
+
const result = evaluateTransitions(ctx);
|
|
340
|
+
expect(result.action).toBe('trigger-parallel-group');
|
|
341
|
+
expect(result.reason).toContain('parallel group');
|
|
342
|
+
expect(result.reason).toContain('dev-parallel');
|
|
343
|
+
expect(result.reason).toContain('fan-out');
|
|
344
|
+
});
|
|
345
|
+
it('non-parent issue with phase in parallelism group returns normal action', () => {
|
|
346
|
+
const workflow = makeWorkflow({
|
|
347
|
+
parallelism: [
|
|
348
|
+
{
|
|
349
|
+
name: 'dev-parallel',
|
|
350
|
+
phases: ['development'],
|
|
351
|
+
strategy: 'fan-out',
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
});
|
|
355
|
+
const ctx = makeContext({
|
|
356
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
357
|
+
registry: registryWith(workflow),
|
|
358
|
+
isParentIssue: false,
|
|
359
|
+
});
|
|
360
|
+
const result = evaluateTransitions(ctx);
|
|
361
|
+
expect(result.action).toBe('trigger-development');
|
|
362
|
+
expect(result.reason).not.toContain('parallel group');
|
|
363
|
+
});
|
|
364
|
+
it('issue with no parallelism groups returns normal action', () => {
|
|
365
|
+
const ctx = makeContext({
|
|
366
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
367
|
+
isParentIssue: true,
|
|
368
|
+
});
|
|
369
|
+
const result = evaluateTransitions(ctx);
|
|
370
|
+
expect(result.action).toBe('trigger-development');
|
|
371
|
+
expect(result.reason).not.toContain('parallel group');
|
|
372
|
+
});
|
|
373
|
+
it('parent issue with phase NOT in any parallelism group returns normal action', () => {
|
|
374
|
+
const workflow = makeWorkflow({
|
|
375
|
+
parallelism: [
|
|
376
|
+
{
|
|
377
|
+
name: 'qa-parallel',
|
|
378
|
+
phases: ['qa'],
|
|
379
|
+
strategy: 'fan-in',
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
});
|
|
383
|
+
const ctx = makeContext({
|
|
384
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
385
|
+
registry: registryWith(workflow),
|
|
386
|
+
isParentIssue: true,
|
|
387
|
+
});
|
|
388
|
+
const result = evaluateTransitions(ctx);
|
|
389
|
+
// 'development' is not in the parallelism group, so normal action
|
|
390
|
+
expect(result.action).toBe('trigger-development');
|
|
391
|
+
expect(result.reason).toContain('coordination template');
|
|
392
|
+
});
|
|
393
|
+
it('escalation strategy overrides parallelism group detection', () => {
|
|
394
|
+
const workflow = makeWorkflow({
|
|
395
|
+
parallelism: [
|
|
396
|
+
{
|
|
397
|
+
name: 'dev-parallel',
|
|
398
|
+
phases: ['development'],
|
|
399
|
+
strategy: 'fan-out',
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
const ctx = makeContext({
|
|
404
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
405
|
+
registry: registryWith(workflow),
|
|
406
|
+
isParentIssue: true,
|
|
407
|
+
workflowStrategy: 'escalate-human',
|
|
408
|
+
});
|
|
409
|
+
const result = evaluateTransitions(ctx);
|
|
410
|
+
// Escalation takes priority over parallelism
|
|
411
|
+
expect(result.action).toBe('escalate-human');
|
|
412
|
+
});
|
|
413
|
+
});
|
|
322
414
|
// --- Phase-to-action mapping ---
|
|
323
415
|
describe('phase-to-action mapping', () => {
|
|
324
416
|
it('returns none for unknown phase name', () => {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 2. Project-level override (e.g., .agentfactory/workflow.yaml)
|
|
10
10
|
* 3. Inline config override (programmatic)
|
|
11
11
|
*/
|
|
12
|
-
import type { WorkflowDefinition, EscalationConfig } from './workflow-types.js';
|
|
12
|
+
import type { WorkflowDefinition, EscalationConfig, ParallelismGroupDefinition } from './workflow-types.js';
|
|
13
13
|
/**
|
|
14
14
|
* Interface for an external workflow store (e.g., Redis-backed).
|
|
15
15
|
* WorkflowRegistry can load definitions from this store as an additional layer.
|
|
@@ -86,6 +86,10 @@ export declare class WorkflowRegistry {
|
|
|
86
86
|
* Falls back to 'normal' if no match or no escalation config.
|
|
87
87
|
*/
|
|
88
88
|
getEscalationStrategy(cycleCount: number): string;
|
|
89
|
+
/**
|
|
90
|
+
* Get the parallelism group that contains the given phase, if any.
|
|
91
|
+
*/
|
|
92
|
+
getParallelismGroup(phaseName: string): ParallelismGroupDefinition | undefined;
|
|
89
93
|
/**
|
|
90
94
|
* Get circuit breaker limits from the workflow definition.
|
|
91
95
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow-registry.d.ts","sourceRoot":"","sources":["../../../src/workflow/workflow-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"workflow-registry.d.ts","sourceRoot":"","sources":["../../../src/workflow/workflow-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAA;AAO3G;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAChF,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,kBAAkB,CAAA;IAC7B,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,wFAAwF;IACxF,KAAK,CAAC,EAAE,mBAAmB,CAAA;IAC3B,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAcD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,SAAS,CAAC,CAAwC;;IAI1D;;;OAGG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,GAAE,sBAA2B,GAAG,gBAAgB;IAMpE;;OAEG;WACU,WAAW,CAAC,MAAM,GAAE,sBAA2B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAMxF;;;;OAIG;IACH,UAAU,CAAC,MAAM,GAAE,sBAA2B,GAAG,IAAI;IAsBrD;;;;;;;OAOG;IACG,eAAe,CAAC,MAAM,GAAE,sBAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCzE;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI;IAK/C;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,GAAG,IAAI;IAIhE;;OAEG;IACH,WAAW,IAAI,kBAAkB,GAAG,IAAI;IAIxC;;OAEG;IACH,aAAa,IAAI,gBAAgB,GAAG,IAAI;IAIxC;;;;;OAKG;IACH,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAcjD;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,0BAA0B,GAAG,SAAS;IAK9E;;OAEG;IACH,uBAAuB,IAAI;QAAE,mBAAmB,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE;CAOxF"}
|
|
@@ -148,6 +148,14 @@ export class WorkflowRegistry {
|
|
|
148
148
|
const match = sorted.find(rung => cycleCount >= rung.cycle);
|
|
149
149
|
return match?.strategy ?? 'normal';
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Get the parallelism group that contains the given phase, if any.
|
|
153
|
+
*/
|
|
154
|
+
getParallelismGroup(phaseName) {
|
|
155
|
+
if (!this.workflow?.parallelism)
|
|
156
|
+
return undefined;
|
|
157
|
+
return this.workflow.parallelism.find(g => g.phases.includes(phaseName));
|
|
158
|
+
}
|
|
151
159
|
/**
|
|
152
160
|
* Get circuit breaker limits from the workflow definition.
|
|
153
161
|
*/
|
|
@@ -185,6 +185,60 @@ describe('WorkflowRegistry', () => {
|
|
|
185
185
|
expect(limits.maxSessionsPerPhase).toBe(3);
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
|
+
describe('getParallelismGroup()', () => {
|
|
189
|
+
it('returns the group containing the given phase', () => {
|
|
190
|
+
const custom = makeWorkflow({
|
|
191
|
+
parallelism: [
|
|
192
|
+
{
|
|
193
|
+
name: 'dev-parallel',
|
|
194
|
+
phases: ['development'],
|
|
195
|
+
strategy: 'fan-out',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'qa-parallel',
|
|
199
|
+
phases: ['qa'],
|
|
200
|
+
strategy: 'fan-in',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
const registry = WorkflowRegistry.create({ workflow: custom });
|
|
205
|
+
const group = registry.getParallelismGroup('development');
|
|
206
|
+
expect(group).toBeDefined();
|
|
207
|
+
expect(group.name).toBe('dev-parallel');
|
|
208
|
+
expect(group.strategy).toBe('fan-out');
|
|
209
|
+
});
|
|
210
|
+
it('returns undefined for phase not in any group', () => {
|
|
211
|
+
const custom = makeWorkflow({
|
|
212
|
+
parallelism: [
|
|
213
|
+
{
|
|
214
|
+
name: 'dev-parallel',
|
|
215
|
+
phases: ['development'],
|
|
216
|
+
strategy: 'fan-out',
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
const registry = WorkflowRegistry.create({ workflow: custom });
|
|
221
|
+
expect(registry.getParallelismGroup('qa')).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
it('returns undefined when no parallelism defined', () => {
|
|
224
|
+
const custom = makeWorkflow(); // No parallelism
|
|
225
|
+
const registry = WorkflowRegistry.create({ workflow: custom });
|
|
226
|
+
expect(registry.getParallelismGroup('development')).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
it('returns undefined for a completely unknown phase name', () => {
|
|
229
|
+
const custom = makeWorkflow({
|
|
230
|
+
parallelism: [
|
|
231
|
+
{
|
|
232
|
+
name: 'dev-parallel',
|
|
233
|
+
phases: ['development'],
|
|
234
|
+
strategy: 'fan-out',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
const registry = WorkflowRegistry.create({ workflow: custom });
|
|
239
|
+
expect(registry.getParallelismGroup('nonexistent-phase')).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
188
242
|
describe('getEscalation()', () => {
|
|
189
243
|
it('returns escalation config from workflow', () => {
|
|
190
244
|
const registry = WorkflowRegistry.create();
|