@renseiai/agentfactory 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +1 -0
- package/dist/src/config/repository-config.d.ts +44 -0
- package/dist/src/config/repository-config.d.ts.map +1 -0
- package/dist/src/config/repository-config.js +88 -0
- package/dist/src/config/repository-config.test.d.ts +2 -0
- package/dist/src/config/repository-config.test.d.ts.map +1 -0
- package/dist/src/config/repository-config.test.js +249 -0
- package/dist/src/deployment/deployment-checker.d.ts +110 -0
- package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
- package/dist/src/deployment/deployment-checker.js +242 -0
- package/dist/src/deployment/index.d.ts +3 -0
- package/dist/src/deployment/index.d.ts.map +1 -0
- package/dist/src/deployment/index.js +2 -0
- package/dist/src/frontend/index.d.ts +2 -0
- package/dist/src/frontend/index.d.ts.map +1 -0
- package/dist/src/frontend/index.js +1 -0
- package/dist/src/frontend/types.d.ts +106 -0
- package/dist/src/frontend/types.d.ts.map +1 -0
- package/dist/src/frontend/types.js +11 -0
- package/dist/src/governor/decision-engine.d.ts +52 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.js +220 -0
- package/dist/src/governor/decision-engine.test.d.ts +2 -0
- package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.test.js +629 -0
- package/dist/src/governor/event-bus.d.ts +43 -0
- package/dist/src/governor/event-bus.d.ts.map +1 -0
- package/dist/src/governor/event-bus.js +8 -0
- package/dist/src/governor/event-deduplicator.d.ts +43 -0
- package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
- package/dist/src/governor/event-deduplicator.js +53 -0
- package/dist/src/governor/event-driven-governor.d.ts +131 -0
- package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.js +379 -0
- package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
- package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.test.js +673 -0
- package/dist/src/governor/event-types.d.ts +78 -0
- package/dist/src/governor/event-types.d.ts.map +1 -0
- package/dist/src/governor/event-types.js +32 -0
- package/dist/src/governor/governor-types.d.ts +82 -0
- package/dist/src/governor/governor-types.d.ts.map +1 -0
- package/dist/src/governor/governor-types.js +21 -0
- package/dist/src/governor/governor.d.ts +100 -0
- package/dist/src/governor/governor.d.ts.map +1 -0
- package/dist/src/governor/governor.js +262 -0
- package/dist/src/governor/governor.test.d.ts +2 -0
- package/dist/src/governor/governor.test.d.ts.map +1 -0
- package/dist/src/governor/governor.test.js +514 -0
- package/dist/src/governor/human-touchpoints.d.ts +131 -0
- package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.js +251 -0
- package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
- package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.test.js +366 -0
- package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
- package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
- package/dist/src/governor/in-memory-event-bus.js +79 -0
- package/dist/src/governor/index.d.ts +14 -0
- package/dist/src/governor/index.d.ts.map +1 -0
- package/dist/src/governor/index.js +13 -0
- package/dist/src/governor/override-parser.d.ts +60 -0
- package/dist/src/governor/override-parser.d.ts.map +1 -0
- package/dist/src/governor/override-parser.js +98 -0
- package/dist/src/governor/override-parser.test.d.ts +2 -0
- package/dist/src/governor/override-parser.test.d.ts.map +1 -0
- package/dist/src/governor/override-parser.test.js +312 -0
- package/dist/src/governor/platform-adapter.d.ts +69 -0
- package/dist/src/governor/platform-adapter.d.ts.map +1 -0
- package/dist/src/governor/platform-adapter.js +11 -0
- package/dist/src/governor/processing-state.d.ts +66 -0
- package/dist/src/governor/processing-state.d.ts.map +1 -0
- package/dist/src/governor/processing-state.js +43 -0
- package/dist/src/governor/processing-state.test.d.ts +2 -0
- package/dist/src/governor/processing-state.test.d.ts.map +1 -0
- package/dist/src/governor/processing-state.test.js +96 -0
- package/dist/src/governor/top-of-funnel.d.ts +118 -0
- package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.js +168 -0
- package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
- package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.test.js +331 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/linear-cli.d.ts +38 -0
- package/dist/src/linear-cli.d.ts.map +1 -0
- package/dist/src/linear-cli.js +674 -0
- package/dist/src/logger.d.ts +117 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +430 -0
- package/dist/src/manifest/generate.d.ts +20 -0
- package/dist/src/manifest/generate.d.ts.map +1 -0
- package/dist/src/manifest/generate.js +65 -0
- package/dist/src/manifest/index.d.ts +4 -0
- package/dist/src/manifest/index.d.ts.map +1 -0
- package/dist/src/manifest/index.js +2 -0
- package/dist/src/manifest/route-manifest.d.ts +34 -0
- package/dist/src/manifest/route-manifest.d.ts.map +1 -0
- package/dist/src/manifest/route-manifest.js +148 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/activity-emitter.js +306 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/api-activity-emitter.js +417 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
- package/dist/src/orchestrator/heartbeat-writer.js +137 -0
- package/dist/src/orchestrator/index.d.ts +20 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/index.js +22 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
- package/dist/src/orchestrator/log-analyzer.js +572 -0
- package/dist/src/orchestrator/log-config.d.ts +39 -0
- package/dist/src/orchestrator/log-config.d.ts.map +1 -0
- package/dist/src/orchestrator/log-config.js +45 -0
- package/dist/src/orchestrator/orchestrator.d.ts +316 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator/orchestrator.js +3290 -0
- package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.js +135 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.test.js +234 -0
- package/dist/src/orchestrator/progress-logger.d.ts +72 -0
- package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/progress-logger.js +135 -0
- package/dist/src/orchestrator/session-logger.d.ts +159 -0
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/session-logger.js +275 -0
- package/dist/src/orchestrator/state-recovery.d.ts +96 -0
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
- package/dist/src/orchestrator/state-recovery.js +302 -0
- package/dist/src/orchestrator/state-types.d.ts +165 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -0
- package/dist/src/orchestrator/state-types.js +7 -0
- package/dist/src/orchestrator/stream-parser.d.ts +151 -0
- package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
- package/dist/src/orchestrator/stream-parser.js +137 -0
- package/dist/src/orchestrator/types.d.ts +232 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +4 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
- package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
- package/dist/src/providers/a2a-auth.d.ts +81 -0
- package/dist/src/providers/a2a-auth.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.js +188 -0
- package/dist/src/providers/a2a-auth.test.d.ts +2 -0
- package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.test.js +232 -0
- package/dist/src/providers/a2a-provider.d.ts +254 -0
- package/dist/src/providers/a2a-provider.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.js +665 -0
- package/dist/src/providers/a2a-provider.js +811 -0
- package/dist/src/providers/a2a-provider.test.d.ts +2 -0
- package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.test.js +681 -0
- package/dist/src/providers/amp-provider.d.ts +20 -0
- package/dist/src/providers/amp-provider.d.ts.map +1 -0
- package/dist/src/providers/amp-provider.js +24 -0
- package/dist/src/providers/claude-provider.d.ts +18 -0
- package/dist/src/providers/claude-provider.d.ts.map +1 -0
- package/dist/src/providers/claude-provider.js +437 -0
- package/dist/src/providers/codex-provider.d.ts +133 -0
- package/dist/src/providers/codex-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.js +381 -0
- package/dist/src/providers/codex-provider.test.d.ts +2 -0
- package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.test.js +387 -0
- package/dist/src/providers/index.d.ts +44 -0
- package/dist/src/providers/index.d.ts.map +1 -0
- package/dist/src/providers/index.js +85 -0
- package/dist/src/providers/spring-ai-provider.d.ts +90 -0
- package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
- package/dist/src/providers/spring-ai-provider.js +317 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.test.js +200 -0
- package/dist/src/providers/types.d.ts +165 -0
- package/dist/src/providers/types.d.ts.map +1 -0
- package/dist/src/providers/types.js +13 -0
- package/dist/src/templates/adapters.d.ts +51 -0
- package/dist/src/templates/adapters.d.ts.map +1 -0
- package/dist/src/templates/adapters.js +104 -0
- package/dist/src/templates/adapters.test.d.ts +2 -0
- package/dist/src/templates/adapters.test.d.ts.map +1 -0
- package/dist/src/templates/adapters.test.js +165 -0
- package/dist/src/templates/agent-definition.d.ts +85 -0
- package/dist/src/templates/agent-definition.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.js +97 -0
- package/dist/src/templates/agent-definition.test.d.ts +2 -0
- package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.test.js +209 -0
- package/dist/src/templates/index.d.ts +14 -0
- package/dist/src/templates/index.d.ts.map +1 -0
- package/dist/src/templates/index.js +11 -0
- package/dist/src/templates/loader.d.ts +41 -0
- package/dist/src/templates/loader.d.ts.map +1 -0
- package/dist/src/templates/loader.js +114 -0
- package/dist/src/templates/registry.d.ts +80 -0
- package/dist/src/templates/registry.d.ts.map +1 -0
- package/dist/src/templates/registry.js +177 -0
- package/dist/src/templates/registry.test.d.ts +2 -0
- package/dist/src/templates/registry.test.d.ts.map +1 -0
- package/dist/src/templates/registry.test.js +198 -0
- package/dist/src/templates/renderer.d.ts +29 -0
- package/dist/src/templates/renderer.d.ts.map +1 -0
- package/dist/src/templates/renderer.js +35 -0
- package/dist/src/templates/strategy-templates.test.d.ts +2 -0
- package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
- package/dist/src/templates/strategy-templates.test.js +619 -0
- package/dist/src/templates/types.d.ts +233 -0
- package/dist/src/templates/types.d.ts.map +1 -0
- package/dist/src/templates/types.js +127 -0
- package/dist/src/templates/types.test.d.ts +2 -0
- package/dist/src/templates/types.test.d.ts.map +1 -0
- package/dist/src/templates/types.test.js +232 -0
- package/dist/src/tools/index.d.ts +6 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +3 -0
- package/dist/src/tools/linear-runner.d.ts +34 -0
- package/dist/src/tools/linear-runner.d.ts.map +1 -0
- package/dist/src/tools/linear-runner.js +700 -0
- package/dist/src/tools/plugins/linear.d.ts +9 -0
- package/dist/src/tools/plugins/linear.d.ts.map +1 -0
- package/dist/src/tools/plugins/linear.js +138 -0
- package/dist/src/tools/registry.d.ts +9 -0
- package/dist/src/tools/registry.d.ts.map +1 -0
- package/dist/src/tools/registry.js +18 -0
- package/dist/src/tools/types.d.ts +18 -0
- package/dist/src/tools/types.d.ts.map +1 -0
- package/dist/src/tools/types.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { EventDrivenGovernor } from './event-driven-governor.js';
|
|
3
|
+
import { DEFAULT_GOVERNOR_CONFIG } from './governor-types.js';
|
|
4
|
+
import { InMemoryEventBus } from './in-memory-event-bus.js';
|
|
5
|
+
import { InMemoryEventDeduplicator } from './event-deduplicator.js';
|
|
6
|
+
import { initTouchpointStorage, InMemoryOverrideStorage, getOverrideState, } from './human-touchpoints.js';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
function makeIssue(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
id: 'issue-1',
|
|
13
|
+
identifier: 'SUP-100',
|
|
14
|
+
title: 'Test Issue',
|
|
15
|
+
description: undefined,
|
|
16
|
+
status: 'Backlog',
|
|
17
|
+
labels: [],
|
|
18
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function makeMockDeps(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
listIssues: vi.fn().mockResolvedValue([]),
|
|
25
|
+
hasActiveSession: vi.fn().mockResolvedValue(false),
|
|
26
|
+
isWithinCooldown: vi.fn().mockResolvedValue(false),
|
|
27
|
+
isParentIssue: vi.fn().mockResolvedValue(false),
|
|
28
|
+
isHeld: vi.fn().mockResolvedValue(false),
|
|
29
|
+
getOverridePriority: vi.fn().mockResolvedValue(null),
|
|
30
|
+
getWorkflowStrategy: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
isResearchCompleted: vi.fn().mockResolvedValue(false),
|
|
32
|
+
isBacklogCreationCompleted: vi.fn().mockResolvedValue(false),
|
|
33
|
+
getCompletedSessionCount: vi.fn().mockResolvedValue(0),
|
|
34
|
+
dispatchWork: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function makeConfig(eventBus, overrides = {}) {
|
|
39
|
+
return {
|
|
40
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
41
|
+
projects: ['TestProject'],
|
|
42
|
+
eventBus,
|
|
43
|
+
enablePolling: false, // Disable polling by default in tests
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function makeStatusChangedEvent(issue, newStatus, previousStatus) {
|
|
48
|
+
return {
|
|
49
|
+
type: 'issue-status-changed',
|
|
50
|
+
issueId: issue.id,
|
|
51
|
+
issue: { ...issue, status: newStatus },
|
|
52
|
+
previousStatus,
|
|
53
|
+
newStatus,
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
source: 'webhook',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function makeCommentEvent(issue, commentBody, commentId = 'comment-1') {
|
|
59
|
+
return {
|
|
60
|
+
type: 'comment-added',
|
|
61
|
+
issueId: issue.id,
|
|
62
|
+
issue,
|
|
63
|
+
commentId,
|
|
64
|
+
commentBody,
|
|
65
|
+
userId: 'user-1',
|
|
66
|
+
userName: 'Test User',
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
source: 'webhook',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function makeSessionCompletedEvent(issue, outcome = 'success') {
|
|
72
|
+
return {
|
|
73
|
+
type: 'session-completed',
|
|
74
|
+
issueId: issue.id,
|
|
75
|
+
issue,
|
|
76
|
+
sessionId: 'session-1',
|
|
77
|
+
outcome,
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
source: 'webhook',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function makePollSnapshotEvent(issue, project = 'TestProject') {
|
|
83
|
+
return {
|
|
84
|
+
type: 'poll-snapshot',
|
|
85
|
+
issueId: issue.id,
|
|
86
|
+
issue,
|
|
87
|
+
project,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
source: 'poll',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Helper to wait for events to be processed.
|
|
94
|
+
* Publishes events, then gives the event loop time to consume them.
|
|
95
|
+
*/
|
|
96
|
+
async function waitForProcessing(ms = 50) {
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Setup
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
let overrideStorage;
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
overrideStorage = new InMemoryOverrideStorage();
|
|
105
|
+
initTouchpointStorage(overrideStorage);
|
|
106
|
+
});
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Lifecycle
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
describe('EventDrivenGovernor — lifecycle', () => {
|
|
111
|
+
it('isRunning returns false before start', () => {
|
|
112
|
+
const bus = new InMemoryEventBus();
|
|
113
|
+
const deps = makeMockDeps();
|
|
114
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
115
|
+
expect(governor.isRunning()).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
it('isRunning returns true after start', async () => {
|
|
118
|
+
const bus = new InMemoryEventBus();
|
|
119
|
+
const deps = makeMockDeps();
|
|
120
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
121
|
+
await governor.start();
|
|
122
|
+
expect(governor.isRunning()).toBe(true);
|
|
123
|
+
await governor.stop();
|
|
124
|
+
});
|
|
125
|
+
it('isRunning returns false after stop', async () => {
|
|
126
|
+
const bus = new InMemoryEventBus();
|
|
127
|
+
const deps = makeMockDeps();
|
|
128
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
129
|
+
await governor.start();
|
|
130
|
+
await governor.stop();
|
|
131
|
+
expect(governor.isRunning()).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
it('calling start twice is idempotent', async () => {
|
|
134
|
+
const bus = new InMemoryEventBus();
|
|
135
|
+
const deps = makeMockDeps();
|
|
136
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
137
|
+
await governor.start();
|
|
138
|
+
await governor.start(); // should not throw or create duplicates
|
|
139
|
+
expect(governor.isRunning()).toBe(true);
|
|
140
|
+
await governor.stop();
|
|
141
|
+
});
|
|
142
|
+
it('calling stop when not running is safe', async () => {
|
|
143
|
+
const bus = new InMemoryEventBus();
|
|
144
|
+
const deps = makeMockDeps();
|
|
145
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
146
|
+
// Should not throw
|
|
147
|
+
await governor.stop();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Event processing — issue-status-changed
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
describe('EventDrivenGovernor — issue-status-changed events', () => {
|
|
154
|
+
it('dispatches development for Backlog issue', async () => {
|
|
155
|
+
const bus = new InMemoryEventBus();
|
|
156
|
+
const deps = makeMockDeps();
|
|
157
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
158
|
+
await governor.start();
|
|
159
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
160
|
+
const event = makeStatusChangedEvent(issue, 'Backlog', 'Icebox');
|
|
161
|
+
await bus.publish(event);
|
|
162
|
+
await waitForProcessing();
|
|
163
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
164
|
+
await governor.stop();
|
|
165
|
+
});
|
|
166
|
+
it('dispatches QA for Finished issue', async () => {
|
|
167
|
+
const bus = new InMemoryEventBus();
|
|
168
|
+
const deps = makeMockDeps();
|
|
169
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
170
|
+
await governor.start();
|
|
171
|
+
const issue = makeIssue({ status: 'Finished' });
|
|
172
|
+
const event = makeStatusChangedEvent(issue, 'Finished', 'Started');
|
|
173
|
+
await bus.publish(event);
|
|
174
|
+
await waitForProcessing();
|
|
175
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-qa');
|
|
176
|
+
await governor.stop();
|
|
177
|
+
});
|
|
178
|
+
it('dispatches acceptance for Delivered issue', async () => {
|
|
179
|
+
const bus = new InMemoryEventBus();
|
|
180
|
+
const deps = makeMockDeps();
|
|
181
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
182
|
+
await governor.start();
|
|
183
|
+
const issue = makeIssue({ status: 'Delivered' });
|
|
184
|
+
const event = makeStatusChangedEvent(issue, 'Delivered', 'Finished');
|
|
185
|
+
await bus.publish(event);
|
|
186
|
+
await waitForProcessing();
|
|
187
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-acceptance');
|
|
188
|
+
await governor.stop();
|
|
189
|
+
});
|
|
190
|
+
it('dispatches refinement for Rejected issue', async () => {
|
|
191
|
+
const bus = new InMemoryEventBus();
|
|
192
|
+
const deps = makeMockDeps();
|
|
193
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
194
|
+
await governor.start();
|
|
195
|
+
const issue = makeIssue({ status: 'Rejected' });
|
|
196
|
+
const event = makeStatusChangedEvent(issue, 'Rejected', 'Finished');
|
|
197
|
+
await bus.publish(event);
|
|
198
|
+
await waitForProcessing();
|
|
199
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-refinement');
|
|
200
|
+
await governor.stop();
|
|
201
|
+
});
|
|
202
|
+
it('does not dispatch for terminal status', async () => {
|
|
203
|
+
const bus = new InMemoryEventBus();
|
|
204
|
+
const deps = makeMockDeps();
|
|
205
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
206
|
+
await governor.start();
|
|
207
|
+
const issue = makeIssue({ status: 'Accepted' });
|
|
208
|
+
const event = makeStatusChangedEvent(issue, 'Accepted', 'Delivered');
|
|
209
|
+
await bus.publish(event);
|
|
210
|
+
await waitForProcessing();
|
|
211
|
+
expect(deps.dispatchWork).not.toHaveBeenCalled();
|
|
212
|
+
await governor.stop();
|
|
213
|
+
});
|
|
214
|
+
it('skips issues with active sessions', async () => {
|
|
215
|
+
const bus = new InMemoryEventBus();
|
|
216
|
+
const deps = makeMockDeps({
|
|
217
|
+
hasActiveSession: vi.fn().mockResolvedValue(true),
|
|
218
|
+
});
|
|
219
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
220
|
+
await governor.start();
|
|
221
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
222
|
+
const event = makeStatusChangedEvent(issue, 'Backlog');
|
|
223
|
+
await bus.publish(event);
|
|
224
|
+
await waitForProcessing();
|
|
225
|
+
expect(deps.dispatchWork).not.toHaveBeenCalled();
|
|
226
|
+
await governor.stop();
|
|
227
|
+
});
|
|
228
|
+
it('skips held issues', async () => {
|
|
229
|
+
const bus = new InMemoryEventBus();
|
|
230
|
+
const deps = makeMockDeps({
|
|
231
|
+
isHeld: vi.fn().mockResolvedValue(true),
|
|
232
|
+
});
|
|
233
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
234
|
+
await governor.start();
|
|
235
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
236
|
+
const event = makeStatusChangedEvent(issue, 'Backlog');
|
|
237
|
+
await bus.publish(event);
|
|
238
|
+
await waitForProcessing();
|
|
239
|
+
expect(deps.dispatchWork).not.toHaveBeenCalled();
|
|
240
|
+
await governor.stop();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Event processing — session-completed
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
describe('EventDrivenGovernor — session-completed events', () => {
|
|
247
|
+
it('evaluates the issue after session completion', async () => {
|
|
248
|
+
const bus = new InMemoryEventBus();
|
|
249
|
+
const deps = makeMockDeps();
|
|
250
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
251
|
+
await governor.start();
|
|
252
|
+
const issue = makeIssue({ status: 'Finished' });
|
|
253
|
+
const event = makeSessionCompletedEvent(issue, 'success');
|
|
254
|
+
await bus.publish(event);
|
|
255
|
+
await waitForProcessing();
|
|
256
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-qa');
|
|
257
|
+
await governor.stop();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Event processing — poll-snapshot
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
describe('EventDrivenGovernor — poll-snapshot events', () => {
|
|
264
|
+
it('evaluates the issue from a poll snapshot', async () => {
|
|
265
|
+
const bus = new InMemoryEventBus();
|
|
266
|
+
const deps = makeMockDeps();
|
|
267
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
268
|
+
await governor.start();
|
|
269
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
270
|
+
const event = makePollSnapshotEvent(issue);
|
|
271
|
+
await bus.publish(event);
|
|
272
|
+
await waitForProcessing();
|
|
273
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
274
|
+
await governor.stop();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Event processing — comment-added (override directives)
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
describe('EventDrivenGovernor — comment-added events', () => {
|
|
281
|
+
it('sets hold override when HOLD comment is received', async () => {
|
|
282
|
+
const bus = new InMemoryEventBus();
|
|
283
|
+
const deps = makeMockDeps();
|
|
284
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
285
|
+
await governor.start();
|
|
286
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
287
|
+
const event = makeCommentEvent(issue, 'HOLD');
|
|
288
|
+
await bus.publish(event);
|
|
289
|
+
await waitForProcessing();
|
|
290
|
+
const state = await getOverrideState(issue.id);
|
|
291
|
+
expect(state).not.toBeNull();
|
|
292
|
+
expect(state.directive.type).toBe('hold');
|
|
293
|
+
// HOLD should not trigger dispatch
|
|
294
|
+
expect(deps.dispatchWork).not.toHaveBeenCalled();
|
|
295
|
+
await governor.stop();
|
|
296
|
+
});
|
|
297
|
+
it('sets hold override with reason', async () => {
|
|
298
|
+
const bus = new InMemoryEventBus();
|
|
299
|
+
const deps = makeMockDeps();
|
|
300
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
301
|
+
await governor.start();
|
|
302
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
303
|
+
const event = makeCommentEvent(issue, 'HOLD - waiting for design review');
|
|
304
|
+
await bus.publish(event);
|
|
305
|
+
await waitForProcessing();
|
|
306
|
+
const state = await getOverrideState(issue.id);
|
|
307
|
+
expect(state).not.toBeNull();
|
|
308
|
+
expect(state.directive.type).toBe('hold');
|
|
309
|
+
expect(state.directive.reason).toBe('waiting for design review');
|
|
310
|
+
await governor.stop();
|
|
311
|
+
});
|
|
312
|
+
it('clears override and re-evaluates on RESUME', async () => {
|
|
313
|
+
const bus = new InMemoryEventBus();
|
|
314
|
+
const deps = makeMockDeps();
|
|
315
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
316
|
+
await governor.start();
|
|
317
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
318
|
+
// First, set a HOLD
|
|
319
|
+
const holdEvent = makeCommentEvent(issue, 'HOLD', 'comment-hold');
|
|
320
|
+
await bus.publish(holdEvent);
|
|
321
|
+
await waitForProcessing();
|
|
322
|
+
// Verify hold is set
|
|
323
|
+
let state = await getOverrideState(issue.id);
|
|
324
|
+
expect(state).not.toBeNull();
|
|
325
|
+
expect(state.directive.type).toBe('hold');
|
|
326
|
+
// Now RESUME
|
|
327
|
+
const resumeEvent = makeCommentEvent(issue, 'RESUME', 'comment-resume');
|
|
328
|
+
await bus.publish(resumeEvent);
|
|
329
|
+
await waitForProcessing();
|
|
330
|
+
// Override should be cleared
|
|
331
|
+
state = await getOverrideState(issue.id);
|
|
332
|
+
expect(state).toBeNull();
|
|
333
|
+
// Issue should have been re-evaluated and dispatched
|
|
334
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
335
|
+
await governor.stop();
|
|
336
|
+
});
|
|
337
|
+
it('sets priority override when PRIORITY comment is received', async () => {
|
|
338
|
+
const bus = new InMemoryEventBus();
|
|
339
|
+
const deps = makeMockDeps();
|
|
340
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
341
|
+
await governor.start();
|
|
342
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
343
|
+
const event = makeCommentEvent(issue, 'PRIORITY: high');
|
|
344
|
+
await bus.publish(event);
|
|
345
|
+
await waitForProcessing();
|
|
346
|
+
const state = await getOverrideState(issue.id);
|
|
347
|
+
expect(state).not.toBeNull();
|
|
348
|
+
expect(state.directive.type).toBe('priority');
|
|
349
|
+
expect(state.directive.priority).toBe('high');
|
|
350
|
+
await governor.stop();
|
|
351
|
+
});
|
|
352
|
+
it('evaluates issue when comment has no directive', async () => {
|
|
353
|
+
const bus = new InMemoryEventBus();
|
|
354
|
+
const deps = makeMockDeps();
|
|
355
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
356
|
+
await governor.start();
|
|
357
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
358
|
+
const event = makeCommentEvent(issue, 'Just a regular comment with no directive');
|
|
359
|
+
await bus.publish(event);
|
|
360
|
+
await waitForProcessing();
|
|
361
|
+
// Should still evaluate the issue
|
|
362
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
363
|
+
await governor.stop();
|
|
364
|
+
});
|
|
365
|
+
it('handles SKIP QA directive', async () => {
|
|
366
|
+
const bus = new InMemoryEventBus();
|
|
367
|
+
const deps = makeMockDeps();
|
|
368
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
369
|
+
await governor.start();
|
|
370
|
+
const issue = makeIssue({ status: 'Finished' });
|
|
371
|
+
const event = makeCommentEvent(issue, 'SKIP QA');
|
|
372
|
+
await bus.publish(event);
|
|
373
|
+
await waitForProcessing();
|
|
374
|
+
const state = await getOverrideState(issue.id);
|
|
375
|
+
expect(state).not.toBeNull();
|
|
376
|
+
expect(state.directive.type).toBe('skip-qa');
|
|
377
|
+
await governor.stop();
|
|
378
|
+
});
|
|
379
|
+
it('handles DECOMPOSE directive', async () => {
|
|
380
|
+
const bus = new InMemoryEventBus();
|
|
381
|
+
const deps = makeMockDeps();
|
|
382
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
383
|
+
await governor.start();
|
|
384
|
+
const issue = makeIssue({ status: 'Rejected' });
|
|
385
|
+
const event = makeCommentEvent(issue, 'DECOMPOSE');
|
|
386
|
+
await bus.publish(event);
|
|
387
|
+
await waitForProcessing();
|
|
388
|
+
const state = await getOverrideState(issue.id);
|
|
389
|
+
expect(state).not.toBeNull();
|
|
390
|
+
expect(state.directive.type).toBe('decompose');
|
|
391
|
+
await governor.stop();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Deduplication
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
describe('EventDrivenGovernor — deduplication', () => {
|
|
398
|
+
it('skips duplicate events when deduplicator is configured', async () => {
|
|
399
|
+
const bus = new InMemoryEventBus();
|
|
400
|
+
const deduplicator = new InMemoryEventDeduplicator();
|
|
401
|
+
const deps = makeMockDeps();
|
|
402
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { deduplicator }), deps);
|
|
403
|
+
await governor.start();
|
|
404
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
405
|
+
const event = makeStatusChangedEvent(issue, 'Backlog');
|
|
406
|
+
// Publish the same event twice
|
|
407
|
+
await bus.publish(event);
|
|
408
|
+
await bus.publish(event);
|
|
409
|
+
await waitForProcessing();
|
|
410
|
+
// Only dispatched once due to deduplication
|
|
411
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(1);
|
|
412
|
+
await governor.stop();
|
|
413
|
+
});
|
|
414
|
+
it('processes all events when no deduplicator is configured', async () => {
|
|
415
|
+
const bus = new InMemoryEventBus();
|
|
416
|
+
const deps = makeMockDeps();
|
|
417
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), // no deduplicator
|
|
418
|
+
deps);
|
|
419
|
+
await governor.start();
|
|
420
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
421
|
+
const event = makeStatusChangedEvent(issue, 'Backlog');
|
|
422
|
+
// Publish the same event twice
|
|
423
|
+
await bus.publish(event);
|
|
424
|
+
await bus.publish(event);
|
|
425
|
+
await waitForProcessing();
|
|
426
|
+
// Both are processed since there is no deduplicator
|
|
427
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(2);
|
|
428
|
+
await governor.stop();
|
|
429
|
+
});
|
|
430
|
+
it('acknowledges duplicate events on the bus', async () => {
|
|
431
|
+
const bus = new InMemoryEventBus();
|
|
432
|
+
const deduplicator = new InMemoryEventDeduplicator();
|
|
433
|
+
const deps = makeMockDeps();
|
|
434
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { deduplicator }), deps);
|
|
435
|
+
await governor.start();
|
|
436
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
437
|
+
const event = makeStatusChangedEvent(issue, 'Backlog');
|
|
438
|
+
const id1 = await bus.publish(event);
|
|
439
|
+
const id2 = await bus.publish(event);
|
|
440
|
+
await waitForProcessing();
|
|
441
|
+
// Both events should be acked (even the duplicate)
|
|
442
|
+
expect(bus.isAcked(id1)).toBe(true);
|
|
443
|
+
expect(bus.isAcked(id2)).toBe(true);
|
|
444
|
+
await governor.stop();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
// Event acknowledgement
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
describe('EventDrivenGovernor — event acknowledgement', () => {
|
|
451
|
+
it('acknowledges processed events', async () => {
|
|
452
|
+
const bus = new InMemoryEventBus();
|
|
453
|
+
const deps = makeMockDeps();
|
|
454
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
455
|
+
await governor.start();
|
|
456
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
457
|
+
const event = makeStatusChangedEvent(issue, 'Backlog');
|
|
458
|
+
const eventId = await bus.publish(event);
|
|
459
|
+
await waitForProcessing();
|
|
460
|
+
expect(bus.isAcked(eventId)).toBe(true);
|
|
461
|
+
await governor.stop();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Error handling
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
describe('EventDrivenGovernor — error handling', () => {
|
|
468
|
+
it('continues processing after a dispatch error', async () => {
|
|
469
|
+
const bus = new InMemoryEventBus();
|
|
470
|
+
const dispatchWork = vi.fn()
|
|
471
|
+
.mockRejectedValueOnce(new Error('Dispatch failed'))
|
|
472
|
+
.mockResolvedValueOnce(undefined);
|
|
473
|
+
const deps = makeMockDeps({ dispatchWork });
|
|
474
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
475
|
+
await governor.start();
|
|
476
|
+
const issue1 = makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' });
|
|
477
|
+
const issue2 = makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Backlog' });
|
|
478
|
+
await bus.publish(makeStatusChangedEvent(issue1, 'Backlog'));
|
|
479
|
+
await bus.publish(makeStatusChangedEvent(issue2, 'Backlog'));
|
|
480
|
+
await waitForProcessing();
|
|
481
|
+
// Both events attempted, second one succeeded
|
|
482
|
+
expect(dispatchWork).toHaveBeenCalledTimes(2);
|
|
483
|
+
await governor.stop();
|
|
484
|
+
});
|
|
485
|
+
it('continues processing after a context-gathering error', async () => {
|
|
486
|
+
const bus = new InMemoryEventBus();
|
|
487
|
+
const hasActiveSession = vi.fn()
|
|
488
|
+
.mockRejectedValueOnce(new Error('Redis down'))
|
|
489
|
+
.mockResolvedValueOnce(false);
|
|
490
|
+
const deps = makeMockDeps({ hasActiveSession });
|
|
491
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
492
|
+
await governor.start();
|
|
493
|
+
const issue1 = makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' });
|
|
494
|
+
const issue2 = makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Backlog' });
|
|
495
|
+
await bus.publish(makeStatusChangedEvent(issue1, 'Backlog'));
|
|
496
|
+
await bus.publish(makeStatusChangedEvent(issue2, 'Backlog'));
|
|
497
|
+
await waitForProcessing();
|
|
498
|
+
// First event failed during context gathering, second event dispatched
|
|
499
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(1);
|
|
500
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-2' }), 'trigger-development');
|
|
501
|
+
await governor.stop();
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Multiple events in sequence
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
describe('EventDrivenGovernor — sequential event processing', () => {
|
|
508
|
+
it('processes multiple different events in order', async () => {
|
|
509
|
+
const bus = new InMemoryEventBus();
|
|
510
|
+
const deps = makeMockDeps();
|
|
511
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
512
|
+
await governor.start();
|
|
513
|
+
const issue1 = makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' });
|
|
514
|
+
const issue2 = makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Finished' });
|
|
515
|
+
const issue3 = makeIssue({ id: 'issue-3', identifier: 'SUP-3', status: 'Delivered' });
|
|
516
|
+
await bus.publish(makeStatusChangedEvent(issue1, 'Backlog'));
|
|
517
|
+
await bus.publish(makeStatusChangedEvent(issue2, 'Finished'));
|
|
518
|
+
await bus.publish(makeStatusChangedEvent(issue3, 'Delivered'));
|
|
519
|
+
await waitForProcessing();
|
|
520
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(3);
|
|
521
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
522
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-2' }), 'trigger-qa');
|
|
523
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-3' }), 'trigger-acceptance');
|
|
524
|
+
await governor.stop();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
// Poll sweep
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
describe('EventDrivenGovernor — pollSweep', () => {
|
|
531
|
+
it('publishes poll-snapshot events for all issues in all projects', async () => {
|
|
532
|
+
const bus = new InMemoryEventBus();
|
|
533
|
+
const issues = [
|
|
534
|
+
makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
535
|
+
makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Finished' }),
|
|
536
|
+
];
|
|
537
|
+
const deps = makeMockDeps({
|
|
538
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
539
|
+
});
|
|
540
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { projects: ['ProjectA'] }), deps);
|
|
541
|
+
await governor.start();
|
|
542
|
+
await governor.pollSweep();
|
|
543
|
+
await waitForProcessing();
|
|
544
|
+
// The poll sweep publishes events that get processed by the event loop
|
|
545
|
+
expect(deps.listIssues).toHaveBeenCalledWith('ProjectA');
|
|
546
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
547
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-2' }), 'trigger-qa');
|
|
548
|
+
await governor.stop();
|
|
549
|
+
});
|
|
550
|
+
it('polls multiple projects', async () => {
|
|
551
|
+
const bus = new InMemoryEventBus();
|
|
552
|
+
const listIssues = vi.fn().mockImplementation((project) => {
|
|
553
|
+
if (project === 'ProjectA') {
|
|
554
|
+
return Promise.resolve([makeIssue({ id: 'a-1', identifier: 'A-1', status: 'Backlog' })]);
|
|
555
|
+
}
|
|
556
|
+
if (project === 'ProjectB') {
|
|
557
|
+
return Promise.resolve([makeIssue({ id: 'b-1', identifier: 'B-1', status: 'Finished' })]);
|
|
558
|
+
}
|
|
559
|
+
return Promise.resolve([]);
|
|
560
|
+
});
|
|
561
|
+
const deps = makeMockDeps({ listIssues });
|
|
562
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { projects: ['ProjectA', 'ProjectB'] }), deps);
|
|
563
|
+
await governor.start();
|
|
564
|
+
await governor.pollSweep();
|
|
565
|
+
await waitForProcessing();
|
|
566
|
+
expect(listIssues).toHaveBeenCalledWith('ProjectA');
|
|
567
|
+
expect(listIssues).toHaveBeenCalledWith('ProjectB');
|
|
568
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'a-1' }), 'trigger-development');
|
|
569
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'b-1' }), 'trigger-qa');
|
|
570
|
+
await governor.stop();
|
|
571
|
+
});
|
|
572
|
+
it('handles errors in poll sweep for individual projects', async () => {
|
|
573
|
+
const bus = new InMemoryEventBus();
|
|
574
|
+
const listIssues = vi.fn()
|
|
575
|
+
.mockRejectedValueOnce(new Error('API timeout'))
|
|
576
|
+
.mockResolvedValueOnce([makeIssue({ id: 'b-1', status: 'Backlog' })]);
|
|
577
|
+
const deps = makeMockDeps({ listIssues });
|
|
578
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { projects: ['ProjectA', 'ProjectB'] }), deps);
|
|
579
|
+
await governor.start();
|
|
580
|
+
// Should not throw even though ProjectA fails
|
|
581
|
+
await governor.pollSweep();
|
|
582
|
+
await waitForProcessing();
|
|
583
|
+
// ProjectB's issue should still be published and processed
|
|
584
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'b-1' }), 'trigger-development');
|
|
585
|
+
await governor.stop();
|
|
586
|
+
});
|
|
587
|
+
it('poll sweep events are deduplicated', async () => {
|
|
588
|
+
const bus = new InMemoryEventBus();
|
|
589
|
+
const deduplicator = new InMemoryEventDeduplicator();
|
|
590
|
+
const issues = [makeIssue({ id: 'issue-1', status: 'Backlog' })];
|
|
591
|
+
const deps = makeMockDeps({
|
|
592
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
593
|
+
});
|
|
594
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { deduplicator, projects: ['ProjectA'] }), deps);
|
|
595
|
+
await governor.start();
|
|
596
|
+
// First sweep
|
|
597
|
+
await governor.pollSweep();
|
|
598
|
+
await waitForProcessing();
|
|
599
|
+
// Second sweep immediately — same issue+status should be deduped
|
|
600
|
+
await governor.pollSweep();
|
|
601
|
+
await waitForProcessing();
|
|
602
|
+
// Only dispatched once due to deduplication
|
|
603
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(1);
|
|
604
|
+
await governor.stop();
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
// Polling timer
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
describe('EventDrivenGovernor — poll timer', () => {
|
|
611
|
+
beforeEach(() => {
|
|
612
|
+
vi.useFakeTimers();
|
|
613
|
+
});
|
|
614
|
+
afterEach(() => {
|
|
615
|
+
vi.useRealTimers();
|
|
616
|
+
});
|
|
617
|
+
it('starts poll timer when enablePolling is true', async () => {
|
|
618
|
+
const bus = new InMemoryEventBus();
|
|
619
|
+
const deps = makeMockDeps({
|
|
620
|
+
listIssues: vi.fn().mockResolvedValue([]),
|
|
621
|
+
});
|
|
622
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { enablePolling: true, pollIntervalMs: 5000 }), deps);
|
|
623
|
+
await governor.start();
|
|
624
|
+
// Advance past the poll interval
|
|
625
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
626
|
+
expect(deps.listIssues).toHaveBeenCalled();
|
|
627
|
+
await governor.stop();
|
|
628
|
+
});
|
|
629
|
+
it('does not start poll timer when enablePolling is false', async () => {
|
|
630
|
+
const bus = new InMemoryEventBus();
|
|
631
|
+
const deps = makeMockDeps();
|
|
632
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { enablePolling: false }), deps);
|
|
633
|
+
await governor.start();
|
|
634
|
+
await vi.advanceTimersByTimeAsync(600_000); // 10 minutes
|
|
635
|
+
// listIssues is only called by pollSweep, which should not have fired
|
|
636
|
+
expect(deps.listIssues).not.toHaveBeenCalled();
|
|
637
|
+
await governor.stop();
|
|
638
|
+
});
|
|
639
|
+
it('stop clears the poll timer', async () => {
|
|
640
|
+
const bus = new InMemoryEventBus();
|
|
641
|
+
const deps = makeMockDeps({
|
|
642
|
+
listIssues: vi.fn().mockResolvedValue([]),
|
|
643
|
+
});
|
|
644
|
+
const governor = new EventDrivenGovernor(makeConfig(bus, { enablePolling: true, pollIntervalMs: 5000 }), deps);
|
|
645
|
+
await governor.start();
|
|
646
|
+
await governor.stop();
|
|
647
|
+
// Advance well past the poll interval — no sweep should fire
|
|
648
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
649
|
+
expect(deps.listIssues).not.toHaveBeenCalled();
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
// Context gathering
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
describe('EventDrivenGovernor — context gathering', () => {
|
|
656
|
+
it('calls all dependency checks for an issue', async () => {
|
|
657
|
+
const bus = new InMemoryEventBus();
|
|
658
|
+
const deps = makeMockDeps();
|
|
659
|
+
const governor = new EventDrivenGovernor(makeConfig(bus), deps);
|
|
660
|
+
await governor.start();
|
|
661
|
+
const issue = makeIssue({ id: 'issue-1', status: 'Backlog' });
|
|
662
|
+
await bus.publish(makeStatusChangedEvent(issue, 'Backlog'));
|
|
663
|
+
await waitForProcessing();
|
|
664
|
+
expect(deps.hasActiveSession).toHaveBeenCalledWith('issue-1');
|
|
665
|
+
expect(deps.isWithinCooldown).toHaveBeenCalledWith('issue-1');
|
|
666
|
+
expect(deps.isParentIssue).toHaveBeenCalledWith('issue-1');
|
|
667
|
+
expect(deps.isHeld).toHaveBeenCalledWith('issue-1');
|
|
668
|
+
expect(deps.getWorkflowStrategy).toHaveBeenCalledWith('issue-1');
|
|
669
|
+
expect(deps.isResearchCompleted).toHaveBeenCalledWith('issue-1');
|
|
670
|
+
expect(deps.isBacklogCreationCompleted).toHaveBeenCalledWith('issue-1');
|
|
671
|
+
await governor.stop();
|
|
672
|
+
});
|
|
673
|
+
});
|