@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,170 @@
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('config settings lifecycle', () => {
13
+ test('loadSettings falls back to defaults and normalizes repo selection', async () => {
14
+ harness = createRuntimeHarness();
15
+ const configModule = await harness.importModule('./src/config.js');
16
+
17
+ const defaults = configModule.getDefaults();
18
+ expect(defaults.agents.planners.max).toBe(4);
19
+ expect(configModule.loadSettings().defaultRepoPath).toBe(defaults.defaultRepoPath);
20
+ expect(configModule.getWorkspacesDir()).toContain(harness.runtimeDir);
21
+ });
22
+
23
+ test('saveSettings persists normalized settings shape', async () => {
24
+ harness = createRuntimeHarness();
25
+ const configModule = await harness.importModule('./src/config.js');
26
+
27
+ configModule.saveSettings({
28
+ repos: ['/repo-a', '/repo-b'],
29
+ defaultRepoPath: '/missing',
30
+ reposDir: '/legacy-root',
31
+ workspaceRoot: '',
32
+ agents: {
33
+ planners: { max: 0, cli: 'claude', count: 1 },
34
+ implementors: { max: 2, cli: 'codex', count: 8 },
35
+ reviewers: { max: 1, cli: 'claude', count: 3 },
36
+ },
37
+ prompts: {
38
+ implementation: 'Custom implementation prompt',
39
+ },
40
+ });
41
+
42
+ const loaded = configModule.loadSettings();
43
+ expect(loaded.defaultRepoPath).toBe('/repo-a');
44
+ expect(loaded.workspaceRoot).toBe('/legacy-root');
45
+ expect(loaded.agents.planners.count).toBeUndefined();
46
+ expect(loaded.prompts.planning).toContain('Plan Mode Instructions');
47
+ expect(loaded.prompts.implementation).toBe('Custom implementation prompt');
48
+ });
49
+
50
+ test('validateSettings reports invalid values across roles and prompts', async () => {
51
+ harness = createRuntimeHarness();
52
+ const { validateSettings } = await harness.importModule('./src/config.js');
53
+
54
+ const errors = validateSettings({
55
+ repos: ['/repo-a'],
56
+ defaultRepoPath: '/repo-b',
57
+ workspaceRoot: '',
58
+ agents: {
59
+ planners: { max: -1, cli: 'bad-cli' },
60
+ implementors: { max: 0, cli: 'codex' },
61
+ reviewers: { max: 11, cli: 'claude' },
62
+ },
63
+ prompts: {
64
+ planning: 'ok',
65
+ implementation: 12,
66
+ },
67
+ });
68
+
69
+ expect(errors).toContain('workspaceRoot is required');
70
+ expect(errors).toContain('defaultRepoPath must match one of the configured repos');
71
+ expect(errors).toContain('planners.max must be between 0 and 10');
72
+ expect(errors).toContain('planners.cli must be one of: claude, codex');
73
+ expect(errors).toContain('implementors.max must be between 1 and 10');
74
+ expect(errors).toContain('reviewers.max must be between 0 and 10');
75
+ expect(errors).toContain('prompts.implementation must be a string');
76
+ expect(errors).toContain('prompts.review must be a string');
77
+ });
78
+
79
+ test('reads env defaults for repos, port, and legacy implementor cli', async () => {
80
+ harness = createRuntimeHarness();
81
+ const { writeFileSync } = await import('node:fs');
82
+ writeFileSync(`${harness.runtimeDir}/.env.local`, [
83
+ 'PORT=4010',
84
+ 'REPOS=/repo-a,/repo-b',
85
+ 'IMPLEMENTOR_1_CLI=codex',
86
+ '# comment',
87
+ 'MALFORMED',
88
+ ].join('\n'));
89
+
90
+ const configModule = await harness.importModule('./src/config.js');
91
+
92
+ expect(configModule.default.PORT).toBe(4010);
93
+ expect(configModule.default.REPOS).toEqual(['/repo-a', '/repo-b']);
94
+ expect(configModule.getDefaults().agents.implementors.cli).toBe('codex');
95
+ });
96
+
97
+ test('defaults include model field for all agent roles', async () => {
98
+ harness = createRuntimeHarness();
99
+ const { getDefaults } = await harness.importModule('./src/config.js');
100
+ const defaults = getDefaults();
101
+ for (const role of ['planners', 'implementors', 'reviewers']) {
102
+ expect(defaults.agents[role].model).toBe('');
103
+ }
104
+ });
105
+
106
+ test('normalizeSettingsShape backfills missing model field', async () => {
107
+ harness = createRuntimeHarness();
108
+ const configModule = await harness.importModule('./src/config.js');
109
+
110
+ configModule.saveSettings({
111
+ repos: ['/repo-a'],
112
+ defaultRepoPath: '/repo-a',
113
+ workspaceRoot: '/tmp/ws',
114
+ agents: {
115
+ planners: { max: 2, cli: 'claude' },
116
+ implementors: { max: 4, cli: 'claude' },
117
+ reviewers: { max: 2, cli: 'claude' },
118
+ },
119
+ prompts: {
120
+ planning: 'p', implementation: 'i', review: 'r',
121
+ },
122
+ });
123
+
124
+ const loaded = configModule.loadSettings();
125
+ expect(loaded.agents.planners.model).toBe('');
126
+ expect(loaded.agents.implementors.model).toBe('');
127
+ expect(loaded.agents.reviewers.model).toBe('');
128
+ });
129
+
130
+ test('validateSettings accepts valid model strings and rejects non-strings', async () => {
131
+ harness = createRuntimeHarness();
132
+ const { validateSettings } = await harness.importModule('./src/config.js');
133
+
134
+ const base = {
135
+ repos: ['/repo'],
136
+ defaultRepoPath: '/repo',
137
+ workspaceRoot: '/tmp/ws',
138
+ agents: {
139
+ planners: { max: 1, cli: 'claude', model: 'haiku' },
140
+ implementors: { max: 1, cli: 'claude', model: 'opus' },
141
+ reviewers: { max: 1, cli: 'claude', model: '' },
142
+ },
143
+ prompts: { planning: 'p', implementation: 'i', review: 'r' },
144
+ };
145
+ expect(validateSettings(base)).toEqual([]);
146
+
147
+ const bad = JSON.parse(JSON.stringify(base));
148
+ bad.agents.planners.model = 42;
149
+ const errors = validateSettings(bad);
150
+ expect(errors).toContain('planners.model must be a string');
151
+ });
152
+
153
+ test('returns early when agents are missing and validates repo types', async () => {
154
+ harness = createRuntimeHarness();
155
+ const { validateSettings } = await harness.importModule('./src/config.js');
156
+
157
+ expect(validateSettings({})).toEqual(['Missing agents configuration']);
158
+ expect(validateSettings({
159
+ agents: {
160
+ planners: { max: 0, cli: 'claude' },
161
+ implementors: { max: 1, cli: 'codex' },
162
+ reviewers: { max: 0, cli: 'claude' },
163
+ },
164
+ workspaceRoot: '/tmp/workspaces',
165
+ repos: 'not-an-array',
166
+ defaultRepoPath: 123,
167
+ prompts: {},
168
+ })).toContain('repos must be an array');
169
+ });
170
+ });
@@ -94,7 +94,7 @@ app.get('/api/browse-dir', (req, res) => {
94
94
  parent: parent !== absPath ? parent : null,
95
95
  dirs,
96
96
  });
