@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,431 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+
3
+ import {
4
+ buildAgentCommand,
5
+ cleanTerminalArtifacts,
6
+ extractPlannerPlanText,
7
+ extractReviewerReviewText,
8
+ sanitizeBranchName,
9
+ } from './orchestrator.js';
10
+
11
+ describe('structured output extraction', () => {
12
+ test('planner extraction falls back to agent structured capture when the PTY tail lost the full block', () => {
13
+ const readCaptured = vi.fn(() => null);
14
+ const agent = {
15
+ cli: 'claude',
16
+ getBufferString: vi.fn(() => '...tail...\n=== PLAN END ==='),
17
+ getStructuredBlock: vi.fn(() => `=== PLAN START ===
18
+ SUMMARY: Persist completed planner output.
19
+ BRANCH: feature/test-plan
20
+ FILES_TO_MODIFY:
21
+ - server/src/agents.js (capture plan output)
22
+ STEPS:
23
+ 1. Read from stable structured state.
24
+ TESTS_NEEDED:
25
+ - Run npm run test:server
26
+ RISKS:
27
+ - none
28
+ === PLAN END ===`),
29
+ };
30
+
31
+ expect(extractPlannerPlanText(agent, { readCapturedCodexMessage: readCaptured })).toContain(
32
+ 'Persist completed planner output.'
33
+ );
34
+ expect(readCaptured).not.toHaveBeenCalled();
35
+ });
36
+
37
+ test('review extraction prefers Codex captured output before agent structured capture', () => {
38
+ const readCaptured = vi.fn(() => `=== REVIEW START ===
39
+ VERDICT: PASS
40
+ CRITICAL_ISSUES:
41
+ - none
42
+ MINOR_ISSUES:
43
+ - none
44
+ SUMMARY: Captured Codex output should win.
45
+ === REVIEW END ===`);
46
+ const agent = {
47
+ cli: 'codex',
48
+ getBufferString: vi.fn(() => '=== CODEX_LAST_MESSAGE_FILE:/tmp/test ==='),
49
+ getStructuredBlock: vi.fn(() => `=== REVIEW START ===
50
+ VERDICT: PASS
51
+ CRITICAL_ISSUES:
52
+ - none
53
+ MINOR_ISSUES:
54
+ - none
55
+ SUMMARY: Agent fallback should not be used here.
56
+ === REVIEW END ===`),
57
+ };
58
+
59
+ expect(extractReviewerReviewText(agent, { readCapturedCodexMessage: readCaptured })).toContain(
60
+ 'Captured Codex output should win.'
61
+ );
62
+ expect(readCaptured).toHaveBeenCalledOnce();
63
+ });
64
+
65
+ test('planner extraction finds real plan via getAllCapturedBlocks when buffer is exhausted', () => {
66
+ const readCaptured = vi.fn(() => null);
67
+ const realPlan = `=== PLAN START ===
68
+ SUMMARY: Real plan found via captured blocks.
69
+ BRANCH: feature/test-captured
70
+ FILES_TO_MODIFY:
71
+ - server/src/agents.js (fix capture)
72
+ STEPS:
73
+ 1. Store all captured blocks.
74
+ TESTS_NEEDED:
75
+ - Run npm run test:server
76
+ RISKS:
77
+ - none
78
+ === PLAN END ===`;
79
+ const templateBlock = `=== PLAN START ===
80
+ SUMMARY: (one sentence describing what will be built)
81
+ BRANCH: (feature/t-xxx-short-descriptive-slug)
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 ===`;
91
+ const agent = {
92
+ cli: 'claude',
93
+ // Buffer exhausted — only noise, no plan markers
94
+ getBufferString: vi.fn(() => 'lots of noise without any plan markers'),
95
+ // Structured capture has the placeholder (overwritten real plan)
96
+ getStructuredBlock: vi.fn(() => templateBlock),
97
+ // But getAllCapturedBlocks has the full history
98
+ getAllCapturedBlocks: vi.fn(() => [templateBlock, realPlan, templateBlock]),
99
+ };
100
+
101
+ const result = extractPlannerPlanText(agent, { readCapturedCodexMessage: readCaptured });
102
+ expect(result).toContain('Real plan found via captured blocks.');
103
+ expect(result).not.toContain('(one sentence describing');
104
+ });
105
+
106
+ test('planner extraction falls back to buffer scan when getStructuredBlock returns null', () => {
107
+ const readCaptured = vi.fn(() => null);
108
+ const planBlock = `=== PLAN START ===
109
+ SUMMARY: Buffer scan fallback works.
110
+ BRANCH: feature/buffer-fallback
111
+ FILES_TO_MODIFY:
112
+ - server/src/orchestrator.js (add fallback)
113
+ STEPS:
114
+ 1. Try structured capture first, then scan buffer.
115
+ TESTS_NEEDED:
116
+ - Run npm run test:server
117
+ RISKS:
118
+ - none
119
+ === PLAN END ===`;
120
+ const agent = {
121
+ cli: 'claude',
122
+ getBufferString: vi.fn(() => `some noise\n${planBlock}\nmore noise`),
123
+ getStructuredBlock: vi.fn(() => null),
124
+ };
125
+
126
+ const result = extractPlannerPlanText(agent, { readCapturedCodexMessage: readCaptured });
127
+ expect(result).toContain('Buffer scan fallback works.');
128
+ expect(result).toContain('=== PLAN START ===');
129
+ expect(result).toContain('=== PLAN END ===');
130
+ expect(agent.getStructuredBlock).toHaveBeenCalledWith('plan');
131
+ });
132
+
133
+ test('planner extraction skips placeholder blocks to find real plan in buffer', () => {
134
+ // The CLI echoes the prompt template (1st block), agent outputs real plan (2nd),
135
+ // then CLI re-renders the template in the status area (3rd). The last block is a
136
+ // placeholder, but the 2nd block has real content.
137
+ const templateBlock = `=== PLAN START ===
138
+ SUMMARY: (one sentence describing what will be built)
139
+ BRANCH: (feature/t-xxx-short-descriptive-slug)
140
+ FILES_TO_MODIFY:
141
+ - path/to/file.ts (reason for modification)
142
+ STEPS:
143
+ 1. (detailed, actionable step)
144
+ TESTS_NEEDED:
145
+ - (test description, or 'none')
146
+ RISKS:
147
+ - (potential issue or edge case, or 'none')
148
+ === PLAN END ===`;
149
+
150
+ const realBlock = `=== PLAN START ===
151
+ SUMMARY: Add a Reports modal accessible from the top bar with per-repo task counts and token stats.
152
+ BRANCH: feature/t-91eadd-reports-dashboard
153
+ FILES_TO_MODIFY:
154
+ - client/src/ReportsModal.jsx (new reporting modal)
155
+ - server/src/index.js (add reports endpoint)
156
+ STEPS:
157
+ 1. Create ReportsModal component.
158
+ 2. Wire up the top-bar button.
159
+ TESTS_NEEDED:
160
+ - Run npm run test
161
+ RISKS:
162
+ - none
163
+ === PLAN END ===`;
164
+
165
+ const readCaptured = vi.fn(() => null);
166
+ const bufferContent = `noise\n${templateBlock}\nmore noise\n${realBlock}\neven more\n${templateBlock}\ntrailing`;
167
+ const agent = {
168
+ cli: 'claude',
169
+ getBufferString: vi.fn(() => bufferContent),
170
+ // Structured capture got the template (last completed block)
171
+ getStructuredBlock: vi.fn(() => templateBlock),
172
+ };
173
+
174
+ const result = extractPlannerPlanText(agent, { readCapturedCodexMessage: readCaptured });
175
+ expect(result).toContain('Reports modal accessible from the top bar');
176
+ expect(result).not.toContain('(one sentence describing what will be built)');
177
+ });
178
+
179
+ test('review extraction finds real review via getAllCapturedBlocks when template overwrites capture', () => {
180
+ const readCaptured = vi.fn(() => null);
181
+ const realReview = `=== REVIEW START ===
182
+ VERDICT: PASS
183
+ CRITICAL_ISSUES:
184
+ - none
185
+ MINOR_ISSUES:
186
+ - Minor typo in variable name
187
+ SUMMARY: All changes look good. Tests pass and code follows conventions.
188
+ === REVIEW END ===`;
189
+ const templateBlock = `=== REVIEW START ===
190
+ VERDICT: PASS or FAIL
191
+ CRITICAL_ISSUES:
192
+ - concrete issue, or none
193
+ MINOR_ISSUES:
194
+ - concrete issue, or none
195
+ SUMMARY: 2-3 concrete sentences summarising the review, including changed files and strengths
196
+ === REVIEW END ===`;
197
+ const agent = {
198
+ cli: 'claude',
199
+ // Buffer exhausted — no review markers
200
+ getBufferString: vi.fn(() => 'noise without markers'),
201
+ // Structured capture has the placeholder (overwritten real review)
202
+ getStructuredBlock: vi.fn(() => templateBlock),
203
+ // But getAllCapturedBlocks has the full history
204
+ getAllCapturedBlocks: vi.fn(() => [templateBlock, realReview, templateBlock]),
205
+ };
206
+
207
+ const result = extractReviewerReviewText(agent, { readCapturedCodexMessage: readCaptured });
208
+ expect(result).toContain('All changes look good.');
209
+ expect(result).not.toContain('2-3 concrete sentences');
210
+ });
211
+
212
+ test('review extraction falls back to agent structured capture when the live tail only contains the end marker', () => {
213
+ const readCaptured = vi.fn(() => null);
214
+ const agent = {
215
+ cli: 'claude',
216
+ getBufferString: vi.fn(() => '...tail...\n=== REVIEW END ==='),
217
+ getStructuredBlock: vi.fn(() => `=== REVIEW START ===
218
+ VERDICT: PASS
219
+ CRITICAL_ISSUES:
220
+ - none
221
+ MINOR_ISSUES:
222
+ - none
223
+ SUMMARY: Stable review capture prevents timeout.
224
+ === REVIEW END ===`),
225
+ };
226
+
227
+ expect(extractReviewerReviewText(agent, { readCapturedCodexMessage: readCaptured })).toContain(
228
+ 'Stable review capture prevents timeout.'
229
+ );
230
+ });
231
+ });
232
+
233
+ describe('sanitizeBranchName', () => {
234
+ test('strips garbage text appended by ANSI cursor collapse', () => {
235
+ expect(sanitizeBranchName('feature/t-a811ca-reporting FILES_TO_MODIFY:'))
236
+ .toBe('feature/t-a811ca-reporting');
237
+ });
238
+
239
+ test('strips trailing prompt characters and whitespace', () => {
240
+ expect(sanitizeBranchName('feature/t-b60f78-repo-reports ❯'))
241
+ .toBe('feature/t-b60f78-repo-reports');
242
+ });
243
+
244
+ test('preserves clean branch names unchanged', () => {
245
+ expect(sanitizeBranchName('feature/t-91eadd-reports-dashboard'))
246
+ .toBe('feature/t-91eadd-reports-dashboard');
247
+ });
248
+
249
+ test('handles branch names with dots and underscores', () => {
250
+ expect(sanitizeBranchName('fix/v2.1_hotfix'))
251
+ .toBe('fix/v2.1_hotfix');
252
+ });
253
+
254
+ test('strips trailing dots from branch names', () => {
255
+ expect(sanitizeBranchName('feature/test.'))
256
+ .toBe('feature/test');
257
+ });
258
+ });
259
+
260
+ describe('buildAgentCommand model flag', () => {
261
+ test('claude CLI includes --model flag when model is non-empty', () => {
262
+ const cmd = buildAgentCommand('claude', 'do stuff', 'plan', 'haiku');
263
+ expect(cmd).toContain('--model haiku');
264
+ expect(cmd).toContain('--dangerously-skip-permissions');
265
+ });
266
+
267
+ test('claude CLI omits --model flag when model is empty', () => {
268
+ const cmd = buildAgentCommand('claude', 'do stuff', 'plan', '');
269
+ expect(cmd).not.toContain('--model');
270
+ });
271
+
272
+ test('claude CLI omits --model flag when model is not provided', () => {
273
+ const cmd = buildAgentCommand('claude', 'do stuff', 'plan');
274
+ expect(cmd).not.toContain('--model');
275
+ });
276
+
277
+ test('codex CLI includes -m flag when model is non-empty', () => {
278
+ const cmd = buildAgentCommand('codex', 'do stuff', 'plan', 'opus');
279
+ expect(cmd).toContain('-m opus');
280
+ expect(cmd).toContain('codex exec');
281
+ });
282
+
283
+ test('codex CLI omits -m flag when model is empty', () => {
284
+ const cmd = buildAgentCommand('codex', 'do stuff', 'interactive', '');
285
+ expect(cmd).not.toContain('-m ');
286
+ });
287
+
288
+ test('claude print mode includes --model flag', () => {
289
+ const cmd = buildAgentCommand('claude', 'do stuff', 'print', 'sonnet');
290
+ expect(cmd).toContain('--model sonnet');
291
+ expect(cmd).toContain('--print');
292
+ });
293
+ });
294
+
295
+ describe('cleanTerminalArtifacts', () => {
296
+ test('removes CLI status bar, permission toggle, box drawings, and header lines', () => {
297
+ const dirty = `=== PLAN START ===
298
+ Opus4.6(1Mcontext) │T-91EADD ░░░░░░░░░░6%
299
+ ⏵⏵bypasspermissionson (shift+tabtocycle)
300
+ SUMMARY: Add a Reports modal with per-repo task counts.
301
+ ⏵⏵bypasspermissionson (shift+tabtocycle)
302
+ BRANCH: feature/t-91eadd-reports-dashboard
303
+ ────────────────────────────────────────────────────────
304
+ Opus4.6(1Mcontext) │T-91EADD ░░░░░░░░░░6%
305
+ ⏵⏵bypasspermissionson (shift+tabtocycle)
306
+ FILES_TO_MODIFY:
307
+ - client/src/ReportsModal.jsx (new modal component)
308
+ ▐▛███▜▌ClaudeCodev2.1.76
309
+ ▝▜█████▛▘Opus4.6(1Mcontext)·ClaudeMax
310
+ ~/Developer/stilero/bankan/.data/workspaces/T-91EADD
311
+ STEPS:
312
+ 1. Create the ReportsModal component.
313
+
314
+ TESTS_NEEDED:
315
+ - Run npm run test
316
+ RISKS:
317
+ - none
318
+ === PLAN END ===`;
319
+
320
+ const cleaned = cleanTerminalArtifacts(dirty);
321
+ expect(cleaned).toContain('SUMMARY: Add a Reports modal');
322
+ expect(cleaned).toContain('BRANCH: feature/t-91eadd-reports-dashboard');
323
+ expect(cleaned).toContain('- client/src/ReportsModal.jsx');
324
+ expect(cleaned).toContain('1. Create the ReportsModal component.');
325
+ expect(cleaned).not.toContain('Opus4.6');
326
+ expect(cleaned).not.toContain('bypasspermission');
327
+ expect(cleaned).not.toContain('────');
328
+ expect(cleaned).not.toContain('▐▛███');
329
+ expect(cleaned).not.toContain('ClaudeCode');
330
+ expect(cleaned).not.toContain('ClaudeMax');
331
+ expect(cleaned).not.toContain('.data/workspaces');
332
+ expect(cleaned).not.toContain('❯');
333
+ });
334
+
335
+ test('strips trailing artifacts from content lines', () => {
336
+ // The terminal can put artifacts on the same line as real content
337
+ const dirty = '=== PLAN START === ❯ ──────────────────────────────────\nSUMMARY: Real plan.\n=== PLAN END ===';
338
+ const cleaned = cleanTerminalArtifacts(dirty);
339
+ expect(cleaned).toContain('=== PLAN START ===');
340
+ expect(cleaned).toContain('SUMMARY: Real plan.');
341
+ expect(cleaned).not.toContain('❯');
342
+ expect(cleaned).not.toContain('────');
343
+ });
344
+
345
+ test('preserves clean plan text unchanged', () => {
346
+ const clean = `=== PLAN START ===
347
+ SUMMARY: Add automated tests.
348
+ BRANCH: feature/add-tests
349
+ FILES_TO_MODIFY:
350
+ - server/src/workflow.test.js (expand coverage)
351
+ STEPS:
352
+ 1. Add tests for retry status edge cases.
353
+ TESTS_NEEDED:
354
+ - Run npm run test:server
355
+ RISKS:
356
+ - none
357
+ === PLAN END ===`;
358
+
359
+ expect(cleanTerminalArtifacts(clean)).toBe(clean);
360
+ });
361
+
362
+ test('removes embedded prompt echo and template block from captured plan', () => {
363
+ // When the real plan's END marker is lost in ANSI rendering, the extraction
364
+ // grabs from the first START to the template's END, including echoed prompt text.
365
+ const dirty = `=== PLAN START ===
366
+ SUMMARY: Add a Reports modal from the top bar.
367
+ BRANCH: feature/t-b60f78-repo-reports
368
+ FILES_TO_MODIFY:
369
+ - client/src/App.jsx (add Reports button)
370
+ STEPS:
371
+ 1. Create ReportsModal component
372
+ TESTS_NEEDED:
373
+ - Run npm run test
374
+ RISKS:
375
+ - none
376
+
377
+ Message from org:
378
+ Make sure to update CLAUDE.md
379
+
380
+ ❯ You are a senior software architect. A task has been assigned to you.
381
+ Repository: https://github.com/stilero/bankan
382
+ TASK ID: T-B60F78
383
+ TITLE: Reports
384
+
385
+ Plan Mode Instructions
386
+ - Do not edit files
387
+ Output ONLY in this exact format:
388
+
389
+ === PLAN START ===
390
+ SUMMARY: (one sentence describing what will be built)
391
+ BRANCH: (feature/t-b60f78-short-descriptive-slug)
392
+ FILES_TO_MODIFY:
393
+ - path/to/file.ts (reason for modification)
394
+ STEPS:
395
+ 1. (detailed, actionable step)
396
+ TESTS_NEEDED:
397
+ - (test description, or 'none')
398
+ RISKS:
399
+ - (potential issue or edge case, or 'none')
400
+ === PLAN END ===`;
401
+
402
+ const cleaned = cleanTerminalArtifacts(dirty);
403
+ expect(cleaned).toContain('SUMMARY: Add a Reports modal');
404
+ expect(cleaned).toContain('BRANCH: feature/t-b60f78-repo-reports');
405
+ expect(cleaned).toContain('client/src/App.jsx');
406
+ // Should NOT contain the echoed prompt template
407
+ expect(cleaned).not.toContain('(one sentence describing');
408
+ expect(cleaned).not.toContain('You are a senior software architect');
409
+ expect(cleaned).not.toContain('Plan Mode Instructions');
410
+ expect(cleaned).not.toContain('Message from org');
411
+ });
412
+
413
+ test('removes inline prompt character from content lines', () => {
414
+ const dirty = `=== PLAN START ===
415
+ SUMMARY: Fix the bug.
416
+ BRANCH: feature/fix ❯
417
+ FILES_TO_MODIFY:
418
+ - file.js (fix)
419
+ STEPS:
420
+ 1. Fix it
421
+ TESTS_NEEDED:
422
+ - none
423
+ RISKS:
424
+ - none
425
+ === PLAN END ===`;
426
+
427
+ const cleaned = cleanTerminalArtifacts(dirty);
428
+ expect(cleaned).toContain('BRANCH: feature/fix');
429
+ expect(cleaned).not.toContain('❯');
430
+ });
431
+ });
@@ -0,0 +1,49 @@
1
+ import { afterEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ const previousMode = process.env.BANKAN_RUNTIME_MODE;
4
+ const previousHome = process.env.BANKAN_HOME;
5
+
6
+ afterEach(() => {
7
+ if (previousMode === undefined) {
8
+ delete process.env.BANKAN_RUNTIME_MODE;
9
+ } else {
10
+ process.env.BANKAN_RUNTIME_MODE = previousMode;
11
+ }
12
+
13
+ if (previousHome === undefined) {
14
+ delete process.env.BANKAN_HOME;
15
+ } else {
16
+ process.env.BANKAN_HOME = previousHome;
17
+ }
18
+
19
+ vi.resetModules();
20
+ });
21
+
22
+ describe('runtime path resolution', () => {
23
+ test('uses repository .data in development mode', async () => {
24
+ delete process.env.BANKAN_RUNTIME_MODE;
25
+ delete process.env.BANKAN_HOME;
26
+ vi.resetModules();
27
+
28
+ const { getRuntimePaths } = await import('./paths.js');
29
+ const paths = getRuntimePaths();
30
+
31
+ expect(paths.packaged).toBe(false);
32
+ expect(paths.dataDir.endsWith('/.data')).toBe(true);
33
+ expect(paths.envFile.endsWith('/.env.local')).toBe(true);
34
+ });
35
+
36
+ test('uses BANKAN_HOME in packaged mode', async () => {
37
+ process.env.BANKAN_RUNTIME_MODE = 'packaged';
38
+ process.env.BANKAN_HOME = '/tmp/bankan-home';
39
+ vi.resetModules();
40
+
41
+ const { getAppDataDir, getEnvFilePath, getRuntimePaths } = await import('./paths.js');
42
+ const paths = getRuntimePaths();
43
+
44
+ expect(paths.packaged).toBe(true);
45
+ expect(getAppDataDir()).toBe('/tmp/bankan-home');
46
+ expect(getEnvFilePath()).toBe('/tmp/bankan-home/.env.local');
47
+ expect(paths.bridgesDir.endsWith('/bankan/terminal-bridges')).toBe(true);
48
+ });
49
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { createSessionEntry, getAgentStage } from './sessionHistory.js';
4
+
5
+ describe('session history helpers', () => {
6
+ test('maps agent ids to stages', () => {
7
+ expect(getAgentStage('plan-1')).toBe('planning');
8
+ expect(getAgentStage('imp-1')).toBe('implementation');
9
+ expect(getAgentStage('rev-1')).toBe('review');
10
+ expect(getAgentStage('orch')).toBe('unknown');
11
+ });
12
+
13
+ test('creates session entries with stable shape', () => {
14
+ const entry = createSessionEntry({
15
+ id: 'imp-1',
16
+ name: 'Implementor 1',
17
+ role: 'Code Generation',
18
+ tokens: 320,
19
+ }, {
20
+ taskId: 'T-123',
21
+ outcome: 'blocked',
22
+ transcript: 'Need input',
23
+ finishedAt: '2026-03-15T12:00:00.000Z',
24
+ });
25
+
26
+ expect(entry).toEqual({
27
+ id: 'imp-1:2026-03-15T12:00:00.000Z',
28
+ agentId: 'imp-1',
29
+ agentName: 'Implementor 1',
30
+ role: 'Code Generation',
31
+ stage: 'implementation',
32
+ taskId: 'T-123',
33
+ outcome: 'blocked',
34
+ finishedAt: '2026-03-15T12:00:00.000Z',
35
+ transcript: 'Need input',
36
+ tokens: 320,
37
+ });
38
+ });
39
+ });
@@ -212,10 +212,9 @@ class TaskStore {
212
212
  if (!task) return null;
213
213
  if (typeof totalTokens !== 'number' || totalTokens < task.totalTokens) return task;
214
214
  task.totalTokens = totalTokens;
215
- task.updatedAt = new Date().toISOString();
215
+ // Intentionally skip updatedAt and tasks:changed — token counts are
216
+ // telemetry and should not affect sort order or trigger full re-renders.
216
217
  this._save();
217
- bus.emit('task:updated', task);
218
- bus.emit('tasks:changed', this.tasks);
219
218
  return task;
220
219
  }
221
220