@stilero/bankan 1.0.13 → 1.0.17

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.
@@ -0,0 +1,186 @@
1
+ import { afterEach, describe, expect, test } from 'vitest';
2
+
3
+ import { createRuntimeHarness } from '../test-utils.js';
4
+
5
+ let harness = null;
6
+
7
+ afterEach(() => {
8
+ harness?.cleanup();
9
+ harness = null;
10
+ });
11
+
12
+ describe('TaskStore persistence and recovery', () => {
13
+ test('addTask populates default fields and update helpers persist changes', async () => {
14
+ harness = createRuntimeHarness();
15
+ const storeModule = await harness.importModule('./src/store.js');
16
+ const store = storeModule.default;
17
+
18
+ const task = store.addTask({
19
+ title: 'Add tests',
20
+ priority: 'high',
21
+ description: 'Focus on critical paths',
22
+ repoPath: '/repo',
23
+ });
24
+
25
+ expect(task.id).toMatch(/^T-/);
26
+ expect(task.status).toBe('backlog');
27
+ expect(task.lastActiveStage).toBe('backlog');
28
+ expect(task.log.at(-1).message).toBe('Task created');
29
+
30
+ store.updateTask(task.id, { status: 'planning', assignedTo: 'plan-1' });
31
+ store.appendLog(task.id, 'Planner started');
32
+ store.appendSession(task.id, { id: 'session-1' });
33
+ store.updateTaskTokens(task.id, 250);
34
+ store.updateTaskTokens(task.id, 100);
35
+
36
+ const updated = store.getTask(task.id);
37
+ expect(updated.assignedTo).toBe('plan-1');
38
+ expect(updated.lastActiveStage).toBe('planning');
39
+ expect(updated.totalTokens).toBe(250);
40
+ expect(updated.sessionHistory).toEqual([{ id: 'session-1' }]);
41
+ expect(updated.log.map(entry => entry.message)).toEqual([
42
+ 'Task created',
43
+ 'Status changed to planning',
44
+ 'Planner started',
45
+ ]);
46
+ });
47
+
48
+ test('savePlan, removePlan, and deleteTask update persisted task state', async () => {
49
+ harness = createRuntimeHarness();
50
+ const storeModule = await harness.importModule('./src/store.js');
51
+ const configModule = await harness.importModule('./src/config.js');
52
+ const store = storeModule.default;
53
+
54
+ const task = store.addTask({ title: 'Delete me' });
55
+ store.savePlan(task.id, 'Plan content');
56
+
57
+ expect(configModule.getRuntimeStatePaths().plansDir).toContain(harness.runtimeDir);
58
+
59
+ store.removePlan(task.id);
60
+ const removed = store.deleteTask(task.id);
61
+
62
+ expect(removed.id).toBe(task.id);
63
+ expect(store.getTask(task.id)).toBeNull();
64
+ expect(store.deleteTask('missing')).toBeNull();
65
+ });
66
+
67
+ test('restartRecovery normalizes in-flight and legacy blocked tasks', async () => {
68
+ harness = createRuntimeHarness();
69
+ const storeModule = await harness.importModule('./src/store.js');
70
+ const store = storeModule.default;
71
+
72
+ const planningTask = store.addTask({ title: 'Planning task', repoPath: '/repo' });
73
+ const reviewTask = store.addTask({ title: 'Review task', repoPath: '/repo' });
74
+ const pausedTask = store.addTask({ title: 'Paused task', repoPath: '/repo' });
75
+ const doneTask = store.addTask({ title: 'Done task', repoPath: '/repo' });
76
+ const legacyBlockedTask = store.addTask({
77
+ title: 'Legacy blocker',
78
+ repoPath: 'https://github.com/stilero/bankan.git',
79
+ });
80
+
81
+ store.updateTask(planningTask.id, { status: 'planning', assignedTo: 'plan-1' });
82
+ store.updateTask(reviewTask.id, { status: 'review', assignedTo: 'rev-1' });
83
+ store.updateTask(pausedTask.id, { status: 'paused', assignedTo: 'imp-1' });
84
+ store.updateTask(doneTask.id, { status: 'awaiting_human_review', assignedTo: 'rev-2', workspacePath: '/tmp/work' });
85
+ store.updateTask(legacyBlockedTask.id, {
86
+ status: 'blocked',
87
+ blockedReason: 'Invalid repository path: https://github.com/stilero/bankan.git',
88
+ workspacePath: null,
89
+ assignedTo: 'plan-2',
90
+ });
91
+
92
+ store.restartRecovery();
93
+
94
+ expect(store.getTask(planningTask.id).status).toBe('backlog');
95
+ expect(store.getTask(reviewTask.id).status).toBe('review');
96
+ expect(store.getTask(reviewTask.id).assignedTo).toBeNull();
97
+ expect(store.getTask(pausedTask.id).status).toBe('paused');
98
+ expect(store.getTask(pausedTask.id).assignedTo).toBeNull();
99
+ expect(store.getTask(doneTask.id).status).toBe('done');
100
+ expect(store.getTask(doneTask.id).workspacePath).toBeNull();
101
+ expect(store.getTask(legacyBlockedTask.id).status).toBe('backlog');
102
+ expect(store.getTask(legacyBlockedTask.id).blockedReason).toBeNull();
103
+ });
104
+
105
+ test('corrupt task files fall back to an empty store', async () => {
106
+ harness = createRuntimeHarness();
107
+ const configModule = await harness.importModule('./src/config.js');
108
+ const { writeFileSync } = await import('node:fs');
109
+
110
+ writeFileSync(configModule.getRuntimeStatePaths().tasksFile, '{not-json');
111
+
112
+ const storeModule = await harness.importModule('./src/store.js');
113
+ expect(storeModule.default.getAllTasks()).toEqual([]);
114
+ });
115
+
116
+ test('normalizes legacy task records on load and handles null update helpers', async () => {
117
+ harness = createRuntimeHarness();
118
+ const configModule = await harness.importModule('./src/config.js');
119
+ const { writeFileSync } = await import('node:fs');
120
+
121
+ writeFileSync(configModule.getRuntimeStatePaths().tasksFile, JSON.stringify([
122
+ {
123
+ id: 'T-LEGACY',
124
+ title: 'Legacy',
125
+ status: 'awaiting_human_review',
126
+ repoPath: '/repo',
127
+ reviewCycleCount: -2,
128
+ totalTokens: -10,
129
+ startedAt: 42,
130
+ completedAt: 24,
131
+ sessionHistory: null,
132
+ assignedTo: 'rev-1',
133
+ workspacePath: '/tmp/work',
134
+ log: [],
135
+ },
136
+ ]));
137
+
138
+ const storeModule = await harness.importModule('./src/store.js');
139
+ const store = storeModule.default;
140
+ const task = store.getTask('T-LEGACY');
141
+
142
+ expect(task.status).toBe('done');
143
+ expect(task.assignedTo).toBeNull();
144
+ expect(task.workspacePath).toBeNull();
145
+ expect(task.reviewCycleCount).toBe(0);
146
+ expect(task.totalTokens).toBe(0);
147
+ expect(task.startedAt).toBeNull();
148
+ expect(task.completedAt).toBeNull();
149
+ expect(task.sessionHistory).toEqual([]);
150
+
151
+ expect(store.updateTask('missing', { status: 'done' })).toBeNull();
152
+ expect(store.appendLog('missing', 'nope')).toBeNull();
153
+ expect(store.appendSession('missing', { id: 's-1' })).toBeNull();
154
+ expect(store.appendSession('T-LEGACY', null)).toBeNull();
155
+ expect(store.updateTaskTokens('missing', 10)).toBeNull();
156
+ });
157
+
158
+ test('restartRecovery handles planner-path blockers and invalid counters', async () => {
159
+ harness = createRuntimeHarness();
160
+ const storeModule = await harness.importModule('./src/store.js');
161
+ const store = storeModule.default;
162
+
163
+ const task = store.addTask({
164
+ title: 'Planner dir blocker',
165
+ repoPath: 'git@github.com:stilero/bankan.git',
166
+ });
167
+
168
+ store.updateTask(task.id, {
169
+ status: 'blocked',
170
+ blockedReason: 'Invalid planner working directory: git@github.com:stilero/bankan.git',
171
+ workspacePath: null,
172
+ reviewCycleCount: -1,
173
+ totalTokens: -1,
174
+ lastActiveStage: null,
175
+ });
176
+
177
+ store.restartRecovery();
178
+
179
+ const recovered = store.getTask(task.id);
180
+ expect(recovered.status).toBe('backlog');
181
+ expect(recovered.previousStatus).toBeNull();
182
+ expect(recovered.lastActiveStage).toBe('backlog');
183
+ expect(recovered.reviewCycleCount).toBe(0);
184
+ expect(recovered.totalTokens).toBe(0);
185
+ });
186
+ });
@@ -48,10 +48,11 @@ export function isReviewResultPlaceholder(reviewText, reviewResult = parseReview
48
48
  const normalized = reviewText.replace(/\s+/g, ' ').trim().toLowerCase();
49
49
  if (normalized.includes("(issue description, or 'none')")) return true;
50
50
  if (normalized.includes('(2-3 sentences summarising the review)')) return true;
51
+ if (normalized.includes('concrete issue, or none')) return true;
51
52
 
52
53
  const summary = (reviewResult.summary || '').replace(/\s+/g, ' ').trim().toLowerCase();
53
54
  if (!summary) return true;
54
- if (summary.includes('2-3 sentences summarising the review')) return true;
55
+ if (summary.includes('sentences summarising the review')) return true;
55
56
 
56
57
  return false;
57
58
  }
