@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,251 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { parseDuration, InMemoryGateStorage, activateGate, satisfyGate, timeoutGate, } from './gate-state.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function makeGateDefinition(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'test-gate',
|
|
9
|
+
type: 'signal',
|
|
10
|
+
trigger: { source: 'comment', match: 'APPROVE' },
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function makeGateState(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
issueId: 'issue-1',
|
|
17
|
+
gateName: 'test-gate',
|
|
18
|
+
gateType: 'signal',
|
|
19
|
+
status: 'active',
|
|
20
|
+
activatedAt: Date.now(),
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// parseDuration
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
describe('parseDuration', () => {
|
|
28
|
+
it('parses hours: "4h" → 14400000ms', () => {
|
|
29
|
+
expect(parseDuration('4h')).toBe(14_400_000);
|
|
30
|
+
});
|
|
31
|
+
it('parses minutes: "30m" → 1800000ms', () => {
|
|
32
|
+
expect(parseDuration('30m')).toBe(1_800_000);
|
|
33
|
+
});
|
|
34
|
+
it('parses days: "2d" → 172800000ms', () => {
|
|
35
|
+
expect(parseDuration('2d')).toBe(172_800_000);
|
|
36
|
+
});
|
|
37
|
+
it('parses seconds: "15s" → 15000ms', () => {
|
|
38
|
+
expect(parseDuration('15s')).toBe(15_000);
|
|
39
|
+
});
|
|
40
|
+
it('throws on invalid format (missing unit)', () => {
|
|
41
|
+
expect(() => parseDuration('100')).toThrow('Invalid duration format');
|
|
42
|
+
});
|
|
43
|
+
it('throws on invalid format (unknown unit)', () => {
|
|
44
|
+
expect(() => parseDuration('5w')).toThrow('Invalid duration format');
|
|
45
|
+
});
|
|
46
|
+
it('throws on empty string', () => {
|
|
47
|
+
expect(() => parseDuration('')).toThrow('Invalid duration format');
|
|
48
|
+
});
|
|
49
|
+
it('throws on non-numeric value', () => {
|
|
50
|
+
expect(() => parseDuration('abch')).toThrow('Invalid duration format');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// InMemoryGateStorage
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
describe('InMemoryGateStorage', () => {
|
|
57
|
+
let storage;
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
storage = new InMemoryGateStorage();
|
|
60
|
+
});
|
|
61
|
+
describe('getGateState', () => {
|
|
62
|
+
it('returns null for non-existent gate', async () => {
|
|
63
|
+
const result = await storage.getGateState('issue-1', 'missing-gate');
|
|
64
|
+
expect(result).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
it('returns stored gate state', async () => {
|
|
67
|
+
const state = makeGateState();
|
|
68
|
+
await storage.setGateState('issue-1', 'test-gate', state);
|
|
69
|
+
const result = await storage.getGateState('issue-1', 'test-gate');
|
|
70
|
+
expect(result).toEqual(state);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('setGateState', () => {
|
|
74
|
+
it('stores and retrieves gate state', async () => {
|
|
75
|
+
const state = makeGateState({ gateName: 'my-gate' });
|
|
76
|
+
await storage.setGateState('issue-1', 'my-gate', state);
|
|
77
|
+
const result = await storage.getGateState('issue-1', 'my-gate');
|
|
78
|
+
expect(result).toEqual(state);
|
|
79
|
+
});
|
|
80
|
+
it('overwrites existing gate state', async () => {
|
|
81
|
+
const state1 = makeGateState({ status: 'active' });
|
|
82
|
+
const state2 = makeGateState({ status: 'satisfied', satisfiedAt: Date.now() });
|
|
83
|
+
await storage.setGateState('issue-1', 'test-gate', state1);
|
|
84
|
+
await storage.setGateState('issue-1', 'test-gate', state2);
|
|
85
|
+
const result = await storage.getGateState('issue-1', 'test-gate');
|
|
86
|
+
expect(result?.status).toBe('satisfied');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('getActiveGates', () => {
|
|
90
|
+
it('returns empty array when no gates exist', async () => {
|
|
91
|
+
const result = await storage.getActiveGates('issue-1');
|
|
92
|
+
expect(result).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
it('returns only active gates for the given issue', async () => {
|
|
95
|
+
const active1 = makeGateState({ gateName: 'gate-a', status: 'active' });
|
|
96
|
+
const satisfied = makeGateState({ gateName: 'gate-b', status: 'satisfied' });
|
|
97
|
+
const active2 = makeGateState({ gateName: 'gate-c', status: 'active' });
|
|
98
|
+
await storage.setGateState('issue-1', 'gate-a', active1);
|
|
99
|
+
await storage.setGateState('issue-1', 'gate-b', satisfied);
|
|
100
|
+
await storage.setGateState('issue-1', 'gate-c', active2);
|
|
101
|
+
const result = await storage.getActiveGates('issue-1');
|
|
102
|
+
expect(result).toHaveLength(2);
|
|
103
|
+
expect(result.map(g => g.gateName)).toContain('gate-a');
|
|
104
|
+
expect(result.map(g => g.gateName)).toContain('gate-c');
|
|
105
|
+
});
|
|
106
|
+
it('does not return gates from other issues', async () => {
|
|
107
|
+
const state = makeGateState({ issueId: 'issue-2', gateName: 'gate-a', status: 'active' });
|
|
108
|
+
await storage.setGateState('issue-2', 'gate-a', state);
|
|
109
|
+
const result = await storage.getActiveGates('issue-1');
|
|
110
|
+
expect(result).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('clearGateStates', () => {
|
|
114
|
+
it('clears all gates for an issue', async () => {
|
|
115
|
+
await storage.setGateState('issue-1', 'gate-a', makeGateState({ gateName: 'gate-a' }));
|
|
116
|
+
await storage.setGateState('issue-1', 'gate-b', makeGateState({ gateName: 'gate-b' }));
|
|
117
|
+
await storage.clearGateStates('issue-1');
|
|
118
|
+
expect(await storage.getGateState('issue-1', 'gate-a')).toBeNull();
|
|
119
|
+
expect(await storage.getGateState('issue-1', 'gate-b')).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
it('does not affect other issues', async () => {
|
|
122
|
+
await storage.setGateState('issue-1', 'gate-a', makeGateState({ issueId: 'issue-1', gateName: 'gate-a' }));
|
|
123
|
+
await storage.setGateState('issue-2', 'gate-b', makeGateState({ issueId: 'issue-2', gateName: 'gate-b' }));
|
|
124
|
+
await storage.clearGateStates('issue-1');
|
|
125
|
+
expect(await storage.getGateState('issue-1', 'gate-a')).toBeNull();
|
|
126
|
+
expect(await storage.getGateState('issue-2', 'gate-b')).not.toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// activateGate
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
describe('activateGate', () => {
|
|
134
|
+
let storage;
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
vi.useFakeTimers();
|
|
137
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
138
|
+
storage = new InMemoryGateStorage();
|
|
139
|
+
});
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
vi.useRealTimers();
|
|
142
|
+
});
|
|
143
|
+
it('creates an active gate state', async () => {
|
|
144
|
+
const gateDef = makeGateDefinition();
|
|
145
|
+
const state = await activateGate('issue-1', gateDef, storage);
|
|
146
|
+
expect(state.issueId).toBe('issue-1');
|
|
147
|
+
expect(state.gateName).toBe('test-gate');
|
|
148
|
+
expect(state.gateType).toBe('signal');
|
|
149
|
+
expect(state.status).toBe('active');
|
|
150
|
+
expect(state.activatedAt).toBe(Date.now());
|
|
151
|
+
});
|
|
152
|
+
it('computes timeout deadline from gate definition', async () => {
|
|
153
|
+
const gateDef = makeGateDefinition({
|
|
154
|
+
timeout: { duration: '4h', action: 'escalate' },
|
|
155
|
+
});
|
|
156
|
+
const state = await activateGate('issue-1', gateDef, storage);
|
|
157
|
+
expect(state.timeoutDeadline).toBe(Date.now() + 14_400_000);
|
|
158
|
+
expect(state.timeoutAction).toBe('escalate');
|
|
159
|
+
expect(state.timeoutDuration).toBe('4h');
|
|
160
|
+
});
|
|
161
|
+
it('does not set timeout fields when gate has no timeout', async () => {
|
|
162
|
+
const gateDef = makeGateDefinition();
|
|
163
|
+
const state = await activateGate('issue-1', gateDef, storage);
|
|
164
|
+
expect(state.timeoutDeadline).toBeUndefined();
|
|
165
|
+
expect(state.timeoutAction).toBeUndefined();
|
|
166
|
+
expect(state.timeoutDuration).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
it('persists gate state in storage', async () => {
|
|
169
|
+
const gateDef = makeGateDefinition();
|
|
170
|
+
await activateGate('issue-1', gateDef, storage);
|
|
171
|
+
const stored = await storage.getGateState('issue-1', 'test-gate');
|
|
172
|
+
expect(stored).not.toBeNull();
|
|
173
|
+
expect(stored?.status).toBe('active');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// satisfyGate
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
describe('satisfyGate', () => {
|
|
180
|
+
let storage;
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
vi.useFakeTimers();
|
|
183
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
184
|
+
storage = new InMemoryGateStorage();
|
|
185
|
+
});
|
|
186
|
+
afterEach(() => {
|
|
187
|
+
vi.useRealTimers();
|
|
188
|
+
});
|
|
189
|
+
it('marks an active gate as satisfied with source', async () => {
|
|
190
|
+
await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'active' }));
|
|
191
|
+
const result = await satisfyGate('issue-1', 'test-gate', 'comment-123', storage);
|
|
192
|
+
expect(result).not.toBeNull();
|
|
193
|
+
expect(result?.status).toBe('satisfied');
|
|
194
|
+
expect(result?.signalSource).toBe('comment-123');
|
|
195
|
+
expect(result?.satisfiedAt).toBe(Date.now());
|
|
196
|
+
});
|
|
197
|
+
it('returns null for non-existent gate', async () => {
|
|
198
|
+
const result = await satisfyGate('issue-1', 'missing-gate', 'source', storage);
|
|
199
|
+
expect(result).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
it('returns null for non-active gate', async () => {
|
|
202
|
+
await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'satisfied' }));
|
|
203
|
+
const result = await satisfyGate('issue-1', 'test-gate', 'source', storage);
|
|
204
|
+
expect(result).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// timeoutGate
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
describe('timeoutGate', () => {
|
|
211
|
+
let storage;
|
|
212
|
+
beforeEach(() => {
|
|
213
|
+
vi.useFakeTimers();
|
|
214
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
215
|
+
storage = new InMemoryGateStorage();
|
|
216
|
+
});
|
|
217
|
+
afterEach(() => {
|
|
218
|
+
vi.useRealTimers();
|
|
219
|
+
});
|
|
220
|
+
it('marks an active gate as timed-out', async () => {
|
|
221
|
+
await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'active' }));
|
|
222
|
+
const result = await timeoutGate('issue-1', 'test-gate', storage);
|
|
223
|
+
expect(result).not.toBeNull();
|
|
224
|
+
expect(result?.status).toBe('timed-out');
|
|
225
|
+
expect(result?.timedOutAt).toBe(Date.now());
|
|
226
|
+
});
|
|
227
|
+
it('returns null for non-existent gate', async () => {
|
|
228
|
+
const result = await timeoutGate('issue-1', 'missing-gate', storage);
|
|
229
|
+
expect(result).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
it('returns null for non-active gate', async () => {
|
|
232
|
+
await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'timed-out' }));
|
|
233
|
+
const result = await timeoutGate('issue-1', 'test-gate', storage);
|
|
234
|
+
expect(result).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// getActiveGates (functional)
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
describe('getActiveGates returns only active gates', () => {
|
|
241
|
+
it('filters by status correctly', async () => {
|
|
242
|
+
const storage = new InMemoryGateStorage();
|
|
243
|
+
await storage.setGateState('issue-1', 'gate-active', makeGateState({ gateName: 'gate-active', status: 'active' }));
|
|
244
|
+
await storage.setGateState('issue-1', 'gate-satisfied', makeGateState({ gateName: 'gate-satisfied', status: 'satisfied' }));
|
|
245
|
+
await storage.setGateState('issue-1', 'gate-timed-out', makeGateState({ gateName: 'gate-timed-out', status: 'timed-out' }));
|
|
246
|
+
await storage.setGateState('issue-1', 'gate-pending', makeGateState({ gateName: 'gate-pending', status: 'pending' }));
|
|
247
|
+
const active = await storage.getActiveGates('issue-1');
|
|
248
|
+
expect(active).toHaveLength(1);
|
|
249
|
+
expect(active[0].gateName).toBe('gate-active');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Evaluator — Decision Engine Integration Layer
|
|
3
|
+
*
|
|
4
|
+
* Unified gate evaluation module that the decision engine calls to check
|
|
5
|
+
* whether workflow phase transitions are blocked by unsatisfied gates.
|
|
6
|
+
* Wires together signal gates, timer gates, webhook gates, and the timeout
|
|
7
|
+
* engine into a single evaluation pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Main entry points:
|
|
10
|
+
* - evaluateGatesForPhase() — called by the governor before decideAction()
|
|
11
|
+
* - activateGatesForPhase() — called when entering a new workflow phase
|
|
12
|
+
* - clearGatesForIssue() — called when an issue reaches terminal state
|
|
13
|
+
*/
|
|
14
|
+
import type { GateState, GateStorage } from '../gate-state.js';
|
|
15
|
+
import type { WorkflowDefinition, GateDefinition } from '../workflow-types.js';
|
|
16
|
+
import type { TimeoutResolution } from './timeout-engine.js';
|
|
17
|
+
/**
|
|
18
|
+
* Options for evaluating gates applicable to a workflow phase.
|
|
19
|
+
* The governor populates this before calling the decision engine.
|
|
20
|
+
*/
|
|
21
|
+
export interface GateEvaluationOptions {
|
|
22
|
+
/** The issue identifier */
|
|
23
|
+
issueId: string;
|
|
24
|
+
/** The current workflow phase name */
|
|
25
|
+
phase: string;
|
|
26
|
+
/** The workflow definition containing gate configurations */
|
|
27
|
+
workflow: WorkflowDefinition;
|
|
28
|
+
/** Storage adapter for gate state persistence */
|
|
29
|
+
storage: GateStorage;
|
|
30
|
+
/** For signal gates — latest comments to check */
|
|
31
|
+
comments?: Array<{
|
|
32
|
+
text: string;
|
|
33
|
+
isBot: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
/** Current time for timer/timeout evaluation (epoch ms, defaults to Date.now()) */
|
|
36
|
+
now?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Result of evaluating all gates for a workflow phase.
|
|
40
|
+
* Consumed by the decision engine to determine if transitions are blocked.
|
|
41
|
+
*/
|
|
42
|
+
export interface GateEvaluationResult {
|
|
43
|
+
/** Whether all gates for this phase are satisfied (or no gates apply) */
|
|
44
|
+
allSatisfied: boolean;
|
|
45
|
+
/** Active unsatisfied gates */
|
|
46
|
+
activeGates: GateState[];
|
|
47
|
+
/** Gates that were satisfied during this evaluation */
|
|
48
|
+
newlySatisfied: GateState[];
|
|
49
|
+
/** Timeout resolutions that need to be acted on */
|
|
50
|
+
timeoutResolutions: TimeoutResolution[];
|
|
51
|
+
/** Reason string for decision engine logging */
|
|
52
|
+
reason: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get all gate definitions from a workflow that apply to a given phase,
|
|
56
|
+
* across all gate types (signal, timer, webhook).
|
|
57
|
+
*
|
|
58
|
+
* @param workflow - The workflow definition containing gate configurations
|
|
59
|
+
* @param phase - The phase name to filter by
|
|
60
|
+
* @returns Array of GateDefinition objects applicable to the phase
|
|
61
|
+
*/
|
|
62
|
+
export declare function getApplicableGates(workflow: WorkflowDefinition, phase: string): GateDefinition[];
|
|
63
|
+
/**
|
|
64
|
+
* Evaluate all gates applicable to a workflow phase.
|
|
65
|
+
*
|
|
66
|
+
* This is the main entry point called by the governor before invoking
|
|
67
|
+
* the decision engine. It evaluates each active gate according to its
|
|
68
|
+
* type and returns an aggregated result indicating whether all gates
|
|
69
|
+
* are satisfied.
|
|
70
|
+
*
|
|
71
|
+
* Evaluation pipeline:
|
|
72
|
+
* 1. Get applicable gates for the phase from the workflow definition
|
|
73
|
+
* 2. For each applicable gate, retrieve its persisted state from storage
|
|
74
|
+
* 3. For each active gate:
|
|
75
|
+
* - Signal gates: check each comment via evaluateSignalGate(); satisfy on match
|
|
76
|
+
* - Timer gates: check via evaluateTimerGate(); satisfy if fired
|
|
77
|
+
* - Webhook gates: check via evaluateWebhookGate() (satisfaction is external)
|
|
78
|
+
* 4. Check timeouts via processGateTimeouts()
|
|
79
|
+
* 5. Return aggregated result
|
|
80
|
+
*
|
|
81
|
+
* @param opts - Gate evaluation options
|
|
82
|
+
* @returns Aggregated gate evaluation result
|
|
83
|
+
*/
|
|
84
|
+
export declare function evaluateGatesForPhase(opts: GateEvaluationOptions): Promise<GateEvaluationResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Activate all gates applicable to a workflow phase.
|
|
87
|
+
*
|
|
88
|
+
* Called when a workflow transitions to a new phase. Finds all gate
|
|
89
|
+
* definitions that apply to the phase and activates each one via
|
|
90
|
+
* the gate-state module, creating persisted gate state with computed
|
|
91
|
+
* timeout deadlines.
|
|
92
|
+
*
|
|
93
|
+
* Gates that are already activated (i.e., have existing state) are
|
|
94
|
+
* skipped to prevent re-activation on subsequent poll cycles.
|
|
95
|
+
*
|
|
96
|
+
* For webhook gates, a cryptographic token is generated and stored in
|
|
97
|
+
* the gate state. The callback URL is stored in `signalSource` so
|
|
98
|
+
* external systems can discover it.
|
|
99
|
+
*
|
|
100
|
+
* @param issueId - The issue identifier
|
|
101
|
+
* @param phase - The phase being entered
|
|
102
|
+
* @param workflow - The workflow definition containing gate configurations
|
|
103
|
+
* @param storage - The gate storage adapter for persisting state
|
|
104
|
+
* @param baseUrl - Optional base URL for webhook callback URLs
|
|
105
|
+
* @returns Array of newly activated gate states
|
|
106
|
+
*/
|
|
107
|
+
export declare function activateGatesForPhase(issueId: string, phase: string, workflow: WorkflowDefinition, storage: GateStorage, baseUrl?: string): Promise<GateState[]>;
|
|
108
|
+
/**
|
|
109
|
+
* Clear all gate states for an issue.
|
|
110
|
+
*
|
|
111
|
+
* Called when an issue reaches a terminal state (Accepted, Canceled,
|
|
112
|
+
* Duplicate) to clean up any persisted gate state. Delegates directly
|
|
113
|
+
* to the storage adapter's clearGateStates() method.
|
|
114
|
+
*
|
|
115
|
+
* @param issueId - The issue identifier
|
|
116
|
+
* @param storage - The gate storage adapter
|
|
117
|
+
*/
|
|
118
|
+
export declare function clearGatesForIssue(issueId: string, storage: GateStorage): Promise<void>;
|
|
119
|
+
//# sourceMappingURL=gate-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-evaluator.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/gate-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAO9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAqB5D;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,iDAAiD;IACjD,OAAO,EAAE,WAAW,CAAA;IACpB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAClD,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,YAAY,EAAE,OAAO,CAAA;IACrB,+BAA+B;IAC/B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,uDAAuD;IACvD,cAAc,EAAE,SAAS,EAAE,CAAA;IAC3B,mDAAmD;IACnD,kBAAkB,EAAE,iBAAiB,EAAE,CAAA;IACvC,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;CACf;AAMD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAMhG;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA2HtG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,EAAE,CAAC,CAwCtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Evaluator — Decision Engine Integration Layer
|
|
3
|
+
*
|
|
4
|
+
* Unified gate evaluation module that the decision engine calls to check
|
|
5
|
+
* whether workflow phase transitions are blocked by unsatisfied gates.
|
|
6
|
+
* Wires together signal gates, timer gates, webhook gates, and the timeout
|
|
7
|
+
* engine into a single evaluation pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Main entry points:
|
|
10
|
+
* - evaluateGatesForPhase() — called by the governor before decideAction()
|
|
11
|
+
* - activateGatesForPhase() — called when entering a new workflow phase
|
|
12
|
+
* - clearGatesForIssue() — called when an issue reaches terminal state
|
|
13
|
+
*/
|
|
14
|
+
import { activateGate, satisfyGate } from '../gate-state.js';
|
|
15
|
+
import { evaluateSignalGate } from './signal-gate.js';
|
|
16
|
+
import { getApplicableSignalGates } from './signal-gate.js';
|
|
17
|
+
import { evaluateTimerGate } from './timer-gate.js';
|
|
18
|
+
import { getApplicableTimerGates } from './timer-gate.js';
|
|
19
|
+
import { evaluateWebhookGate, generateWebhookToken, buildCallbackUrl } from './webhook-gate.js';
|
|
20
|
+
import { getApplicableWebhookGates } from './webhook-gate.js';
|
|
21
|
+
import { processGateTimeouts } from './timeout-engine.js';
|
|
22
|
+
// ============================================
|
|
23
|
+
// Logger
|
|
24
|
+
// ============================================
|
|
25
|
+
const log = {
|
|
26
|
+
info: (msg, data) => console.log(`[gate-evaluator] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
27
|
+
warn: (msg, data) => console.warn(`[gate-evaluator] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
28
|
+
error: (msg, data) => console.error(`[gate-evaluator] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
29
|
+
debug: (_msg, _data) => { },
|
|
30
|
+
};
|
|
31
|
+
// ============================================
|
|
32
|
+
// Gate Query Helpers
|
|
33
|
+
// ============================================
|
|
34
|
+
/**
|
|
35
|
+
* Get all gate definitions from a workflow that apply to a given phase,
|
|
36
|
+
* across all gate types (signal, timer, webhook).
|
|
37
|
+
*
|
|
38
|
+
* @param workflow - The workflow definition containing gate configurations
|
|
39
|
+
* @param phase - The phase name to filter by
|
|
40
|
+
* @returns Array of GateDefinition objects applicable to the phase
|
|
41
|
+
*/
|
|
42
|
+
export function getApplicableGates(workflow, phase) {
|
|
43
|
+
return [
|
|
44
|
+
...getApplicableSignalGates(workflow, phase),
|
|
45
|
+
...getApplicableTimerGates(workflow, phase),
|
|
46
|
+
...getApplicableWebhookGates(workflow, phase),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
// ============================================
|
|
50
|
+
// Main Entry Points
|
|
51
|
+
// ============================================
|
|
52
|
+
/**
|
|
53
|
+
* Evaluate all gates applicable to a workflow phase.
|
|
54
|
+
*
|
|
55
|
+
* This is the main entry point called by the governor before invoking
|
|
56
|
+
* the decision engine. It evaluates each active gate according to its
|
|
57
|
+
* type and returns an aggregated result indicating whether all gates
|
|
58
|
+
* are satisfied.
|
|
59
|
+
*
|
|
60
|
+
* Evaluation pipeline:
|
|
61
|
+
* 1. Get applicable gates for the phase from the workflow definition
|
|
62
|
+
* 2. For each applicable gate, retrieve its persisted state from storage
|
|
63
|
+
* 3. For each active gate:
|
|
64
|
+
* - Signal gates: check each comment via evaluateSignalGate(); satisfy on match
|
|
65
|
+
* - Timer gates: check via evaluateTimerGate(); satisfy if fired
|
|
66
|
+
* - Webhook gates: check via evaluateWebhookGate() (satisfaction is external)
|
|
67
|
+
* 4. Check timeouts via processGateTimeouts()
|
|
68
|
+
* 5. Return aggregated result
|
|
69
|
+
*
|
|
70
|
+
* @param opts - Gate evaluation options
|
|
71
|
+
* @returns Aggregated gate evaluation result
|
|
72
|
+
*/
|
|
73
|
+
export async function evaluateGatesForPhase(opts) {
|
|
74
|
+
const { issueId, phase, workflow, storage, comments, now } = opts;
|
|
75
|
+
const currentTime = now ?? Date.now();
|
|
76
|
+
// 1. Get applicable gate definitions for this phase
|
|
77
|
+
const applicableGateDefs = getApplicableGates(workflow, phase);
|
|
78
|
+
// If no gates apply, everything is satisfied
|
|
79
|
+
if (applicableGateDefs.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
allSatisfied: true,
|
|
82
|
+
activeGates: [],
|
|
83
|
+
newlySatisfied: [],
|
|
84
|
+
timeoutResolutions: [],
|
|
85
|
+
reason: `No gates apply to phase "${phase}"`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const newlySatisfied = [];
|
|
89
|
+
// 2. For each applicable gate, check if it's already activated
|
|
90
|
+
for (const gateDef of applicableGateDefs) {
|
|
91
|
+
const gateState = await storage.getGateState(issueId, gateDef.name);
|
|
92
|
+
// Gate not yet activated — skip evaluation (it will be activated
|
|
93
|
+
// when the phase is entered via activateGatesForPhase)
|
|
94
|
+
if (!gateState || gateState.status !== 'active') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// 3. Evaluate based on gate type
|
|
98
|
+
switch (gateDef.type) {
|
|
99
|
+
case 'signal': {
|
|
100
|
+
// Check each comment against the signal gate
|
|
101
|
+
if (comments && comments.length > 0) {
|
|
102
|
+
for (const comment of comments) {
|
|
103
|
+
const result = evaluateSignalGate(gateDef, comment.text, comment.isBot);
|
|
104
|
+
if (result.matched) {
|
|
105
|
+
const satisfied = await satisfyGate(issueId, gateDef.name, result.source ?? 'signal-match', storage);
|
|
106
|
+
if (satisfied) {
|
|
107
|
+
newlySatisfied.push(satisfied);
|
|
108
|
+
log.info('Signal gate satisfied by comment', {
|
|
109
|
+
issueId,
|
|
110
|
+
gateName: gateDef.name,
|
|
111
|
+
source: result.source,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
break; // Gate satisfied, no need to check more comments
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'timer': {
|
|
121
|
+
const result = evaluateTimerGate(gateDef, currentTime);
|
|
122
|
+
if (result.fired) {
|
|
123
|
+
const satisfied = await satisfyGate(issueId, gateDef.name, `timer-fired:${result.nextFireTime}`, storage);
|
|
124
|
+
if (satisfied) {
|
|
125
|
+
newlySatisfied.push(satisfied);
|
|
126
|
+
log.info('Timer gate fired', {
|
|
127
|
+
issueId,
|
|
128
|
+
gateName: gateDef.name,
|
|
129
|
+
nextFireTime: result.nextFireTime,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'webhook': {
|
|
136
|
+
// Webhook gates are satisfied externally via HTTP callback.
|
|
137
|
+
// We just evaluate the current state to check for timeout.
|
|
138
|
+
evaluateWebhookGate(gateDef, gateState);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 4. Check timeouts on all active gates
|
|
144
|
+
const activeGates = await storage.getActiveGates(issueId);
|
|
145
|
+
const timeoutResolutions = await processGateTimeouts(activeGates, storage, currentTime);
|
|
146
|
+
// 5. Re-fetch active gates after timeout processing (some may have been timed-out)
|
|
147
|
+
const remainingActiveGates = await storage.getActiveGates(issueId);
|
|
148
|
+
// Filter to only those gates that are relevant to this phase
|
|
149
|
+
const applicableGateNames = new Set(applicableGateDefs.map(g => g.name));
|
|
150
|
+
const phaseActiveGates = remainingActiveGates.filter(g => applicableGateNames.has(g.gateName));
|
|
151
|
+
const allSatisfied = phaseActiveGates.length === 0;
|
|
152
|
+
// Build reason string
|
|
153
|
+
let reason;
|
|
154
|
+
if (allSatisfied) {
|
|
155
|
+
if (newlySatisfied.length > 0) {
|
|
156
|
+
const names = newlySatisfied.map(g => g.gateName).join(', ');
|
|
157
|
+
reason = `All gates satisfied for phase "${phase}" (newly satisfied: ${names})`;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
reason = `All gates satisfied for phase "${phase}"`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const activeNames = phaseActiveGates.map(g => g.gateName).join(', ');
|
|
165
|
+
reason = `Unsatisfied gates for phase "${phase}": ${activeNames}`;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
allSatisfied,
|
|
169
|
+
activeGates: phaseActiveGates,
|
|
170
|
+
newlySatisfied,
|
|
171
|
+
timeoutResolutions,
|
|
172
|
+
reason,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Activate all gates applicable to a workflow phase.
|
|
177
|
+
*
|
|
178
|
+
* Called when a workflow transitions to a new phase. Finds all gate
|
|
179
|
+
* definitions that apply to the phase and activates each one via
|
|
180
|
+
* the gate-state module, creating persisted gate state with computed
|
|
181
|
+
* timeout deadlines.
|
|
182
|
+
*
|
|
183
|
+
* Gates that are already activated (i.e., have existing state) are
|
|
184
|
+
* skipped to prevent re-activation on subsequent poll cycles.
|
|
185
|
+
*
|
|
186
|
+
* For webhook gates, a cryptographic token is generated and stored in
|
|
187
|
+
* the gate state. The callback URL is stored in `signalSource` so
|
|
188
|
+
* external systems can discover it.
|
|
189
|
+
*
|
|
190
|
+
* @param issueId - The issue identifier
|
|
191
|
+
* @param phase - The phase being entered
|
|
192
|
+
* @param workflow - The workflow definition containing gate configurations
|
|
193
|
+
* @param storage - The gate storage adapter for persisting state
|
|
194
|
+
* @param baseUrl - Optional base URL for webhook callback URLs
|
|
195
|
+
* @returns Array of newly activated gate states
|
|
196
|
+
*/
|
|
197
|
+
export async function activateGatesForPhase(issueId, phase, workflow, storage, baseUrl) {
|
|
198
|
+
const applicableGateDefs = getApplicableGates(workflow, phase);
|
|
199
|
+
if (applicableGateDefs.length === 0) {
|
|
200
|
+
log.debug('No gates to activate for phase', { issueId, phase });
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
const activated = [];
|
|
204
|
+
for (const gateDef of applicableGateDefs) {
|
|
205
|
+
// Skip if already activated (prevent re-activation on repeated polls)
|
|
206
|
+
const existing = await storage.getGateState(issueId, gateDef.name);
|
|
207
|
+
if (existing) {
|
|
208
|
+
log.debug('Gate already activated, skipping', { issueId, gateName: gateDef.name, status: existing.status });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const gateState = await activateGate(issueId, gateDef, storage);
|
|
212
|
+
// For webhook gates, generate and persist a token + callback URL
|
|
213
|
+
if (gateDef.type === 'webhook' && baseUrl) {
|
|
214
|
+
const token = generateWebhookToken();
|
|
215
|
+
const callbackUrl = buildCallbackUrl(baseUrl, issueId, gateDef.name, token);
|
|
216
|
+
gateState.webhookToken = token;
|
|
217
|
+
gateState.signalSource = callbackUrl;
|
|
218
|
+
await storage.setGateState(issueId, gateDef.name, gateState);
|
|
219
|
+
}
|
|
220
|
+
activated.push(gateState);
|
|
221
|
+
log.info('Gate activated for phase', {
|
|
222
|
+
issueId,
|
|
223
|
+
phase,
|
|
224
|
+
gateName: gateDef.name,
|
|
225
|
+
gateType: gateDef.type,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return activated;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Clear all gate states for an issue.
|
|
232
|
+
*
|
|
233
|
+
* Called when an issue reaches a terminal state (Accepted, Canceled,
|
|
234
|
+
* Duplicate) to clean up any persisted gate state. Delegates directly
|
|
235
|
+
* to the storage adapter's clearGateStates() method.
|
|
236
|
+
*
|
|
237
|
+
* @param issueId - The issue identifier
|
|
238
|
+
* @param storage - The gate storage adapter
|
|
239
|
+
*/
|
|
240
|
+
export async function clearGatesForIssue(issueId, storage) {
|
|
241
|
+
await storage.clearGateStates(issueId);
|
|
242
|
+
log.info('Cleared all gate states for issue', { issueId });
|
|
243
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-evaluator.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/gate-evaluator.test.ts"],"names":[],"mappings":""}
|