@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,514 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { WorkflowGovernor } from './governor.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function makeIssue(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
id: 'issue-1',
|
|
9
|
+
identifier: 'SUP-100',
|
|
10
|
+
title: 'Test Issue',
|
|
11
|
+
description: undefined,
|
|
12
|
+
status: 'Backlog',
|
|
13
|
+
labels: [],
|
|
14
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create mock GovernorDependencies with sensible defaults.
|
|
20
|
+
* All callbacks return "no blockers" so issues are actionable by default.
|
|
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(overrides = {}) {
|
|
39
|
+
return {
|
|
40
|
+
projects: ['TestProject'],
|
|
41
|
+
scanIntervalMs: 60_000,
|
|
42
|
+
maxConcurrentDispatches: 3,
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// scanOnce — basic behavior
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
describe('WorkflowGovernor.scanOnce', () => {
|
|
50
|
+
it('scans all configured projects', async () => {
|
|
51
|
+
const deps = makeMockDeps();
|
|
52
|
+
const governor = new WorkflowGovernor(makeConfig({ projects: ['Project-A', 'Project-B'] }), deps);
|
|
53
|
+
const results = await governor.scanOnce();
|
|
54
|
+
expect(results).toHaveLength(2);
|
|
55
|
+
expect(results[0].project).toBe('Project-A');
|
|
56
|
+
expect(results[1].project).toBe('Project-B');
|
|
57
|
+
expect(deps.listIssues).toHaveBeenCalledWith('Project-A');
|
|
58
|
+
expect(deps.listIssues).toHaveBeenCalledWith('Project-B');
|
|
59
|
+
});
|
|
60
|
+
it('returns empty results for project with no issues', async () => {
|
|
61
|
+
const deps = makeMockDeps({
|
|
62
|
+
listIssues: vi.fn().mockResolvedValue([]),
|
|
63
|
+
});
|
|
64
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
65
|
+
const results = await governor.scanOnce();
|
|
66
|
+
expect(results).toHaveLength(1);
|
|
67
|
+
expect(results[0].scannedIssues).toBe(0);
|
|
68
|
+
expect(results[0].actionsDispatched).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
it('dispatches development for Backlog issues', async () => {
|
|
71
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
72
|
+
const deps = makeMockDeps({
|
|
73
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
74
|
+
});
|
|
75
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
76
|
+
const results = await governor.scanOnce();
|
|
77
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
78
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
79
|
+
});
|
|
80
|
+
it('dispatches QA for Finished issues', async () => {
|
|
81
|
+
const issue = makeIssue({ status: 'Finished' });
|
|
82
|
+
const deps = makeMockDeps({
|
|
83
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
84
|
+
});
|
|
85
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
86
|
+
const results = await governor.scanOnce();
|
|
87
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
88
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-qa');
|
|
89
|
+
});
|
|
90
|
+
it('dispatches acceptance for Delivered issues', async () => {
|
|
91
|
+
const issue = makeIssue({ status: 'Delivered' });
|
|
92
|
+
const deps = makeMockDeps({
|
|
93
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
94
|
+
});
|
|
95
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
96
|
+
const results = await governor.scanOnce();
|
|
97
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
98
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-acceptance');
|
|
99
|
+
});
|
|
100
|
+
it('dispatches refinement for Rejected issues', async () => {
|
|
101
|
+
const issue = makeIssue({ status: 'Rejected' });
|
|
102
|
+
const deps = makeMockDeps({
|
|
103
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
104
|
+
});
|
|
105
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
106
|
+
const results = await governor.scanOnce();
|
|
107
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
108
|
+
expect(deps.dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-refinement');
|
|
109
|
+
});
|
|
110
|
+
it('skips issues with active sessions', async () => {
|
|
111
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
112
|
+
const deps = makeMockDeps({
|
|
113
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
114
|
+
hasActiveSession: vi.fn().mockResolvedValue(true),
|
|
115
|
+
});
|
|
116
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
117
|
+
const results = await governor.scanOnce();
|
|
118
|
+
expect(results[0].actionsDispatched).toBe(0);
|
|
119
|
+
expect(results[0].skippedReasons.size).toBe(1);
|
|
120
|
+
expect(deps.dispatchWork).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
it('skips held issues', async () => {
|
|
123
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
124
|
+
const deps = makeMockDeps({
|
|
125
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
126
|
+
isHeld: vi.fn().mockResolvedValue(true),
|
|
127
|
+
});
|
|
128
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
129
|
+
const results = await governor.scanOnce();
|
|
130
|
+
expect(results[0].actionsDispatched).toBe(0);
|
|
131
|
+
expect(deps.dispatchWork).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
it('skips issues within cooldown', async () => {
|
|
134
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
135
|
+
const deps = makeMockDeps({
|
|
136
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
137
|
+
isWithinCooldown: vi.fn().mockResolvedValue(true),
|
|
138
|
+
});
|
|
139
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
140
|
+
const results = await governor.scanOnce();
|
|
141
|
+
expect(results[0].actionsDispatched).toBe(0);
|
|
142
|
+
expect(deps.dispatchWork).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// maxConcurrentDispatches limiting
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
describe('WorkflowGovernor.scanOnce — dispatch limit', () => {
|
|
149
|
+
it('respects maxConcurrentDispatches limit', async () => {
|
|
150
|
+
const issues = [
|
|
151
|
+
makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
152
|
+
makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Backlog' }),
|
|
153
|
+
makeIssue({ id: 'issue-3', identifier: 'SUP-3', status: 'Backlog' }),
|
|
154
|
+
makeIssue({ id: 'issue-4', identifier: 'SUP-4', status: 'Backlog' }),
|
|
155
|
+
makeIssue({ id: 'issue-5', identifier: 'SUP-5', status: 'Backlog' }),
|
|
156
|
+
];
|
|
157
|
+
const deps = makeMockDeps({
|
|
158
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
159
|
+
});
|
|
160
|
+
const governor = new WorkflowGovernor(makeConfig({ maxConcurrentDispatches: 2 }), deps);
|
|
161
|
+
const results = await governor.scanOnce();
|
|
162
|
+
expect(results[0].actionsDispatched).toBe(2);
|
|
163
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(2);
|
|
164
|
+
});
|
|
165
|
+
it('dispatches all when under the limit', async () => {
|
|
166
|
+
const issues = [
|
|
167
|
+
makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
168
|
+
makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Finished' }),
|
|
169
|
+
];
|
|
170
|
+
const deps = makeMockDeps({
|
|
171
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
172
|
+
});
|
|
173
|
+
const governor = new WorkflowGovernor(makeConfig({ maxConcurrentDispatches: 5 }), deps);
|
|
174
|
+
const results = await governor.scanOnce();
|
|
175
|
+
expect(results[0].actionsDispatched).toBe(2);
|
|
176
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(2);
|
|
177
|
+
});
|
|
178
|
+
it('counts skipped issues toward scanned but not dispatched', async () => {
|
|
179
|
+
const issues = [
|
|
180
|
+
makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Accepted' }), // terminal
|
|
181
|
+
makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Backlog' }), // actionable
|
|
182
|
+
];
|
|
183
|
+
const deps = makeMockDeps({
|
|
184
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
185
|
+
});
|
|
186
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
187
|
+
const results = await governor.scanOnce();
|
|
188
|
+
expect(results[0].scannedIssues).toBe(2);
|
|
189
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
190
|
+
expect(results[0].skippedReasons.size).toBe(1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Error handling
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
describe('WorkflowGovernor.scanOnce — error handling', () => {
|
|
197
|
+
it('records error when listIssues fails', async () => {
|
|
198
|
+
const deps = makeMockDeps({
|
|
199
|
+
listIssues: vi.fn().mockRejectedValue(new Error('API timeout')),
|
|
200
|
+
});
|
|
201
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
202
|
+
const results = await governor.scanOnce();
|
|
203
|
+
expect(results[0].errors).toHaveLength(1);
|
|
204
|
+
expect(results[0].errors[0].error).toContain('API timeout');
|
|
205
|
+
});
|
|
206
|
+
it('single issue failure does not stop scan of remaining issues', async () => {
|
|
207
|
+
const issues = [
|
|
208
|
+
makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
209
|
+
makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Backlog' }),
|
|
210
|
+
makeIssue({ id: 'issue-3', identifier: 'SUP-3', status: 'Backlog' }),
|
|
211
|
+
];
|
|
212
|
+
const dispatchWork = vi.fn()
|
|
213
|
+
.mockResolvedValueOnce(undefined) // issue-1 succeeds
|
|
214
|
+
.mockRejectedValueOnce(new Error('Dispatch failed')) // issue-2 fails
|
|
215
|
+
.mockResolvedValueOnce(undefined); // issue-3 succeeds
|
|
216
|
+
const deps = makeMockDeps({
|
|
217
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
218
|
+
dispatchWork,
|
|
219
|
+
});
|
|
220
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
221
|
+
const results = await governor.scanOnce();
|
|
222
|
+
// issue-1 and issue-3 dispatched, issue-2 errored
|
|
223
|
+
expect(results[0].actionsDispatched).toBe(2);
|
|
224
|
+
expect(results[0].errors).toHaveLength(1);
|
|
225
|
+
expect(results[0].errors[0].issueId).toBe('SUP-2');
|
|
226
|
+
expect(dispatchWork).toHaveBeenCalledTimes(3);
|
|
227
|
+
});
|
|
228
|
+
it('context gathering failure is caught and recorded', async () => {
|
|
229
|
+
const issue = makeIssue({ status: 'Backlog' });
|
|
230
|
+
const deps = makeMockDeps({
|
|
231
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
232
|
+
hasActiveSession: vi.fn().mockRejectedValue(new Error('Redis down')),
|
|
233
|
+
});
|
|
234
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
235
|
+
const results = await governor.scanOnce();
|
|
236
|
+
expect(results[0].errors).toHaveLength(1);
|
|
237
|
+
expect(results[0].errors[0].error).toContain('Redis down');
|
|
238
|
+
expect(results[0].actionsDispatched).toBe(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Gathers context in parallel
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
describe('WorkflowGovernor.scanOnce — context gathering', () => {
|
|
245
|
+
it('calls all dependency checks for each issue', async () => {
|
|
246
|
+
const issue = makeIssue({ id: 'issue-1', status: 'Backlog' });
|
|
247
|
+
const deps = makeMockDeps({
|
|
248
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
249
|
+
});
|
|
250
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
251
|
+
await governor.scanOnce();
|
|
252
|
+
expect(deps.hasActiveSession).toHaveBeenCalledWith('issue-1');
|
|
253
|
+
expect(deps.isWithinCooldown).toHaveBeenCalledWith('issue-1');
|
|
254
|
+
expect(deps.isParentIssue).toHaveBeenCalledWith('issue-1');
|
|
255
|
+
expect(deps.isHeld).toHaveBeenCalledWith('issue-1');
|
|
256
|
+
expect(deps.getWorkflowStrategy).toHaveBeenCalledWith('issue-1');
|
|
257
|
+
expect(deps.isResearchCompleted).toHaveBeenCalledWith('issue-1');
|
|
258
|
+
expect(deps.isBacklogCreationCompleted).toHaveBeenCalledWith('issue-1');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// start / stop lifecycle
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
describe('WorkflowGovernor — start/stop lifecycle', () => {
|
|
265
|
+
beforeEach(() => {
|
|
266
|
+
vi.useFakeTimers();
|
|
267
|
+
});
|
|
268
|
+
afterEach(() => {
|
|
269
|
+
vi.useRealTimers();
|
|
270
|
+
});
|
|
271
|
+
it('isRunning returns false before start', () => {
|
|
272
|
+
const deps = makeMockDeps();
|
|
273
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
274
|
+
expect(governor.isRunning()).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
it('isRunning returns true after start', () => {
|
|
277
|
+
const deps = makeMockDeps();
|
|
278
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
279
|
+
governor.start();
|
|
280
|
+
expect(governor.isRunning()).toBe(true);
|
|
281
|
+
governor.stop();
|
|
282
|
+
});
|
|
283
|
+
it('isRunning returns false after stop', () => {
|
|
284
|
+
const deps = makeMockDeps();
|
|
285
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
286
|
+
governor.start();
|
|
287
|
+
governor.stop();
|
|
288
|
+
expect(governor.isRunning()).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
it('start triggers an immediate scan', async () => {
|
|
291
|
+
const deps = makeMockDeps();
|
|
292
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
293
|
+
governor.start();
|
|
294
|
+
// Flush the fire-and-forget scanOnce by allowing the promise microtask to resolve
|
|
295
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
296
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(1);
|
|
297
|
+
governor.stop();
|
|
298
|
+
});
|
|
299
|
+
it('subsequent scans run on the configured interval', async () => {
|
|
300
|
+
const deps = makeMockDeps();
|
|
301
|
+
const governor = new WorkflowGovernor(makeConfig({ scanIntervalMs: 5000 }), deps);
|
|
302
|
+
governor.start();
|
|
303
|
+
// First immediate scan
|
|
304
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
305
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(1);
|
|
306
|
+
// Advance by one interval
|
|
307
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
308
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(2);
|
|
309
|
+
// Advance by another interval
|
|
310
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
311
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(3);
|
|
312
|
+
governor.stop();
|
|
313
|
+
});
|
|
314
|
+
it('stop prevents further scans', async () => {
|
|
315
|
+
const deps = makeMockDeps();
|
|
316
|
+
const governor = new WorkflowGovernor(makeConfig({ scanIntervalMs: 5000 }), deps);
|
|
317
|
+
governor.start();
|
|
318
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
319
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(1);
|
|
320
|
+
governor.stop();
|
|
321
|
+
await vi.advanceTimersByTimeAsync(10000);
|
|
322
|
+
// No additional calls after stop
|
|
323
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(1);
|
|
324
|
+
});
|
|
325
|
+
it('calling start twice does not create duplicate intervals', async () => {
|
|
326
|
+
const deps = makeMockDeps();
|
|
327
|
+
const governor = new WorkflowGovernor(makeConfig({ scanIntervalMs: 5000 }), deps);
|
|
328
|
+
governor.start();
|
|
329
|
+
governor.start(); // second call should be a no-op
|
|
330
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
331
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(1);
|
|
332
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
333
|
+
// Only 1 interval fires, not 2
|
|
334
|
+
expect(deps.listIssues).toHaveBeenCalledTimes(2);
|
|
335
|
+
governor.stop();
|
|
336
|
+
});
|
|
337
|
+
it('calling stop when not running is safe', () => {
|
|
338
|
+
const deps = makeMockDeps();
|
|
339
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
340
|
+
// Should not throw
|
|
341
|
+
expect(() => governor.stop()).not.toThrow();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Multiple projects
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
describe('WorkflowGovernor.scanOnce — multiple projects', () => {
|
|
348
|
+
it('scans each project independently', async () => {
|
|
349
|
+
const projectAIssues = [
|
|
350
|
+
makeIssue({ id: 'a-1', identifier: 'A-1', status: 'Backlog', project: 'Project-A' }),
|
|
351
|
+
];
|
|
352
|
+
const projectBIssues = [
|
|
353
|
+
makeIssue({ id: 'b-1', identifier: 'B-1', status: 'Finished', project: 'Project-B' }),
|
|
354
|
+
makeIssue({ id: 'b-2', identifier: 'B-2', status: 'Delivered', project: 'Project-B' }),
|
|
355
|
+
];
|
|
356
|
+
const listIssues = vi.fn()
|
|
357
|
+
.mockImplementation((project) => {
|
|
358
|
+
if (project === 'Project-A')
|
|
359
|
+
return Promise.resolve(projectAIssues);
|
|
360
|
+
if (project === 'Project-B')
|
|
361
|
+
return Promise.resolve(projectBIssues);
|
|
362
|
+
return Promise.resolve([]);
|
|
363
|
+
});
|
|
364
|
+
const deps = makeMockDeps({ listIssues });
|
|
365
|
+
const governor = new WorkflowGovernor(makeConfig({ projects: ['Project-A', 'Project-B'] }), deps);
|
|
366
|
+
const results = await governor.scanOnce();
|
|
367
|
+
expect(results).toHaveLength(2);
|
|
368
|
+
expect(results[0].project).toBe('Project-A');
|
|
369
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
370
|
+
expect(results[1].project).toBe('Project-B');
|
|
371
|
+
expect(results[1].actionsDispatched).toBe(2);
|
|
372
|
+
});
|
|
373
|
+
it('dispatch limit is per-project', async () => {
|
|
374
|
+
const issues = [
|
|
375
|
+
makeIssue({ id: 'issue-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
376
|
+
makeIssue({ id: 'issue-2', identifier: 'SUP-2', status: 'Backlog' }),
|
|
377
|
+
makeIssue({ id: 'issue-3', identifier: 'SUP-3', status: 'Backlog' }),
|
|
378
|
+
];
|
|
379
|
+
const deps = makeMockDeps({
|
|
380
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
381
|
+
});
|
|
382
|
+
const governor = new WorkflowGovernor(makeConfig({
|
|
383
|
+
projects: ['Project-A', 'Project-B'],
|
|
384
|
+
maxConcurrentDispatches: 1,
|
|
385
|
+
}), deps);
|
|
386
|
+
const results = await governor.scanOnce();
|
|
387
|
+
// Each project dispatches 1 (the limit), total 2
|
|
388
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
389
|
+
expect(results[1].actionsDispatched).toBe(1);
|
|
390
|
+
expect(deps.dispatchWork).toHaveBeenCalledTimes(2);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// Mixed statuses in a single scan
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
describe('WorkflowGovernor.scanOnce — mixed statuses', () => {
|
|
397
|
+
it('handles a mix of actionable and non-actionable issues', async () => {
|
|
398
|
+
const issues = [
|
|
399
|
+
makeIssue({ id: 'i-1', identifier: 'SUP-1', status: 'Accepted' }), // terminal
|
|
400
|
+
makeIssue({ id: 'i-2', identifier: 'SUP-2', status: 'Backlog' }), // actionable
|
|
401
|
+
makeIssue({ id: 'i-3', identifier: 'SUP-3', status: 'Started' }), // not actionable (already working)
|
|
402
|
+
makeIssue({ id: 'i-4', identifier: 'SUP-4', status: 'Finished' }), // actionable
|
|
403
|
+
makeIssue({ id: 'i-5', identifier: 'SUP-5', status: 'Canceled' }), // terminal
|
|
404
|
+
];
|
|
405
|
+
const deps = makeMockDeps({
|
|
406
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
407
|
+
});
|
|
408
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
409
|
+
const results = await governor.scanOnce();
|
|
410
|
+
expect(results[0].scannedIssues).toBe(5);
|
|
411
|
+
expect(results[0].actionsDispatched).toBe(2); // Backlog + Finished
|
|
412
|
+
expect(results[0].skippedReasons.size).toBe(3); // Accepted + Started + Canceled
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// PRIORITY override affects processing order
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
describe('WorkflowGovernor.scanOnce — PRIORITY override sorting', () => {
|
|
419
|
+
it('dispatches high-priority issues before low-priority ones', async () => {
|
|
420
|
+
const issues = [
|
|
421
|
+
makeIssue({ id: 'low-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
422
|
+
makeIssue({ id: 'high-1', identifier: 'SUP-2', status: 'Backlog' }),
|
|
423
|
+
makeIssue({ id: 'med-1', identifier: 'SUP-3', status: 'Backlog' }),
|
|
424
|
+
];
|
|
425
|
+
const getOverridePriority = vi.fn().mockImplementation((issueId) => {
|
|
426
|
+
if (issueId === 'high-1')
|
|
427
|
+
return Promise.resolve('high');
|
|
428
|
+
if (issueId === 'med-1')
|
|
429
|
+
return Promise.resolve('medium');
|
|
430
|
+
if (issueId === 'low-1')
|
|
431
|
+
return Promise.resolve('low');
|
|
432
|
+
return Promise.resolve(null);
|
|
433
|
+
});
|
|
434
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
435
|
+
const deps = makeMockDeps({
|
|
436
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
437
|
+
getOverridePriority,
|
|
438
|
+
dispatchWork,
|
|
439
|
+
});
|
|
440
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
441
|
+
await governor.scanOnce();
|
|
442
|
+
expect(dispatchWork).toHaveBeenCalledTimes(3);
|
|
443
|
+
// high-1 dispatched first, then med-1, then low-1
|
|
444
|
+
expect(dispatchWork.mock.calls[0][0]).toEqual(expect.objectContaining({ id: 'high-1' }));
|
|
445
|
+
expect(dispatchWork.mock.calls[1][0]).toEqual(expect.objectContaining({ id: 'med-1' }));
|
|
446
|
+
expect(dispatchWork.mock.calls[2][0]).toEqual(expect.objectContaining({ id: 'low-1' }));
|
|
447
|
+
});
|
|
448
|
+
it('dispatches priority-overridden issues before non-overridden ones', async () => {
|
|
449
|
+
const issues = [
|
|
450
|
+
makeIssue({ id: 'none-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
451
|
+
makeIssue({ id: 'none-2', identifier: 'SUP-2', status: 'Backlog' }),
|
|
452
|
+
makeIssue({ id: 'high-1', identifier: 'SUP-3', status: 'Backlog' }),
|
|
453
|
+
];
|
|
454
|
+
const getOverridePriority = vi.fn().mockImplementation((issueId) => {
|
|
455
|
+
if (issueId === 'high-1')
|
|
456
|
+
return Promise.resolve('high');
|
|
457
|
+
return Promise.resolve(null);
|
|
458
|
+
});
|
|
459
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
460
|
+
const deps = makeMockDeps({
|
|
461
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
462
|
+
getOverridePriority,
|
|
463
|
+
dispatchWork,
|
|
464
|
+
});
|
|
465
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
466
|
+
await governor.scanOnce();
|
|
467
|
+
expect(dispatchWork).toHaveBeenCalledTimes(3);
|
|
468
|
+
// high-1 dispatched first, regardless of list order
|
|
469
|
+
expect(dispatchWork.mock.calls[0][0]).toEqual(expect.objectContaining({ id: 'high-1' }));
|
|
470
|
+
});
|
|
471
|
+
it('priority sorting affects which issues get dispatched under the limit', async () => {
|
|
472
|
+
const issues = [
|
|
473
|
+
makeIssue({ id: 'none-1', identifier: 'SUP-1', status: 'Backlog' }),
|
|
474
|
+
makeIssue({ id: 'none-2', identifier: 'SUP-2', status: 'Backlog' }),
|
|
475
|
+
makeIssue({ id: 'high-1', identifier: 'SUP-3', status: 'Backlog' }),
|
|
476
|
+
];
|
|
477
|
+
const getOverridePriority = vi.fn().mockImplementation((issueId) => {
|
|
478
|
+
if (issueId === 'high-1')
|
|
479
|
+
return Promise.resolve('high');
|
|
480
|
+
return Promise.resolve(null);
|
|
481
|
+
});
|
|
482
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
483
|
+
const deps = makeMockDeps({
|
|
484
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
485
|
+
getOverridePriority,
|
|
486
|
+
dispatchWork,
|
|
487
|
+
});
|
|
488
|
+
// Only dispatch 1 — the high-priority issue should win
|
|
489
|
+
const governor = new WorkflowGovernor(makeConfig({ maxConcurrentDispatches: 1 }), deps);
|
|
490
|
+
await governor.scanOnce();
|
|
491
|
+
expect(dispatchWork).toHaveBeenCalledTimes(1);
|
|
492
|
+
expect(dispatchWork.mock.calls[0][0]).toEqual(expect.objectContaining({ id: 'high-1' }));
|
|
493
|
+
});
|
|
494
|
+
it('issues with equal priority maintain stable order', async () => {
|
|
495
|
+
const issues = [
|
|
496
|
+
makeIssue({ id: 'a', identifier: 'SUP-1', status: 'Backlog' }),
|
|
497
|
+
makeIssue({ id: 'b', identifier: 'SUP-2', status: 'Backlog' }),
|
|
498
|
+
makeIssue({ id: 'c', identifier: 'SUP-3', status: 'Backlog' }),
|
|
499
|
+
];
|
|
500
|
+
// All have no priority override
|
|
501
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
502
|
+
const deps = makeMockDeps({
|
|
503
|
+
listIssues: vi.fn().mockResolvedValue(issues),
|
|
504
|
+
dispatchWork,
|
|
505
|
+
});
|
|
506
|
+
const governor = new WorkflowGovernor(makeConfig(), deps);
|
|
507
|
+
await governor.scanOnce();
|
|
508
|
+
expect(dispatchWork).toHaveBeenCalledTimes(3);
|
|
509
|
+
// Original order preserved for equal priority
|
|
510
|
+
expect(dispatchWork.mock.calls[0][0]).toEqual(expect.objectContaining({ id: 'a' }));
|
|
511
|
+
expect(dispatchWork.mock.calls[1][0]).toEqual(expect.objectContaining({ id: 'b' }));
|
|
512
|
+
expect(dispatchWork.mock.calls[2][0]).toEqual(expect.objectContaining({ id: 'c' }));
|
|
513
|
+
});
|
|
514
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human Touchpoint Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages human override state and generates review request notifications.
|
|
5
|
+
* Uses a storage adapter pattern so that packages/core does not depend on
|
|
6
|
+
* packages/server (Redis) directly.
|
|
7
|
+
*/
|
|
8
|
+
import type { OverrideDirective, OverridePriority } from './override-parser.js';
|
|
9
|
+
/**
|
|
10
|
+
* Persisted override state for an issue
|
|
11
|
+
*/
|
|
12
|
+
export interface OverrideState {
|
|
13
|
+
issueId: string;
|
|
14
|
+
directive: OverrideDirective;
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
expiresAt?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for touchpoint timeouts
|
|
20
|
+
*/
|
|
21
|
+
export interface TouchpointConfig {
|
|
22
|
+
/** Timeout for review requests (default: 4 hours) */
|
|
23
|
+
reviewRequestTimeoutMs: number;
|
|
24
|
+
/** Timeout for decomposition proposals (default: 2 hours) */
|
|
25
|
+
decompositionProposalTimeoutMs: number;
|
|
26
|
+
/** Timeout for escalation alerts (default: Infinity — requires human response) */
|
|
27
|
+
escalationTimeoutMs: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Default touchpoint configuration
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_TOUCHPOINT_CONFIG: TouchpointConfig;
|
|
33
|
+
/**
|
|
34
|
+
* Types of human touchpoint notifications
|
|
35
|
+
*/
|
|
36
|
+
export type TouchpointType = 'review-request' | 'decomposition-proposal' | 'escalation-alert';
|
|
37
|
+
/**
|
|
38
|
+
* A notification posted to an issue requesting human attention
|
|
39
|
+
*/
|
|
40
|
+
export interface TouchpointNotification {
|
|
41
|
+
type: TouchpointType;
|
|
42
|
+
issueId: string;
|
|
43
|
+
body: string;
|
|
44
|
+
postedAt: number;
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
respondedAt?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Storage adapter for override state persistence.
|
|
50
|
+
* Implementations can back this with Redis, in-memory maps, etc.
|
|
51
|
+
*/
|
|
52
|
+
export interface OverrideStorage {
|
|
53
|
+
get(issueId: string): Promise<OverrideState | null>;
|
|
54
|
+
set(issueId: string, state: OverrideState): Promise<void>;
|
|
55
|
+
clear(issueId: string): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* In-memory override storage for testing and local development
|
|
59
|
+
*/
|
|
60
|
+
export declare class InMemoryOverrideStorage implements OverrideStorage {
|
|
61
|
+
private store;
|
|
62
|
+
get(issueId: string): Promise<OverrideState | null>;
|
|
63
|
+
set(issueId: string, state: OverrideState): Promise<void>;
|
|
64
|
+
clear(issueId: string): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Initialize the touchpoint manager with a storage adapter.
|
|
68
|
+
* Must be called before using state management functions.
|
|
69
|
+
*/
|
|
70
|
+
export declare function initTouchpointStorage(storage: OverrideStorage): void;
|
|
71
|
+
/**
|
|
72
|
+
* Get the current override state for an issue
|
|
73
|
+
*/
|
|
74
|
+
export declare function getOverrideState(issueId: string): Promise<OverrideState | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Set an override directive for an issue
|
|
77
|
+
*/
|
|
78
|
+
export declare function setOverrideState(issueId: string, directive: OverrideDirective): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Clear the override state for an issue (e.g., on RESUME)
|
|
81
|
+
*/
|
|
82
|
+
export declare function clearOverrideState(issueId: string): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Check if an issue is currently held (HOLD directive active)
|
|
85
|
+
*/
|
|
86
|
+
export declare function isHeld(issueId: string): Promise<boolean>;
|
|
87
|
+
/**
|
|
88
|
+
* Get the PRIORITY override for an issue, if one is active.
|
|
89
|
+
* Returns the priority level ('high' | 'medium' | 'low') or null if no priority override.
|
|
90
|
+
*/
|
|
91
|
+
export declare function getOverridePriority(issueId: string): Promise<OverridePriority | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Generate a review request notification.
|
|
94
|
+
* Typically posted at cycle 2 when the context-enriched strategy kicks in.
|
|
95
|
+
*/
|
|
96
|
+
export declare function generateReviewRequest(context: {
|
|
97
|
+
issueIdentifier: string;
|
|
98
|
+
cycleCount: number;
|
|
99
|
+
failureSummary: string;
|
|
100
|
+
strategy: string;
|
|
101
|
+
totalCostUsd?: number;
|
|
102
|
+
}, config?: TouchpointConfig): TouchpointNotification;
|
|
103
|
+
/**
|
|
104
|
+
* Generate a decomposition proposal notification.
|
|
105
|
+
* Typically posted at cycle 3 when the decompose strategy kicks in.
|
|
106
|
+
*/
|
|
107
|
+
export declare function generateDecompositionProposal(context: {
|
|
108
|
+
issueIdentifier: string;
|
|
109
|
+
cycleCount: number;
|
|
110
|
+
failureSummary: string;
|
|
111
|
+
totalCostUsd?: number;
|
|
112
|
+
}, config?: TouchpointConfig): TouchpointNotification;
|
|
113
|
+
/**
|
|
114
|
+
* Generate an escalation alert notification.
|
|
115
|
+
* Posted at cycle 4+ when human intervention is required.
|
|
116
|
+
*/
|
|
117
|
+
export declare function generateEscalationAlert(context: {
|
|
118
|
+
issueIdentifier: string;
|
|
119
|
+
cycleCount: number;
|
|
120
|
+
failureSummary: string;
|
|
121
|
+
totalCostUsd?: number;
|
|
122
|
+
blockerIdentifier?: string;
|
|
123
|
+
}, config?: TouchpointConfig): TouchpointNotification;
|
|
124
|
+
/**
|
|
125
|
+
* Check if a touchpoint notification has timed out (human did not respond in time).
|
|
126
|
+
*
|
|
127
|
+
* A touchpoint with Infinity timeout never times out (always returns false).
|
|
128
|
+
* A touchpoint that has been responded to (respondedAt is set) never times out.
|
|
129
|
+
*/
|
|
130
|
+
export declare function hasTouchpointTimedOut(notification: TouchpointNotification): boolean;
|
|
131
|
+
//# sourceMappingURL=human-touchpoints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"human-touchpoints.d.ts","sourceRoot":"","sources":["../../../src/governor/human-touchpoints.ts"],"names":[],"mappings":"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"}
|