@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.
Files changed (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/src/config/index.d.ts +3 -0
  4. package/dist/src/config/index.d.ts.map +1 -0
  5. package/dist/src/config/index.js +1 -0
  6. package/dist/src/config/repository-config.d.ts +44 -0
  7. package/dist/src/config/repository-config.d.ts.map +1 -0
  8. package/dist/src/config/repository-config.js +88 -0
  9. package/dist/src/config/repository-config.test.d.ts +2 -0
  10. package/dist/src/config/repository-config.test.d.ts.map +1 -0
  11. package/dist/src/config/repository-config.test.js +249 -0
  12. package/dist/src/deployment/deployment-checker.d.ts +110 -0
  13. package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
  14. package/dist/src/deployment/deployment-checker.js +242 -0
  15. package/dist/src/deployment/index.d.ts +3 -0
  16. package/dist/src/deployment/index.d.ts.map +1 -0
  17. package/dist/src/deployment/index.js +2 -0
  18. package/dist/src/frontend/index.d.ts +2 -0
  19. package/dist/src/frontend/index.d.ts.map +1 -0
  20. package/dist/src/frontend/index.js +1 -0
  21. package/dist/src/frontend/types.d.ts +106 -0
  22. package/dist/src/frontend/types.d.ts.map +1 -0
  23. package/dist/src/frontend/types.js +11 -0
  24. package/dist/src/governor/decision-engine.d.ts +52 -0
  25. package/dist/src/governor/decision-engine.d.ts.map +1 -0
  26. package/dist/src/governor/decision-engine.js +220 -0
  27. package/dist/src/governor/decision-engine.test.d.ts +2 -0
  28. package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
  29. package/dist/src/governor/decision-engine.test.js +629 -0
  30. package/dist/src/governor/event-bus.d.ts +43 -0
  31. package/dist/src/governor/event-bus.d.ts.map +1 -0
  32. package/dist/src/governor/event-bus.js +8 -0
  33. package/dist/src/governor/event-deduplicator.d.ts +43 -0
  34. package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
  35. package/dist/src/governor/event-deduplicator.js +53 -0
  36. package/dist/src/governor/event-driven-governor.d.ts +131 -0
  37. package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
  38. package/dist/src/governor/event-driven-governor.js +379 -0
  39. package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
  40. package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
  41. package/dist/src/governor/event-driven-governor.test.js +673 -0
  42. package/dist/src/governor/event-types.d.ts +78 -0
  43. package/dist/src/governor/event-types.d.ts.map +1 -0
  44. package/dist/src/governor/event-types.js +32 -0
  45. package/dist/src/governor/governor-types.d.ts +82 -0
  46. package/dist/src/governor/governor-types.d.ts.map +1 -0
  47. package/dist/src/governor/governor-types.js +21 -0
  48. package/dist/src/governor/governor.d.ts +100 -0
  49. package/dist/src/governor/governor.d.ts.map +1 -0
  50. package/dist/src/governor/governor.js +262 -0
  51. package/dist/src/governor/governor.test.d.ts +2 -0
  52. package/dist/src/governor/governor.test.d.ts.map +1 -0
  53. package/dist/src/governor/governor.test.js +514 -0
  54. package/dist/src/governor/human-touchpoints.d.ts +131 -0
  55. package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
  56. package/dist/src/governor/human-touchpoints.js +251 -0
  57. package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
  58. package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
  59. package/dist/src/governor/human-touchpoints.test.js +366 -0
  60. package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
  61. package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
  62. package/dist/src/governor/in-memory-event-bus.js +79 -0
  63. package/dist/src/governor/index.d.ts +14 -0
  64. package/dist/src/governor/index.d.ts.map +1 -0
  65. package/dist/src/governor/index.js +13 -0
  66. package/dist/src/governor/override-parser.d.ts +60 -0
  67. package/dist/src/governor/override-parser.d.ts.map +1 -0
  68. package/dist/src/governor/override-parser.js +98 -0
  69. package/dist/src/governor/override-parser.test.d.ts +2 -0
  70. package/dist/src/governor/override-parser.test.d.ts.map +1 -0
  71. package/dist/src/governor/override-parser.test.js +312 -0
  72. package/dist/src/governor/platform-adapter.d.ts +69 -0
  73. package/dist/src/governor/platform-adapter.d.ts.map +1 -0
  74. package/dist/src/governor/platform-adapter.js +11 -0
  75. package/dist/src/governor/processing-state.d.ts +66 -0
  76. package/dist/src/governor/processing-state.d.ts.map +1 -0
  77. package/dist/src/governor/processing-state.js +43 -0
  78. package/dist/src/governor/processing-state.test.d.ts +2 -0
  79. package/dist/src/governor/processing-state.test.d.ts.map +1 -0
  80. package/dist/src/governor/processing-state.test.js +96 -0
  81. package/dist/src/governor/top-of-funnel.d.ts +118 -0
  82. package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
  83. package/dist/src/governor/top-of-funnel.js +168 -0
  84. package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
  85. package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
  86. package/dist/src/governor/top-of-funnel.test.js +331 -0
  87. package/dist/src/index.d.ts +11 -0
  88. package/dist/src/index.d.ts.map +1 -0
  89. package/dist/src/index.js +10 -0
  90. package/dist/src/linear-cli.d.ts +38 -0
  91. package/dist/src/linear-cli.d.ts.map +1 -0
  92. package/dist/src/linear-cli.js +674 -0
  93. package/dist/src/logger.d.ts +117 -0
  94. package/dist/src/logger.d.ts.map +1 -0
  95. package/dist/src/logger.js +430 -0
  96. package/dist/src/manifest/generate.d.ts +20 -0
  97. package/dist/src/manifest/generate.d.ts.map +1 -0
  98. package/dist/src/manifest/generate.js +65 -0
  99. package/dist/src/manifest/index.d.ts +4 -0
  100. package/dist/src/manifest/index.d.ts.map +1 -0
  101. package/dist/src/manifest/index.js +2 -0
  102. package/dist/src/manifest/route-manifest.d.ts +34 -0
  103. package/dist/src/manifest/route-manifest.d.ts.map +1 -0
  104. package/dist/src/manifest/route-manifest.js +148 -0
  105. package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
  106. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
  107. package/dist/src/orchestrator/activity-emitter.js +306 -0
  108. package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
  109. package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
  110. package/dist/src/orchestrator/api-activity-emitter.js +417 -0
  111. package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
  112. package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
  113. package/dist/src/orchestrator/heartbeat-writer.js +137 -0
  114. package/dist/src/orchestrator/index.d.ts +20 -0
  115. package/dist/src/orchestrator/index.d.ts.map +1 -0
  116. package/dist/src/orchestrator/index.js +22 -0
  117. package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
  118. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
  119. package/dist/src/orchestrator/log-analyzer.js +572 -0
  120. package/dist/src/orchestrator/log-config.d.ts +39 -0
  121. package/dist/src/orchestrator/log-config.d.ts.map +1 -0
  122. package/dist/src/orchestrator/log-config.js +45 -0
  123. package/dist/src/orchestrator/orchestrator.d.ts +316 -0
  124. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
  125. package/dist/src/orchestrator/orchestrator.js +3290 -0
  126. package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
  127. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
  128. package/dist/src/orchestrator/parse-work-result.js +135 -0
  129. package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
  130. package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
  131. package/dist/src/orchestrator/parse-work-result.test.js +234 -0
  132. package/dist/src/orchestrator/progress-logger.d.ts +72 -0
  133. package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
  134. package/dist/src/orchestrator/progress-logger.js +135 -0
  135. package/dist/src/orchestrator/session-logger.d.ts +159 -0
  136. package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
  137. package/dist/src/orchestrator/session-logger.js +275 -0
  138. package/dist/src/orchestrator/state-recovery.d.ts +96 -0
  139. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
  140. package/dist/src/orchestrator/state-recovery.js +302 -0
  141. package/dist/src/orchestrator/state-types.d.ts +165 -0
  142. package/dist/src/orchestrator/state-types.d.ts.map +1 -0
  143. package/dist/src/orchestrator/state-types.js +7 -0
  144. package/dist/src/orchestrator/stream-parser.d.ts +151 -0
  145. package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
  146. package/dist/src/orchestrator/stream-parser.js +137 -0
  147. package/dist/src/orchestrator/types.d.ts +232 -0
  148. package/dist/src/orchestrator/types.d.ts.map +1 -0
  149. package/dist/src/orchestrator/types.js +4 -0
  150. package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
  151. package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
  152. package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
  153. package/dist/src/providers/a2a-auth.d.ts +81 -0
  154. package/dist/src/providers/a2a-auth.d.ts.map +1 -0
  155. package/dist/src/providers/a2a-auth.js +188 -0
  156. package/dist/src/providers/a2a-auth.test.d.ts +2 -0
  157. package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
  158. package/dist/src/providers/a2a-auth.test.js +232 -0
  159. package/dist/src/providers/a2a-provider.d.ts +254 -0
  160. package/dist/src/providers/a2a-provider.d.ts.map +1 -0
  161. package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
  162. package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
  163. package/dist/src/providers/a2a-provider.integration.test.js +665 -0
  164. package/dist/src/providers/a2a-provider.js +811 -0
  165. package/dist/src/providers/a2a-provider.test.d.ts +2 -0
  166. package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
  167. package/dist/src/providers/a2a-provider.test.js +681 -0
  168. package/dist/src/providers/amp-provider.d.ts +20 -0
  169. package/dist/src/providers/amp-provider.d.ts.map +1 -0
  170. package/dist/src/providers/amp-provider.js +24 -0
  171. package/dist/src/providers/claude-provider.d.ts +18 -0
  172. package/dist/src/providers/claude-provider.d.ts.map +1 -0
  173. package/dist/src/providers/claude-provider.js +437 -0
  174. package/dist/src/providers/codex-provider.d.ts +133 -0
  175. package/dist/src/providers/codex-provider.d.ts.map +1 -0
  176. package/dist/src/providers/codex-provider.js +381 -0
  177. package/dist/src/providers/codex-provider.test.d.ts +2 -0
  178. package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
  179. package/dist/src/providers/codex-provider.test.js +387 -0
  180. package/dist/src/providers/index.d.ts +44 -0
  181. package/dist/src/providers/index.d.ts.map +1 -0
  182. package/dist/src/providers/index.js +85 -0
  183. package/dist/src/providers/spring-ai-provider.d.ts +90 -0
  184. package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
  185. package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
  186. package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
  187. package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
  188. package/dist/src/providers/spring-ai-provider.js +317 -0
  189. package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
  190. package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
  191. package/dist/src/providers/spring-ai-provider.test.js +200 -0
  192. package/dist/src/providers/types.d.ts +165 -0
  193. package/dist/src/providers/types.d.ts.map +1 -0
  194. package/dist/src/providers/types.js +13 -0
  195. package/dist/src/templates/adapters.d.ts +51 -0
  196. package/dist/src/templates/adapters.d.ts.map +1 -0
  197. package/dist/src/templates/adapters.js +104 -0
  198. package/dist/src/templates/adapters.test.d.ts +2 -0
  199. package/dist/src/templates/adapters.test.d.ts.map +1 -0
  200. package/dist/src/templates/adapters.test.js +165 -0
  201. package/dist/src/templates/agent-definition.d.ts +85 -0
  202. package/dist/src/templates/agent-definition.d.ts.map +1 -0
  203. package/dist/src/templates/agent-definition.js +97 -0
  204. package/dist/src/templates/agent-definition.test.d.ts +2 -0
  205. package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
  206. package/dist/src/templates/agent-definition.test.js +209 -0
  207. package/dist/src/templates/index.d.ts +14 -0
  208. package/dist/src/templates/index.d.ts.map +1 -0
  209. package/dist/src/templates/index.js +11 -0
  210. package/dist/src/templates/loader.d.ts +41 -0
  211. package/dist/src/templates/loader.d.ts.map +1 -0
  212. package/dist/src/templates/loader.js +114 -0
  213. package/dist/src/templates/registry.d.ts +80 -0
  214. package/dist/src/templates/registry.d.ts.map +1 -0
  215. package/dist/src/templates/registry.js +177 -0
  216. package/dist/src/templates/registry.test.d.ts +2 -0
  217. package/dist/src/templates/registry.test.d.ts.map +1 -0
  218. package/dist/src/templates/registry.test.js +198 -0
  219. package/dist/src/templates/renderer.d.ts +29 -0
  220. package/dist/src/templates/renderer.d.ts.map +1 -0
  221. package/dist/src/templates/renderer.js +35 -0
  222. package/dist/src/templates/strategy-templates.test.d.ts +2 -0
  223. package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
  224. package/dist/src/templates/strategy-templates.test.js +619 -0
  225. package/dist/src/templates/types.d.ts +233 -0
  226. package/dist/src/templates/types.d.ts.map +1 -0
  227. package/dist/src/templates/types.js +127 -0
  228. package/dist/src/templates/types.test.d.ts +2 -0
  229. package/dist/src/templates/types.test.d.ts.map +1 -0
  230. package/dist/src/templates/types.test.js +232 -0
  231. package/dist/src/tools/index.d.ts +6 -0
  232. package/dist/src/tools/index.d.ts.map +1 -0
  233. package/dist/src/tools/index.js +3 -0
  234. package/dist/src/tools/linear-runner.d.ts +34 -0
  235. package/dist/src/tools/linear-runner.d.ts.map +1 -0
  236. package/dist/src/tools/linear-runner.js +700 -0
  237. package/dist/src/tools/plugins/linear.d.ts +9 -0
  238. package/dist/src/tools/plugins/linear.d.ts.map +1 -0
  239. package/dist/src/tools/plugins/linear.js +138 -0
  240. package/dist/src/tools/registry.d.ts +9 -0
  241. package/dist/src/tools/registry.d.ts.map +1 -0
  242. package/dist/src/tools/registry.js +18 -0
  243. package/dist/src/tools/types.d.ts +18 -0
  244. package/dist/src/tools/types.d.ts.map +1 -0
  245. package/dist/src/tools/types.js +1 -0
  246. 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
+ });