@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.
- package/README.md +17 -1
- package/bin/bankan.js +1 -1
- package/client/dist/assets/{index-pUZAEGtO.js → index-CHxyLFN_.js} +17 -15
- package/client/dist/index.html +1 -1
- package/docs/images/workflow/taskflow_animated.gif +0 -0
- package/package.json +14 -2
- package/scripts/setup.js +1 -5
- package/server/src/agents.js +123 -4
- package/server/src/agents.test.js +462 -76
- package/server/src/config.js +11 -4
- package/server/src/config.test.js +170 -0
- package/server/src/index.js +11 -2
- package/server/src/linting.test.js +37 -0
- package/server/src/orchestrator.js +279 -99
- package/server/src/orchestrator.test.js +431 -0
- package/server/src/paths.test.js +49 -0
- package/server/src/sessionHistory.test.js +39 -0
- package/server/src/store.js +2 -3
- package/server/src/store.test.js +186 -0
- package/server/src/workflow.js +23 -7
- package/server/src/workflow.test.js +216 -71
|
@@ -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
|
+
});
|
package/server/src/store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|