@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,629 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { decideAction } from './decision-engine.js';
|
|
3
|
+
import { DEFAULT_GOVERNOR_CONFIG } from './governor-types.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** Create a minimal GovernorIssue with sensible defaults. */
|
|
8
|
+
function makeIssue(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
id: 'issue-1',
|
|
11
|
+
identifier: 'SUP-100',
|
|
12
|
+
title: 'Test Issue',
|
|
13
|
+
description: undefined,
|
|
14
|
+
status: 'Backlog',
|
|
15
|
+
labels: [],
|
|
16
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000, // 2 hours ago
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Build a well-researched description (long + structured header). */
|
|
21
|
+
function wellResearchedDescription() {
|
|
22
|
+
return ('## Acceptance Criteria\n' +
|
|
23
|
+
'- The system should handle X\n' +
|
|
24
|
+
'- The system should handle Y\n' +
|
|
25
|
+
'\n' +
|
|
26
|
+
'## Technical Approach\n' +
|
|
27
|
+
'We will implement this by modifying the governor module to add top-of-funnel ' +
|
|
28
|
+
'triggers that automatically research issues in the Icebox. This approach ' +
|
|
29
|
+
'ensures issues are enriched before entering the active backlog.');
|
|
30
|
+
}
|
|
31
|
+
/** Build a sparse description (too short / no headers). */
|
|
32
|
+
function sparseDescription() {
|
|
33
|
+
return 'Fix the thing.';
|
|
34
|
+
}
|
|
35
|
+
/** Build a default DecisionContext for testing. */
|
|
36
|
+
function makeContext(overrides = {}) {
|
|
37
|
+
return {
|
|
38
|
+
issue: makeIssue(),
|
|
39
|
+
config: { ...DEFAULT_GOVERNOR_CONFIG, projects: ['TestProject'] },
|
|
40
|
+
hasActiveSession: false,
|
|
41
|
+
isHeld: false,
|
|
42
|
+
isWithinCooldown: false,
|
|
43
|
+
isParentIssue: false,
|
|
44
|
+
workflowStrategy: undefined,
|
|
45
|
+
researchCompleted: false,
|
|
46
|
+
backlogCreationCompleted: false,
|
|
47
|
+
completedSessionCount: 0,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Universal skip conditions
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
describe('decideAction — universal skip conditions', () => {
|
|
55
|
+
it('returns none when issue has an active session', () => {
|
|
56
|
+
const ctx = makeContext({ hasActiveSession: true });
|
|
57
|
+
const result = decideAction(ctx);
|
|
58
|
+
expect(result.action).toBe('none');
|
|
59
|
+
expect(result.reason).toContain('active agent session');
|
|
60
|
+
});
|
|
61
|
+
it('returns none when issue is within cooldown', () => {
|
|
62
|
+
const ctx = makeContext({ isWithinCooldown: true });
|
|
63
|
+
const result = decideAction(ctx);
|
|
64
|
+
expect(result.action).toBe('none');
|
|
65
|
+
expect(result.reason).toContain('cooldown');
|
|
66
|
+
});
|
|
67
|
+
it('returns none when issue is held', () => {
|
|
68
|
+
const ctx = makeContext({ isHeld: true });
|
|
69
|
+
const result = decideAction(ctx);
|
|
70
|
+
expect(result.action).toBe('none');
|
|
71
|
+
expect(result.reason).toContain('HOLD');
|
|
72
|
+
});
|
|
73
|
+
it('evaluates skip conditions in order: active session before cooldown', () => {
|
|
74
|
+
const ctx = makeContext({ hasActiveSession: true, isWithinCooldown: true, isHeld: true });
|
|
75
|
+
const result = decideAction(ctx);
|
|
76
|
+
expect(result.reason).toContain('active agent session');
|
|
77
|
+
});
|
|
78
|
+
it('evaluates skip conditions in order: cooldown before hold', () => {
|
|
79
|
+
const ctx = makeContext({ isWithinCooldown: true, isHeld: true });
|
|
80
|
+
const result = decideAction(ctx);
|
|
81
|
+
expect(result.reason).toContain('cooldown');
|
|
82
|
+
});
|
|
83
|
+
it('trips circuit breaker when session count exceeds max', () => {
|
|
84
|
+
const ctx = makeContext({ completedSessionCount: 3 });
|
|
85
|
+
const result = decideAction(ctx);
|
|
86
|
+
expect(result.action).toBe('none');
|
|
87
|
+
expect(result.reason).toContain('circuit breaker');
|
|
88
|
+
});
|
|
89
|
+
it('allows dispatch when session count is below max', () => {
|
|
90
|
+
const ctx = makeContext({ completedSessionCount: 2 });
|
|
91
|
+
const result = decideAction(ctx);
|
|
92
|
+
expect(result.action).toBe('trigger-development');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Terminal statuses
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
describe('decideAction — terminal statuses', () => {
|
|
99
|
+
it('returns none for Accepted status', () => {
|
|
100
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'Accepted' }) });
|
|
101
|
+
const result = decideAction(ctx);
|
|
102
|
+
expect(result.action).toBe('none');
|
|
103
|
+
expect(result.reason).toContain('terminal status');
|
|
104
|
+
expect(result.reason).toContain('Accepted');
|
|
105
|
+
});
|
|
106
|
+
it('returns none for Canceled status', () => {
|
|
107
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'Canceled' }) });
|
|
108
|
+
const result = decideAction(ctx);
|
|
109
|
+
expect(result.action).toBe('none');
|
|
110
|
+
expect(result.reason).toContain('terminal status');
|
|
111
|
+
});
|
|
112
|
+
it('returns none for Duplicate status', () => {
|
|
113
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'Duplicate' }) });
|
|
114
|
+
const result = decideAction(ctx);
|
|
115
|
+
expect(result.action).toBe('none');
|
|
116
|
+
expect(result.reason).toContain('terminal status');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Icebox (top-of-funnel delegation)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
describe('decideAction — Icebox (top-of-funnel)', () => {
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
vi.useFakeTimers();
|
|
125
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
126
|
+
});
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
vi.useRealTimers();
|
|
129
|
+
});
|
|
130
|
+
const iceboxConfig = {
|
|
131
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
132
|
+
projects: ['TestProject'],
|
|
133
|
+
enableAutoResearch: true,
|
|
134
|
+
enableAutoBacklogCreation: true,
|
|
135
|
+
};
|
|
136
|
+
it('triggers research for sparse Icebox issue', () => {
|
|
137
|
+
const ctx = makeContext({
|
|
138
|
+
issue: makeIssue({
|
|
139
|
+
status: 'Icebox',
|
|
140
|
+
description: sparseDescription(),
|
|
141
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
142
|
+
}),
|
|
143
|
+
config: iceboxConfig,
|
|
144
|
+
});
|
|
145
|
+
const result = decideAction(ctx);
|
|
146
|
+
expect(result.action).toBe('trigger-research');
|
|
147
|
+
});
|
|
148
|
+
it('triggers backlog-creation for well-researched Icebox issue', () => {
|
|
149
|
+
const ctx = makeContext({
|
|
150
|
+
issue: makeIssue({
|
|
151
|
+
status: 'Icebox',
|
|
152
|
+
description: wellResearchedDescription(),
|
|
153
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
154
|
+
}),
|
|
155
|
+
config: iceboxConfig,
|
|
156
|
+
});
|
|
157
|
+
const result = decideAction(ctx);
|
|
158
|
+
expect(result.action).toBe('trigger-backlog-creation');
|
|
159
|
+
});
|
|
160
|
+
it('returns none for Icebox parent issue', () => {
|
|
161
|
+
const ctx = makeContext({
|
|
162
|
+
issue: makeIssue({
|
|
163
|
+
status: 'Icebox',
|
|
164
|
+
description: sparseDescription(),
|
|
165
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
166
|
+
}),
|
|
167
|
+
isParentIssue: true,
|
|
168
|
+
});
|
|
169
|
+
const result = decideAction(ctx);
|
|
170
|
+
expect(result.action).toBe('none');
|
|
171
|
+
expect(result.reason).toContain('coordination');
|
|
172
|
+
});
|
|
173
|
+
it('returns none when auto-research is disabled and description is sparse', () => {
|
|
174
|
+
const ctx = makeContext({
|
|
175
|
+
issue: makeIssue({
|
|
176
|
+
status: 'Icebox',
|
|
177
|
+
description: sparseDescription(),
|
|
178
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
179
|
+
}),
|
|
180
|
+
config: {
|
|
181
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
182
|
+
projects: ['TestProject'],
|
|
183
|
+
enableAutoResearch: false,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const result = decideAction(ctx);
|
|
187
|
+
expect(result.action).toBe('none');
|
|
188
|
+
});
|
|
189
|
+
it('returns none when auto-backlog-creation is disabled', () => {
|
|
190
|
+
const ctx = makeContext({
|
|
191
|
+
issue: makeIssue({
|
|
192
|
+
status: 'Icebox',
|
|
193
|
+
description: wellResearchedDescription(),
|
|
194
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
195
|
+
}),
|
|
196
|
+
config: {
|
|
197
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
198
|
+
projects: ['TestProject'],
|
|
199
|
+
enableAutoBacklogCreation: false,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const result = decideAction(ctx);
|
|
203
|
+
expect(result.action).toBe('none');
|
|
204
|
+
});
|
|
205
|
+
it('skips research when research already completed', () => {
|
|
206
|
+
const ctx = makeContext({
|
|
207
|
+
issue: makeIssue({
|
|
208
|
+
status: 'Icebox',
|
|
209
|
+
description: sparseDescription(),
|
|
210
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
211
|
+
}),
|
|
212
|
+
researchCompleted: true,
|
|
213
|
+
});
|
|
214
|
+
const result = decideAction(ctx);
|
|
215
|
+
// Research completed but description still sparse -- none
|
|
216
|
+
expect(result.action).toBe('none');
|
|
217
|
+
});
|
|
218
|
+
it('returns none for Icebox issue created too recently', () => {
|
|
219
|
+
const ctx = makeContext({
|
|
220
|
+
issue: makeIssue({
|
|
221
|
+
status: 'Icebox',
|
|
222
|
+
description: sparseDescription(),
|
|
223
|
+
createdAt: Date.now() - 10 * 60 * 1000, // 10 minutes ago
|
|
224
|
+
}),
|
|
225
|
+
config: iceboxConfig,
|
|
226
|
+
});
|
|
227
|
+
const result = decideAction(ctx);
|
|
228
|
+
expect(result.action).toBe('none');
|
|
229
|
+
expect(result.reason).toContain('not been in Icebox long enough');
|
|
230
|
+
});
|
|
231
|
+
it('respects topOfFunnel config overrides', () => {
|
|
232
|
+
const ctx = makeContext({
|
|
233
|
+
issue: makeIssue({
|
|
234
|
+
status: 'Icebox',
|
|
235
|
+
description: sparseDescription(),
|
|
236
|
+
createdAt: Date.now() - 10 * 60 * 1000, // 10 minutes ago
|
|
237
|
+
}),
|
|
238
|
+
config: {
|
|
239
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
240
|
+
projects: ['TestProject'],
|
|
241
|
+
enableAutoResearch: true,
|
|
242
|
+
topOfFunnel: {
|
|
243
|
+
iceboxResearchDelayMs: 5 * 60 * 1000, // 5 minutes
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const result = decideAction(ctx);
|
|
248
|
+
expect(result.action).toBe('trigger-research');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Backlog (development)
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
describe('decideAction — Backlog', () => {
|
|
255
|
+
it('triggers development for Backlog issue', () => {
|
|
256
|
+
const ctx = makeContext({
|
|
257
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
258
|
+
});
|
|
259
|
+
const result = decideAction(ctx);
|
|
260
|
+
expect(result.action).toBe('trigger-development');
|
|
261
|
+
expect(result.reason).toContain('triggering development');
|
|
262
|
+
});
|
|
263
|
+
it('triggers development for parent Backlog issue (coordination)', () => {
|
|
264
|
+
const ctx = makeContext({
|
|
265
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
266
|
+
isParentIssue: true,
|
|
267
|
+
});
|
|
268
|
+
const result = decideAction(ctx);
|
|
269
|
+
expect(result.action).toBe('trigger-development');
|
|
270
|
+
expect(result.reason).toContain('coordination');
|
|
271
|
+
});
|
|
272
|
+
it('skips sub-issues in Backlog (coordinator manages via parent)', () => {
|
|
273
|
+
const ctx = makeContext({
|
|
274
|
+
issue: makeIssue({ status: 'Backlog', parentId: 'parent-issue-id' }),
|
|
275
|
+
});
|
|
276
|
+
const result = decideAction(ctx);
|
|
277
|
+
expect(result.action).toBe('none');
|
|
278
|
+
expect(result.reason).toContain('Sub-issue');
|
|
279
|
+
expect(result.reason).toContain('coordinator manages sub-issues via parent');
|
|
280
|
+
});
|
|
281
|
+
it('skips sub-issues before checking enableAutoDevelopment', () => {
|
|
282
|
+
const ctx = makeContext({
|
|
283
|
+
issue: makeIssue({ status: 'Backlog', parentId: 'parent-issue-id' }),
|
|
284
|
+
config: {
|
|
285
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
286
|
+
projects: ['TestProject'],
|
|
287
|
+
enableAutoDevelopment: false,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const result = decideAction(ctx);
|
|
291
|
+
expect(result.action).toBe('none');
|
|
292
|
+
expect(result.reason).toContain('Sub-issue');
|
|
293
|
+
});
|
|
294
|
+
it('skips sub-issues in Finished (coordinator manages QA via parent)', () => {
|
|
295
|
+
const ctx = makeContext({
|
|
296
|
+
issue: makeIssue({ status: 'Finished', parentId: 'parent-issue-id' }),
|
|
297
|
+
});
|
|
298
|
+
const result = decideAction(ctx);
|
|
299
|
+
expect(result.action).toBe('none');
|
|
300
|
+
expect(result.reason).toContain('Sub-issue');
|
|
301
|
+
expect(result.reason).toContain('coordinator manages sub-issues via parent');
|
|
302
|
+
});
|
|
303
|
+
it('skips sub-issues in Delivered (coordinator manages acceptance via parent)', () => {
|
|
304
|
+
const ctx = makeContext({
|
|
305
|
+
issue: makeIssue({ status: 'Delivered', parentId: 'parent-issue-id' }),
|
|
306
|
+
});
|
|
307
|
+
const result = decideAction(ctx);
|
|
308
|
+
expect(result.action).toBe('none');
|
|
309
|
+
expect(result.reason).toContain('Sub-issue');
|
|
310
|
+
expect(result.reason).toContain('coordinator manages sub-issues via parent');
|
|
311
|
+
});
|
|
312
|
+
it('skips sub-issues in Rejected (coordinator manages refinement via parent)', () => {
|
|
313
|
+
const ctx = makeContext({
|
|
314
|
+
issue: makeIssue({ status: 'Rejected', parentId: 'parent-issue-id' }),
|
|
315
|
+
});
|
|
316
|
+
const result = decideAction(ctx);
|
|
317
|
+
expect(result.action).toBe('none');
|
|
318
|
+
expect(result.reason).toContain('Sub-issue');
|
|
319
|
+
expect(result.reason).toContain('coordinator manages sub-issues via parent');
|
|
320
|
+
});
|
|
321
|
+
it('returns none when auto-development is disabled', () => {
|
|
322
|
+
const ctx = makeContext({
|
|
323
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
324
|
+
config: {
|
|
325
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
326
|
+
projects: ['TestProject'],
|
|
327
|
+
enableAutoDevelopment: false,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
const result = decideAction(ctx);
|
|
331
|
+
expect(result.action).toBe('none');
|
|
332
|
+
expect(result.reason).toContain('disabled');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Started
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
describe('decideAction — Started', () => {
|
|
339
|
+
it('returns none for Started status (agent already working)', () => {
|
|
340
|
+
const ctx = makeContext({
|
|
341
|
+
issue: makeIssue({ status: 'Started' }),
|
|
342
|
+
});
|
|
343
|
+
const result = decideAction(ctx);
|
|
344
|
+
expect(result.action).toBe('none');
|
|
345
|
+
expect(result.reason).toContain('Started');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Finished (QA)
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
describe('decideAction — Finished (QA)', () => {
|
|
352
|
+
it('triggers QA for Finished issue', () => {
|
|
353
|
+
const ctx = makeContext({
|
|
354
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
355
|
+
});
|
|
356
|
+
const result = decideAction(ctx);
|
|
357
|
+
expect(result.action).toBe('trigger-qa');
|
|
358
|
+
expect(result.reason).toContain('triggering QA');
|
|
359
|
+
});
|
|
360
|
+
it('returns none when auto-QA is disabled', () => {
|
|
361
|
+
const ctx = makeContext({
|
|
362
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
363
|
+
config: {
|
|
364
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
365
|
+
projects: ['TestProject'],
|
|
366
|
+
enableAutoQA: false,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
const result = decideAction(ctx);
|
|
370
|
+
expect(result.action).toBe('none');
|
|
371
|
+
expect(result.reason).toContain('disabled');
|
|
372
|
+
});
|
|
373
|
+
it('escalates to human when strategy is escalate-human', () => {
|
|
374
|
+
const ctx = makeContext({
|
|
375
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
376
|
+
workflowStrategy: 'escalate-human',
|
|
377
|
+
});
|
|
378
|
+
const result = decideAction(ctx);
|
|
379
|
+
expect(result.action).toBe('escalate-human');
|
|
380
|
+
expect(result.reason).toContain('human review');
|
|
381
|
+
});
|
|
382
|
+
it('triggers decompose when strategy is decompose', () => {
|
|
383
|
+
const ctx = makeContext({
|
|
384
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
385
|
+
workflowStrategy: 'decompose',
|
|
386
|
+
});
|
|
387
|
+
const result = decideAction(ctx);
|
|
388
|
+
expect(result.action).toBe('decompose');
|
|
389
|
+
expect(result.reason).toContain('decomposition');
|
|
390
|
+
});
|
|
391
|
+
it('triggers normal QA when strategy is normal', () => {
|
|
392
|
+
const ctx = makeContext({
|
|
393
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
394
|
+
workflowStrategy: 'normal',
|
|
395
|
+
});
|
|
396
|
+
const result = decideAction(ctx);
|
|
397
|
+
expect(result.action).toBe('trigger-qa');
|
|
398
|
+
});
|
|
399
|
+
it('triggers normal QA when strategy is context-enriched', () => {
|
|
400
|
+
const ctx = makeContext({
|
|
401
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
402
|
+
workflowStrategy: 'context-enriched',
|
|
403
|
+
});
|
|
404
|
+
const result = decideAction(ctx);
|
|
405
|
+
expect(result.action).toBe('trigger-qa');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Delivered (acceptance)
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
describe('decideAction — Delivered (acceptance)', () => {
|
|
412
|
+
it('triggers acceptance for Delivered issue', () => {
|
|
413
|
+
const ctx = makeContext({
|
|
414
|
+
issue: makeIssue({ status: 'Delivered' }),
|
|
415
|
+
});
|
|
416
|
+
const result = decideAction(ctx);
|
|
417
|
+
expect(result.action).toBe('trigger-acceptance');
|
|
418
|
+
expect(result.reason).toContain('triggering acceptance');
|
|
419
|
+
});
|
|
420
|
+
it('returns none when auto-acceptance is disabled', () => {
|
|
421
|
+
const ctx = makeContext({
|
|
422
|
+
issue: makeIssue({ status: 'Delivered' }),
|
|
423
|
+
config: {
|
|
424
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
425
|
+
projects: ['TestProject'],
|
|
426
|
+
enableAutoAcceptance: false,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
const result = decideAction(ctx);
|
|
430
|
+
expect(result.action).toBe('none');
|
|
431
|
+
expect(result.reason).toContain('disabled');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
// Rejected (refinement)
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
describe('decideAction — Rejected (refinement)', () => {
|
|
438
|
+
it('triggers refinement for Rejected issue', () => {
|
|
439
|
+
const ctx = makeContext({
|
|
440
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
441
|
+
});
|
|
442
|
+
const result = decideAction(ctx);
|
|
443
|
+
expect(result.action).toBe('trigger-refinement');
|
|
444
|
+
expect(result.reason).toContain('triggering refinement');
|
|
445
|
+
});
|
|
446
|
+
it('escalates to human when strategy is escalate-human', () => {
|
|
447
|
+
const ctx = makeContext({
|
|
448
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
449
|
+
workflowStrategy: 'escalate-human',
|
|
450
|
+
});
|
|
451
|
+
const result = decideAction(ctx);
|
|
452
|
+
expect(result.action).toBe('escalate-human');
|
|
453
|
+
expect(result.reason).toContain('human intervention');
|
|
454
|
+
});
|
|
455
|
+
it('triggers decompose when strategy is decompose', () => {
|
|
456
|
+
const ctx = makeContext({
|
|
457
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
458
|
+
workflowStrategy: 'decompose',
|
|
459
|
+
});
|
|
460
|
+
const result = decideAction(ctx);
|
|
461
|
+
expect(result.action).toBe('decompose');
|
|
462
|
+
expect(result.reason).toContain('decomposition');
|
|
463
|
+
});
|
|
464
|
+
it('triggers normal refinement when strategy is normal', () => {
|
|
465
|
+
const ctx = makeContext({
|
|
466
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
467
|
+
workflowStrategy: 'normal',
|
|
468
|
+
});
|
|
469
|
+
const result = decideAction(ctx);
|
|
470
|
+
expect(result.action).toBe('trigger-refinement');
|
|
471
|
+
});
|
|
472
|
+
it('triggers normal refinement when strategy is context-enriched', () => {
|
|
473
|
+
const ctx = makeContext({
|
|
474
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
475
|
+
workflowStrategy: 'context-enriched',
|
|
476
|
+
});
|
|
477
|
+
const result = decideAction(ctx);
|
|
478
|
+
expect(result.action).toBe('trigger-refinement');
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Unknown status
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
describe('decideAction — unknown status', () => {
|
|
485
|
+
it('returns none for unrecognized status', () => {
|
|
486
|
+
const ctx = makeContext({
|
|
487
|
+
issue: makeIssue({ status: 'CustomStatus' }),
|
|
488
|
+
});
|
|
489
|
+
const result = decideAction(ctx);
|
|
490
|
+
expect(result.action).toBe('none');
|
|
491
|
+
expect(result.reason).toContain('unrecognized status');
|
|
492
|
+
expect(result.reason).toContain('CustomStatus');
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Config enable/disable flags across statuses
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
describe('decideAction — config flags integration', () => {
|
|
499
|
+
it('disabling all flags causes all non-terminal statuses to return none', () => {
|
|
500
|
+
const config = {
|
|
501
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
502
|
+
projects: ['TestProject'],
|
|
503
|
+
enableAutoResearch: false,
|
|
504
|
+
enableAutoBacklogCreation: false,
|
|
505
|
+
enableAutoDevelopment: false,
|
|
506
|
+
enableAutoQA: false,
|
|
507
|
+
enableAutoAcceptance: false,
|
|
508
|
+
};
|
|
509
|
+
const statuses = ['Backlog', 'Finished', 'Delivered'];
|
|
510
|
+
for (const status of statuses) {
|
|
511
|
+
const ctx = makeContext({
|
|
512
|
+
issue: makeIssue({ status }),
|
|
513
|
+
config,
|
|
514
|
+
});
|
|
515
|
+
const result = decideAction(ctx);
|
|
516
|
+
expect(result.action).toBe('none');
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
it('Rejected issues always trigger refinement (no disable flag)', () => {
|
|
520
|
+
const config = {
|
|
521
|
+
...DEFAULT_GOVERNOR_CONFIG,
|
|
522
|
+
projects: ['TestProject'],
|
|
523
|
+
enableAutoResearch: false,
|
|
524
|
+
enableAutoBacklogCreation: false,
|
|
525
|
+
enableAutoDevelopment: false,
|
|
526
|
+
enableAutoQA: false,
|
|
527
|
+
enableAutoAcceptance: false,
|
|
528
|
+
};
|
|
529
|
+
const ctx = makeContext({
|
|
530
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
531
|
+
config,
|
|
532
|
+
});
|
|
533
|
+
const result = decideAction(ctx);
|
|
534
|
+
expect(result.action).toBe('trigger-refinement');
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// Parent issue handling
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
describe('decideAction — parent issue handling', () => {
|
|
541
|
+
it('parent Backlog issue triggers development (coordination template)', () => {
|
|
542
|
+
const ctx = makeContext({
|
|
543
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
544
|
+
isParentIssue: true,
|
|
545
|
+
});
|
|
546
|
+
const result = decideAction(ctx);
|
|
547
|
+
expect(result.action).toBe('trigger-development');
|
|
548
|
+
expect(result.reason).toContain('Parent issue');
|
|
549
|
+
expect(result.reason).toContain('coordination');
|
|
550
|
+
});
|
|
551
|
+
it('parent Icebox issue returns none (coordination, no top-of-funnel)', () => {
|
|
552
|
+
vi.useFakeTimers();
|
|
553
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
554
|
+
const ctx = makeContext({
|
|
555
|
+
issue: makeIssue({
|
|
556
|
+
status: 'Icebox',
|
|
557
|
+
description: sparseDescription(),
|
|
558
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
559
|
+
}),
|
|
560
|
+
isParentIssue: true,
|
|
561
|
+
});
|
|
562
|
+
const result = decideAction(ctx);
|
|
563
|
+
expect(result.action).toBe('none');
|
|
564
|
+
vi.useRealTimers();
|
|
565
|
+
});
|
|
566
|
+
it('parent Finished issue triggers QA normally', () => {
|
|
567
|
+
const ctx = makeContext({
|
|
568
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
569
|
+
isParentIssue: true,
|
|
570
|
+
});
|
|
571
|
+
const result = decideAction(ctx);
|
|
572
|
+
expect(result.action).toBe('trigger-qa');
|
|
573
|
+
});
|
|
574
|
+
it('parent Delivered issue triggers acceptance normally', () => {
|
|
575
|
+
const ctx = makeContext({
|
|
576
|
+
issue: makeIssue({ status: 'Delivered' }),
|
|
577
|
+
isParentIssue: true,
|
|
578
|
+
});
|
|
579
|
+
const result = decideAction(ctx);
|
|
580
|
+
expect(result.action).toBe('trigger-acceptance');
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
// Escalation strategy effects across statuses
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
describe('decideAction — escalation strategy effects', () => {
|
|
587
|
+
it('escalate-human on Finished yields escalate-human', () => {
|
|
588
|
+
const ctx = makeContext({
|
|
589
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
590
|
+
workflowStrategy: 'escalate-human',
|
|
591
|
+
});
|
|
592
|
+
expect(decideAction(ctx).action).toBe('escalate-human');
|
|
593
|
+
});
|
|
594
|
+
it('escalate-human on Rejected yields escalate-human', () => {
|
|
595
|
+
const ctx = makeContext({
|
|
596
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
597
|
+
workflowStrategy: 'escalate-human',
|
|
598
|
+
});
|
|
599
|
+
expect(decideAction(ctx).action).toBe('escalate-human');
|
|
600
|
+
});
|
|
601
|
+
it('decompose on Finished yields decompose', () => {
|
|
602
|
+
const ctx = makeContext({
|
|
603
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
604
|
+
workflowStrategy: 'decompose',
|
|
605
|
+
});
|
|
606
|
+
expect(decideAction(ctx).action).toBe('decompose');
|
|
607
|
+
});
|
|
608
|
+
it('decompose on Rejected yields decompose', () => {
|
|
609
|
+
const ctx = makeContext({
|
|
610
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
611
|
+
workflowStrategy: 'decompose',
|
|
612
|
+
});
|
|
613
|
+
expect(decideAction(ctx).action).toBe('decompose');
|
|
614
|
+
});
|
|
615
|
+
it('escalate-human on Backlog has no effect (Backlog uses enableAutoDevelopment)', () => {
|
|
616
|
+
const ctx = makeContext({
|
|
617
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
618
|
+
workflowStrategy: 'escalate-human',
|
|
619
|
+
});
|
|
620
|
+
expect(decideAction(ctx).action).toBe('trigger-development');
|
|
621
|
+
});
|
|
622
|
+
it('escalate-human on Delivered has no effect (Delivered uses enableAutoAcceptance)', () => {
|
|
623
|
+
const ctx = makeContext({
|
|
624
|
+
issue: makeIssue({ status: 'Delivered' }),
|
|
625
|
+
workflowStrategy: 'escalate-human',
|
|
626
|
+
});
|
|
627
|
+
expect(decideAction(ctx).action).toBe('trigger-acceptance');
|
|
628
|
+
});
|
|
629
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Event Bus Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for event transport between producers (webhooks, poll
|
|
5
|
+
* sweeps) and consumers (EventDrivenGovernor). Implementations include
|
|
6
|
+
* InMemoryEventBus (testing/single-process) and RedisEventBus (production).
|
|
7
|
+
*/
|
|
8
|
+
import type { GovernorEvent } from './event-types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Transport layer for GovernorEvents.
|
|
11
|
+
*
|
|
12
|
+
* - `publish`: Enqueue an event for processing.
|
|
13
|
+
* - `subscribe`: Returns an AsyncIterable that yields events one at a time.
|
|
14
|
+
* - `ack`: Acknowledge successful processing (allows the bus to remove it).
|
|
15
|
+
* - `close`: Gracefully shut down the bus.
|
|
16
|
+
*/
|
|
17
|
+
export interface GovernorEventBus {
|
|
18
|
+
/**
|
|
19
|
+
* Publish an event to the bus.
|
|
20
|
+
* Returns the event ID assigned by the transport.
|
|
21
|
+
*/
|
|
22
|
+
publish(event: GovernorEvent): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to events. Returns an AsyncIterable that yields events
|
|
25
|
+
* as they become available. The iterable ends when `close()` is called.
|
|
26
|
+
*
|
|
27
|
+
* Each yielded item includes the transport-assigned event ID for acking.
|
|
28
|
+
*/
|
|
29
|
+
subscribe(): AsyncIterable<{
|
|
30
|
+
id: string;
|
|
31
|
+
event: GovernorEvent;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Acknowledge that an event has been successfully processed.
|
|
35
|
+
* The bus may use this to remove the event from pending state.
|
|
36
|
+
*/
|
|
37
|
+
ack(eventId: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Gracefully close the bus, ending any active subscriptions.
|
|
40
|
+
*/
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=event-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../../src/governor/event-bus.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAMrD;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE9C;;;;;OAKG;IACH,SAAS,IAAI,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC,CAAA;IAEhE;;;OAGG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnC;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Event Bus Interface
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for event transport between producers (webhooks, poll
|
|
5
|
+
* sweeps) and consumers (EventDrivenGovernor). Implementations include
|
|
6
|
+
* InMemoryEventBus (testing/single-process) and RedisEventBus (production).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|