@@ -59,12 +60,27 @@ export function isReviewResultPlaceholder(reviewText, reviewResult = parseReview
59
60
  export function isPlanPlaceholder(planText) {
60
61
  if (typeof planText !== 'string' || !planText.trim()) return true;
61
62
  const normalized = planText.replace(/\s+/g, ' ').trim().toLowerCase();
62
- if (normalized.includes('(one sentence describing what will be built)')) return true;
63
- if (normalized.includes('(detailed, actionable step)')) return true;
64
- if (normalized.includes('path/to/file.ts (reason for modification)')) return true;
65
- if (normalized.includes("(test description, or 'none')")) return true;
66
- if (normalized.includes("(potential issue or edge case, or 'none')")) return true;
67
- return false;
63
+ const hasPlaceholder =
64
+ normalized.includes('(one sentence describing what will be built)') ||
65
+ normalized.includes('(detailed, actionable step)') ||
66
+ normalized.includes('path/to/file.ts (reason for modification)') ||
67
+ normalized.includes("(test description, or 'none')") ||
68
+ normalized.includes("(potential issue or edge case, or 'none')");
69
+
70
+ if (!hasPlaceholder) return false;
71
+
72
+ // Placeholder patterns found — but the plan may contain real content
73
+ // alongside echoed prompt template text (terminal echo contamination).
74
+ // Check if any SUMMARY line has substantive content.
75
+ const summaryMatches = [...planText.matchAll(/SUMMARY:\s*(.+)/gi)];
76
+ const hasRealSummary = summaryMatches.some(m => {
77
+ const val = m[1].trim();
78
+ return val.length > 20 && !val.startsWith('(');
79
+ });
80
+
81
+ if (hasRealSummary) return false;
82
+
83
+ return true;
68
84
  }
69
85
 
70
86
  export function getLiveTaskAgent(task, agentManager) {
@@ -1,16 +1,18 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
1
+ import { describe, expect, test } from 'vitest';
3
2
 
4
3
  import {
5
4
  getLiveTaskAgent,
5
+ getAgentStage,
6
6
  isReviewResultPlaceholder,
7
+ isPlanPlaceholder,
7
8
  parseReviewResult,
8
9
  reviewShouldPass,
9
10
  stageToRetryStatus,
10
11
  } from './workflow.js';
11
12
 
12
- test('minor-only review output is normalized to pass', () => {
13
- const reviewText = `=== REVIEW START ===
13
+ describe('review parsing', () => {
14
+ test('minor-only review output is normalized to pass', () => {
15
+ const reviewText = `=== REVIEW START ===
14
16
  VERDICT: FAIL
15
17
  CRITICAL_ISSUES:
16
18
  - none
@@ -19,16 +21,16 @@ MINOR_ISSUES:
19
21
  SUMMARY: Only minor issues were found.
20
22
  === REVIEW END ===`;
21
23
 
22
- const result = parseReviewResult(reviewText);
24
+ const result = parseReviewResult(reviewText);
23
25
 
24
- assert.equal(result.verdict, 'FAIL');
25
- assert.deepEqual(result.criticalIssues, []);
26
- assert.equal(result.minorIssues.length, 1);
27
- assert.equal(reviewShouldPass(result), true);
28
- });
26
+ expect(result.verdict).toBe('FAIL');
27
+ expect(result.criticalIssues).toEqual([]);
28
+ expect(result.minorIssues).toHaveLength(1);
29
+ expect(reviewShouldPass(result)).toBe(true);
30
+ });
29
31
 
30
- test('review with critical issues still fails', () => {
31
- const reviewText = `=== REVIEW START ===
32
+ test('review with critical issues still fails', () => {
33
+ const reviewText = `=== REVIEW START ===
32
34
  VERDICT: FAIL
33
35
  CRITICAL_ISSUES:
34
36
  - server/src/orchestrator.js: review failures can loop indefinitely
@@ -37,14 +39,14 @@ MINOR_ISSUES:
37
39
  SUMMARY: A must-fix issue remains.
38
40
  === REVIEW END ===`;
39
41
 
40
- const result = parseReviewResult(reviewText);
42
+ const result = parseReviewResult(reviewText);
41
43
 
42
- assert.equal(result.hasCriticalIssues, true);
43
- assert.equal(reviewShouldPass(result), false);
44
- });
44
+ expect(result.hasCriticalIssues).toBe(true);
45
+ expect(reviewShouldPass(result)).toBe(false);
46
+ });
45
47
 
46
- test('placeholder review template is rejected', () => {
47
- const reviewText = `=== REVIEW START ===
48
+ test('placeholder review template is rejected', () => {
49
+ const reviewText = `=== REVIEW START ===
48
50
  VERDICT: PASS
49
51
  CRITICAL_ISSUES:
50
52
  - none
@@ -53,13 +55,13 @@ MINOR_ISSUES:
53
55
  SUMMARY: (2-3 sentences summarising the review)
54
56
  === REVIEW END ===`;
55
57
 
56
- const result = parseReviewResult(reviewText);
58
+ const result = parseReviewResult(reviewText);
57
59
 
58
- assert.equal(isReviewResultPlaceholder(reviewText, result), true);
59
- });
60
+ expect(isReviewResultPlaceholder(reviewText, result)).toBe(true);
61
+ });
60
62
 
61
- test('concrete review output is not treated as placeholder', () => {
62
- const reviewText = `=== REVIEW START ===
63
+ test('concrete review output is not treated as placeholder', () => {
64
+ const reviewText = `=== REVIEW START ===
63
65
  VERDICT: PASS
64
66
  CRITICAL_ISSUES:
65
67
  - none
@@ -68,57 +70,200 @@ MINOR_ISSUES:
68
70
  SUMMARY: Changed files: server/src/orchestrator.js, server/src/workflow.js. The review completion gate now rejects placeholder output and the branch otherwise looks consistent with existing task flow. Strengths: the change is narrowly scoped and adds regression coverage.
69
71
  === REVIEW END ===`;
70
72
 
71
- const result = parseReviewResult(reviewText);
73
+ const result = parseReviewResult(reviewText);
74
+
75
+ expect(isReviewResultPlaceholder(reviewText, result)).toBe(false);
76
+ });
77
+
78
+ test('plan placeholders are detected from default prompt scaffolding', () => {
79
+ expect(isPlanPlaceholder(`=== PLAN START ===
80
+ SUMMARY: (one sentence describing what will be built)
81
+ BRANCH: feature/example
82
+ FILES_TO_MODIFY:
83
+ - path/to/file.ts (reason for modification)
84
+ STEPS:
85
+ 1. (detailed, actionable step)
86
+ TESTS_NEEDED:
87
+ - (test description, or 'none')
88
+ RISKS:
89
+ - (potential issue or edge case, or 'none')
90
+ === PLAN END ===`)).toBe(true);
91
+ });
92
+
93
+ test('review prompt template is detected as placeholder', () => {
94
+ const templateText = `=== REVIEW START ===
95
+ VERDICT: PASS or FAIL
96
+ CRITICAL_ISSUES:
97
+ - concrete issue, or none
98
+ MINOR_ISSUES:
99
+ - concrete issue, or none
100
+ SUMMARY: 2-3 concrete sentences summarising the review, including changed files and strengths
101
+ === REVIEW END ===`;
102
+ expect(isReviewResultPlaceholder(templateText)).toBe(true);
103
+ });
104
+
105
+ test('blank and summary-free reviews are treated as placeholders', () => {
106
+ expect(isReviewResultPlaceholder('')).toBe(true);
107
+ expect(isReviewResultPlaceholder(`=== REVIEW START ===
108
+ VERDICT: PASS
109
+ CRITICAL_ISSUES:
110
+ - none
111
+ MINOR_ISSUES:
112
+ - none
113
+ SUMMARY:
114
+ === REVIEW END ===`)).toBe(true);
115
+ });
116
+
117
+ test('concrete plans are not treated as placeholders', () => {
118
+ expect(isPlanPlaceholder(`=== PLAN START ===
119
+ SUMMARY: Add automated tests for the workflow helpers.
120
+ BRANCH: feature/add-workflow-tests
121
+ FILES_TO_MODIFY:
122
+ - server/src/workflow.test.js (expand retry and placeholder coverage)
123
+ STEPS:
124
+ 1. Add tests for retry status edge cases.
125
+ TESTS_NEEDED:
126
+ - Run npm run test --prefix server
127
+ RISKS:
128
+ - none
129
+ === PLAN END ===`)).toBe(false);
130
+ });
131
+
132
+ test('real plan contaminated with echoed prompt template is not a placeholder', () => {
133
+ // When Claude CLI echoes the prompt, terminal artifacts mix template
134
+ // placeholder text into the real plan block after ANSI stripping.
135
+ const contaminated = `=== PLAN START ===
136
+ after the delimiters: === PLAN START === SUMMARY: (one sentence describing what will be built) BRANCH: (feature/t-91eadd-short-descriptive-slug) FILES_TO_MODIFY: - path/to/file.ts (reason for modification) STEPS: 1. (detailed, actionable step) 2. (detailed, actionable step) TESTS_NEEDED: - (test description, or 'none') RISKS: - (potential issue or edge case, or 'none') === PLAN END ===
137
+ SUMMARY: Add a Reports modal accessible from the top bar that shows per-repo task counts, total time spent, and total tokens spent with visually stunning charts.
138
+ BRANCH: feature/t-91eadd-reports-dashboard
139
+ FILES_TO_MODIFY:
140
+ - client/src/ReportsModal.jsx (new reporting modal component)
141
+ - client/src/App.jsx (add reports button and modal state)
142
+ - server/src/index.js (add REST endpoint for aggregated task stats)
143
+ STEPS:
144
+ 1. Create the ReportsModal component with per-repo breakdown.
145
+ 2. Wire up the top-bar button in App.jsx.
146
+ 3. Add GET /api/reports endpoint in index.js.
147
+ TESTS_NEEDED:
148
+ - Run npm run test to verify no regressions
149
+ RISKS:
150
+ - none
151
+ === PLAN END ===`;
152
+ expect(isPlanPlaceholder(contaminated)).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe('retry status resolution', () => {
157
+ test('retry ignores stale assigned agent process from another task', () => {
158
+ const task = {
159
+ id: 'T-123',
160
+ assignedTo: 'imp-1',
161
+ blockedReason: 'Agent is awaiting user input',
162
+ lastActiveStage: 'implementation',
163
+ };
164
+ const agentManager = {
165
+ get(id) {
166
+ expect(id).toBe('imp-1');
167
+ return {
168
+ id: 'imp-1',
169
+ process: { pid: 1234 },
170
+ currentTask: 'T-999',
171
+ };
172
+ },
173
+ };
174
+
175
+ const liveAgent = getLiveTaskAgent(task, agentManager);
176
+ const retryStatus = stageToRetryStatus(task, { liveAgent, planningDisabled: false });
177
+
178
+ expect(liveAgent).toBeNull();
179
+ expect(retryStatus).toBe('queued');
180
+ });
181
+
182
+ test('retry reuses live agent only when it still owns the task', () => {
183
+ const task = {
184
+ id: 'T-123',
185
+ assignedTo: 'imp-1',
186
+ blockedReason: 'Agent is awaiting user input',
187
+ lastActiveStage: 'implementation',
188
+ };
189
+ const liveAgentDef = {
190
+ id: 'imp-1',
191
+ process: { pid: 1234 },
192
+ currentTask: 'T-123',
193
+ };
194
+ const agentManager = {
195
+ get() {
196
+ return liveAgentDef;
197
+ },
198
+ };
199
+
200
+ const liveAgent = getLiveTaskAgent(task, agentManager);
201
+ const retryStatus = stageToRetryStatus(task, { liveAgent, planningDisabled: false });
202
+
203
+ expect(liveAgent).toBe(liveAgentDef);
204
+ expect(retryStatus).toBe('implementing');
205
+ });
206
+
207
+ test('planning tasks return to approval when a plan already exists', () => {
208
+ expect(stageToRetryStatus({
209
+ lastActiveStage: 'planning',
210
+ plan: 'ready',
211
+ blockedReason: '',
212
+ }, { planningDisabled: false })).toBe('awaiting_approval');
213
+ });
214
+
215
+ test('maximum review cycle blockers return to queue', () => {
216
+ expect(stageToRetryStatus({
217
+ lastActiveStage: 'review',
218
+ blockedReason: 'Reached maximum review cycles for this task',
219
+ }, { planningDisabled: false })).toBe('queued');
220
+ });
221
+
222
+ test('live planner and reviewer agents preserve their active stage', () => {
223
+ expect(stageToRetryStatus({
224
+ lastActiveStage: 'implementation',
225
+ blockedReason: '',
226
+ }, { liveAgent: { id: 'plan-1' }, planningDisabled: false })).toBe('planning');
227
+
228
+ expect(stageToRetryStatus({
229
+ lastActiveStage: 'implementation',
230
+ blockedReason: '',
231
+ }, { liveAgent: { id: 'rev-1' }, planningDisabled: false })).toBe('review');
232
+ });
233
+
234
+ test('planning-disabled tasks skip back to queue and backlog defaults respect planner availability', () => {
235
+ expect(stageToRetryStatus({
236
+ lastActiveStage: 'planning',
237
+ plan: null,
238
+ blockedReason: '',
239
+ }, { planningDisabled: true })).toBe('queued');
240
+
241
+ expect(stageToRetryStatus({
242
+ lastActiveStage: null,
243
+ blockedReason: '',
244
+ }, { planningDisabled: false })).toBe('backlog');
72
245
 
73
- assert.equal(isReviewResultPlaceholder(reviewText, result), false);
246
+ expect(stageToRetryStatus({
247
+ lastActiveStage: null,
248
+ blockedReason: '',
249
+ }, { planningDisabled: true })).toBe('queued');
250
+ });
74
251
  });
75
252
 
76
- test('retry ignores stale assigned agent process from another task', () => {
77
- const task = {
78
- id: 'T-123',
79
- assignedTo: 'imp-1',
80
- blockedReason: 'Agent is awaiting user input',
81
- lastActiveStage: 'implementation',
82
- };
83
- const agentManager = {
84
- get(id) {
85
- assert.equal(id, 'imp-1');
86
- return {
87
- id: 'imp-1',
88
- process: { pid: 1234 },
89
- currentTask: 'T-999',
90
- };
91
- },
92
- };
93
-
94
- const liveAgent = getLiveTaskAgent(task, agentManager);
95
- const retryStatus = stageToRetryStatus(task, { liveAgent, planningDisabled: false });
96
-
97
- assert.equal(liveAgent, null);
98
- assert.equal(retryStatus, 'queued');
253
+ describe('live agent lookup', () => {
254
+ test('returns null when task has no assigned agent or no live process', () => {
255
+ expect(getLiveTaskAgent({ assignedTo: null }, { get: () => null })).toBeNull();
256
+ expect(getLiveTaskAgent({ assignedTo: 'imp-1', id: 'T-1' }, {
257
+ get: () => ({ id: 'imp-1', process: null, currentTask: 'T-1' }),
258
+ })).toBeNull();
259
+ });
99
260
  });
100
261
 
101
- test('retry reuses live agent only when it still owns the task', () => {
102
- const task = {
103
- id: 'T-123',
104
- assignedTo: 'imp-1',
105
- blockedReason: 'Agent is awaiting user input',
106
- lastActiveStage: 'implementation',
107
- };
108
- const liveAgentDef = {
109
- id: 'imp-1',
110
- process: { pid: 1234 },
111
- currentTask: 'T-123',
112
- };
113
- const agentManager = {
114
- get() {
115
- return liveAgentDef;
116
- },
117
- };
118
-
119
- const liveAgent = getLiveTaskAgent(task, agentManager);
120
- const retryStatus = stageToRetryStatus(task, { liveAgent, planningDisabled: false });
121
-
122
- assert.equal(liveAgent, liveAgentDef);
123
- assert.equal(retryStatus, 'implementing');
262
+ describe('agent stage helper', () => {
263
+ test('maps known agent prefixes to stages', () => {
264
+ expect(getAgentStage('plan-2')).toBe('planning');
265
+ expect(getAgentStage('imp-2')).toBe('implementation');
266
+ expect(getAgentStage('rev-2')).toBe('review');
267
+ expect(getAgentStage('orch')).toBeNull();
268
+ });
124
269
  });