97
- } catch (err) {
97
+ } catch {
98
98
  res.status(403).json({ error: 'Permission denied' });
99
99
  }
100
100
  });
@@ -186,7 +186,7 @@ function sendToClient(ws, type, payload) {
186
186
  }
187
187
 
188
188
  function shellQuote(value) {
189
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
189
+ return `'${String(value).replace(/'/g, '\'\\\'\'')}'`;
190
190
  }
191
191
 
192
192
  function ensureBridgeFiles() {
@@ -607,6 +607,15 @@ wss.on('connection', (ws) => {
607
607
  }
608
608
  break;
609
609
  }
610
+ case 'RESIZE_TERMINAL': {
611
+ const { agentId, cols, rows } = msg.payload || {};
612
+ const agent = agentManager.get(agentId);
613
+ if (agent) {
614
+ agent.resize(cols, rows);
615
+ bus.emit('agent:updated', agent.getStatus());
616
+ }
617
+ break;
618
+ }
610
619
  case 'OPEN_TASK_WORKSPACE': {
611
620
  const { taskId } = msg.payload || {};
612
621
  const result = openTaskWorkspace(taskId);
@@ -0,0 +1,37 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { describe, expect, test } from 'vitest';
6
+
7
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
8
+
9
+ describe('linting integration', () => {
10
+ test('exposes a working root lint command', () => {
11
+ expect(() => execFileSync('npm', ['run', 'lint'], {
12
+ cwd: repoRoot,
13
+ stdio: 'pipe',
14
+ })).not.toThrow();
15
+ });
16
+
17
+ test('checks in Claude project hooks for lint enforcement', () => {
18
+ const settingsPath = resolve(repoRoot, '.claude', 'settings.json');
19
+
20
+ expect(existsSync(settingsPath)).toBe(true);
21
+
22
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
23
+ const postToolUseHooks = settings.hooks?.PostToolUse || [];
24
+ const taskCompletedHooks = settings.hooks?.TaskCompleted || [];
25
+
26
+ expect(postToolUseHooks).toHaveLength(1);
27
+ expect(postToolUseHooks[0].matcher).toBe('Edit|Write|MultiEdit');
28
+ expect(postToolUseHooks[0].hooks[0].type).toBe('command');
29
+ expect(postToolUseHooks[0].hooks[0].command).toContain('.claude/hooks/run-lint-on-edit.sh');
30
+ expect(postToolUseHooks[0].hooks[0].timeout).toBe(60);
31
+
32
+ expect(taskCompletedHooks).toHaveLength(1);
33
+ expect(taskCompletedHooks[0].hooks[0].type).toBe('command');
34
+ expect(taskCompletedHooks[0].hooks[0].command).toContain('.claude/hooks/run-lint-on-task-complete.sh');
35
+ expect(taskCompletedHooks[0].hooks[0].timeout).toBe(300);
36
+ });
37
+ });