Skip to content

Commit cd03e6a

Browse files
chore: fix linting errors
1 parent d86a5c4 commit cd03e6a

File tree

9 files changed

+174
-56
lines changed

9 files changed

+174
-56
lines changed

src/features/chat/components/AttachmentsArea.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,8 @@ export const AttachmentsArea: React.FC<AttachmentsAreaProps> = ({
6464
});
6565
}, [attachments, provider, model]);
6666

67-
// Don't render if no attachments
68-
if (attachmentList.length === 0) {
69-
return null;
70-
}
71-
7267
// Build footer text with keyboard shortcuts
68+
// NOTE: This hook must be called before any early return to maintain consistent hook count
7369
const footerText = useMemo(() => {
7470
return buildFooterText([
7571
{ shortcut: keyBindings.navigateLeft, label: 'prev' },
@@ -78,6 +74,11 @@ export const AttachmentsArea: React.FC<AttachmentsAreaProps> = ({
7874
]);
7975
}, [keyBindings]);
8076

77+
// Don't render if no attachments
78+
if (attachmentList.length === 0) {
79+
return null;
80+
}
81+
8182
// Determine if we need scroll indicators
8283
const maxVisibleItems = 3; // Max items to show before scrolling
8384
const needsScrolling = attachmentList.length > maxVisibleItems;

src/shared/utils/bracketedPaste.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ export function detectPasteHeuristic(newValue: string, oldValue: string): boolea
139139
* @returns Input with paste markers removed
140140
*/
141141
export function stripBracketedPasteMarkers(input: string): string {
142-
return input.replace(new RegExp(`${PASTE_START}|${PASTE_END}`, 'g'), '');
142+
// Escape the special regex characters in the paste markers
143+
const escapeRegex = (str: string) => str.replace(/[[\]\\]/g, '\\$&');
144+
const pattern = `${escapeRegex(PASTE_START)}|${escapeRegex(PASTE_END)}`;
145+
return input.replace(new RegExp(pattern, 'g'), '');
143146
}
144147

145148
/**

tests/unit/cli/components/InputBox.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ describe('InputBox Autocomplete Logic', () => {
330330

331331
expect(state.showAutocomplete).toBe(true);
332332
expect(state.isParameterMode).toBe(true);
333-
expect(state.parameterSuggestions).toHaveLength(10); // Updated: now have 10 providers
333+
expect(state.parameterSuggestions).toHaveLength(11); // Updated: now have 11 providers
334334
expect(state.parameterSuggestions).toContain('deepseek');
335335
expect(state.parameterSuggestions).toContain('anthropic');
336336
expect(state.parameterSuggestions).toContain('openai');

tests/unit/cli/keyboard/KeyboardEventBus.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,10 +368,11 @@ describe('KeyboardEventBus', () => {
368368
expect(eventBus.isActionRelevantInContext('interrupt')).toBe(true);
369369
});
370370

371-
it('should NOT be relevant when agent is not running', () => {
371+
it('should always be relevant (can exit app when not running, interrupt when running)', () => {
372372
eventBus.updateContext({ isAgentRunning: false });
373373

374-
expect(eventBus.isActionRelevantInContext('interrupt')).toBe(false);
374+
// interrupt is always relevant - exits app when not running, interrupts agent when running
375+
expect(eventBus.isActionRelevantInContext('interrupt')).toBe(true);
375376
});
376377
});
377378

tests/unit/core/PermissionManager.test.ts

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,155 @@
22
* Unit tests for PermissionManager
33
*/
44

5-
import { describe, it, expect, beforeEach } from 'vitest';
6-
import { PermissionManager } from '@/features/permissions/manager/PermissionManager.js';
5+
import { describe, it, expect, beforeEach, vi } from 'vitest';
6+
import { PermissionManager } from '@codedir/mimir-agents';
7+
import type {
8+
PermissionManagerConfig,
9+
PermissionRequest,
10+
IAuditLogger,
11+
AuditLogEntry,
12+
} from '@codedir/mimir-agents';
713

814
describe('PermissionManager', () => {
915
let manager: PermissionManager;
16+
let auditLog: AuditLogEntry[];
17+
let mockAuditLogger: IAuditLogger;
1018

1119
beforeEach(() => {
12-
manager = new PermissionManager();
20+
auditLog = [];
21+
mockAuditLogger = {
22+
log: vi.fn(async (entry: AuditLogEntry) => {
23+
auditLog.push(entry);
24+
}),
25+
};
26+
27+
const config: PermissionManagerConfig = {
28+
allowlist: [],
29+
blocklist: [],
30+
acceptRiskLevel: 'low',
31+
autoAccept: true,
32+
auditLogger: mockAuditLogger,
33+
};
34+
manager = new PermissionManager(config);
1335
});
1436

1537
it('should allow low risk commands by default', async () => {
16-
const result = await manager.checkPermission('ls -la');
38+
const request: PermissionRequest = { type: 'bash', command: 'ls -la' };
39+
const result = await manager.checkPermission(request);
1740
expect(result.allowed).toBe(true);
1841
});
1942

2043
it('should deny high risk commands by default', async () => {
21-
const result = await manager.checkPermission('rm -rf /');
44+
const request: PermissionRequest = { type: 'bash', command: 'rm -rf /' };
45+
const result = await manager.checkPermission(request);
2246
expect(result.allowed).toBe(false);
2347
});
2448

2549
it('should respect allowlist', async () => {
26-
manager.addToAllowlist('rm -rf node_modules');
27-
const result = await manager.checkPermission('rm -rf node_modules');
50+
const config: PermissionManagerConfig = {
51+
allowlist: ['rm -rf node_modules'],
52+
blocklist: [],
53+
acceptRiskLevel: 'low',
54+
autoAccept: true,
55+
auditLogger: mockAuditLogger,
56+
};
57+
const managerWithAllowlist = new PermissionManager(config);
58+
59+
const request: PermissionRequest = { type: 'bash', command: 'rm -rf node_modules' };
60+
const result = await managerWithAllowlist.checkPermission(request);
2861
expect(result.allowed).toBe(true);
2962
});
3063

3164
it('should respect blocklist', async () => {
32-
manager.addToBlocklist('git push');
33-
const result = await manager.checkPermission('git push');
65+
const config: PermissionManagerConfig = {
66+
allowlist: [],
67+
blocklist: ['git push'],
68+
acceptRiskLevel: 'high', // Allow high risk, but blocklist takes priority
69+
autoAccept: true,
70+
auditLogger: mockAuditLogger,
71+
};
72+
const managerWithBlocklist = new PermissionManager(config);
73+
74+
const request: PermissionRequest = { type: 'bash', command: 'git push' };
75+
const result = await managerWithBlocklist.checkPermission(request);
3476
expect(result.allowed).toBe(false);
3577
});
3678

3779
it('should maintain audit log', async () => {
38-
await manager.checkPermission('ls -la');
39-
await manager.checkPermission('rm -rf /');
80+
const request1: PermissionRequest = { type: 'bash', command: 'ls -la' };
81+
const request2: PermissionRequest = { type: 'bash', command: 'rm -rf /' };
82+
83+
await manager.checkPermission(request1);
84+
await manager.checkPermission(request2);
85+
86+
expect(auditLog).toHaveLength(2);
87+
expect(auditLog[0]?.operation).toBe('ls -la');
88+
expect(auditLog[1]?.operation).toBe('rm -rf /');
89+
});
90+
91+
describe('risk level acceptance', () => {
92+
it('should auto-accept commands at or below accept risk level', async () => {
93+
const config: PermissionManagerConfig = {
94+
allowlist: [],
95+
blocklist: [],
96+
acceptRiskLevel: 'medium',
97+
autoAccept: true,
98+
};
99+
const managerMedium = new PermissionManager(config);
100+
101+
// Low risk - should be accepted
102+
const lowRisk: PermissionRequest = { type: 'bash', command: 'ls -la' };
103+
const lowResult = await managerMedium.checkPermission(lowRisk);
104+
expect(lowResult.allowed).toBe(true);
105+
106+
// Medium risk (npm install) - should be accepted
107+
const mediumRisk: PermissionRequest = { type: 'bash', command: 'npm install express' };
108+
const mediumResult = await managerMedium.checkPermission(mediumRisk);
109+
expect(mediumResult.allowed).toBe(true);
110+
});
111+
112+
it('should deny commands above accept risk level', async () => {
113+
const config: PermissionManagerConfig = {
114+
allowlist: [],
115+
blocklist: [],
116+
acceptRiskLevel: 'low',
117+
autoAccept: true,
118+
};
119+
const managerLow = new PermissionManager(config);
120+
121+
// High risk - should be denied
122+
const highRisk: PermissionRequest = { type: 'bash', command: 'rm -rf ./node_modules' };
123+
const highResult = await managerLow.checkPermission(highRisk);
124+
expect(highResult.allowed).toBe(false);
125+
});
126+
127+
it('should deny all when autoAccept is false', async () => {
128+
const config: PermissionManagerConfig = {
129+
allowlist: [],
130+
blocklist: [],
131+
acceptRiskLevel: 'critical', // Even with high accept level
132+
autoAccept: false, // Auto-accept disabled
133+
};
134+
const managerNoAuto = new PermissionManager(config);
135+
136+
const lowRisk: PermissionRequest = { type: 'bash', command: 'ls -la' };
137+
const result = await managerNoAuto.checkPermission(lowRisk);
138+
expect(result.allowed).toBe(false);
139+
});
140+
});
141+
142+
describe('file operations', () => {
143+
it('should handle file read requests', async () => {
144+
const request: PermissionRequest = { type: 'file_read', path: '/etc/passwd' };
145+
const result = await manager.checkPermission(request);
146+
// Low risk file read with low accept level should be allowed
147+
expect(result.riskLevel).toBeDefined();
148+
});
40149

41-
const log = manager.getAuditLog();
42-
expect(log).toHaveLength(2);
43-
expect(log[0]?.command).toBe('ls -la');
44-
expect(log[1]?.command).toBe('rm -rf /');
150+
it('should handle file write requests', async () => {
151+
const request: PermissionRequest = { type: 'file_write', path: './output.txt' };
152+
const result = await manager.checkPermission(request);
153+
expect(result.riskLevel).toBeDefined();
154+
});
45155
});
46156
});

tests/unit/core/RiskAssessor.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { describe, it, expect } from 'vitest';
6-
import { RiskAssessor } from '@/features/permissions/assessor/RiskAssessor.js';
6+
import { RiskAssessor } from '@codedir/mimir-agents';
77

88
describe('RiskAssessor', () => {
99
const assessor = new RiskAssessor();

tests/unit/features/chat/utils/AttachmentManager.test.ts

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -321,14 +321,14 @@ describe('AttachmentManager', () => {
321321
describe('expandForAPI', () => {
322322
it('should handle empty message and no attachments', () => {
323323
const result = manager.expandForAPI('');
324-
expect(result.content).toEqual([]);
324+
expect(result).toEqual([]);
325325
});
326326

327327
it('should include message text only', () => {
328328
const result = manager.expandForAPI('Hello, world!');
329329

330-
expect(result.content).toHaveLength(1);
331-
expect(result.content[0]).toEqual({
330+
expect(result).toHaveLength(1);
331+
expect(result[0]).toEqual({
332332
type: 'text',
333333
text: 'Hello, world!',
334334
});
@@ -344,12 +344,12 @@ describe('AttachmentManager', () => {
344344
manager.addTextAttachment('Pasted content', metadata);
345345
const result = manager.expandForAPI('Main message');
346346

347-
expect(result.content).toHaveLength(2);
348-
expect(result.content[0].type).toBe('text');
349-
expect(result.content[0].text).toBe('Main message');
350-
expect(result.content[1].type).toBe('text');
351-
expect(result.content[1].text).toContain('[Pasted text #1]');
352-
expect(result.content[1].text).toContain('Pasted content');
347+
expect(result).toHaveLength(2);
348+
expect(result[0].type).toBe('text');
349+
expect((result[0] as { type: 'text'; text: string }).text).toBe('Main message');
350+
expect(result[1].type).toBe('text');
351+
expect((result[1] as { type: 'text'; text: string }).text).toContain('[Pasted text #1]');
352+
expect((result[1] as { type: 'text'; text: string }).text).toContain('Pasted content');
353353
});
354354

355355
it('should expand image attachments as base64', () => {
@@ -358,13 +358,11 @@ describe('AttachmentManager', () => {
358358

359359
const result = manager.expandForAPI('Check this image');
360360

361-
expect(result.content).toHaveLength(2);
362-
expect(result.content[1].type).toBe('image');
363-
expect(result.content[1].source).toEqual({
364-
type: 'base64',
365-
media_type: 'image/png',
366-
data: imageData.toString('base64'),
367-
});
361+
expect(result).toHaveLength(2);
362+
expect(result[1].type).toBe('image_url');
363+
expect((result[1] as { type: 'image_url'; image_url: { url: string } }).image_url.url).toBe(
364+
`data:image/png;base64,${imageData.toString('base64')}`
365+
);
368366
});
369367

370368
it('should handle mixed text and image attachments', () => {
@@ -381,12 +379,12 @@ describe('AttachmentManager', () => {
381379

382380
const result = manager.expandForAPI('Message');
383381

384-
expect(result.content).toHaveLength(5); // 1 message + 4 attachments
385-
expect(result.content[0].type).toBe('text'); // Main message
386-
expect(result.content[1].type).toBe('text'); // Text paste
387-
expect(result.content[2].type).toBe('image'); // Image 1
388-
expect(result.content[3].type).toBe('text'); // More text
389-
expect(result.content[4].type).toBe('image'); // Image 2
382+
expect(result).toHaveLength(5); // 1 message + 4 attachments
383+
expect(result[0].type).toBe('text'); // Main message
384+
expect(result[1].type).toBe('text'); // Text paste
385+
expect(result[2].type).toBe('image_url'); // Image 1
386+
expect(result[3].type).toBe('text'); // More text
387+
expect(result[4].type).toBe('image_url'); // Image 2
390388
});
391389

392390
it('should preserve attachment order by creation time', () => {
@@ -402,10 +400,10 @@ describe('AttachmentManager', () => {
402400

403401
const result = manager.expandForAPI('');
404402

405-
expect(result.content).toHaveLength(3);
406-
expect(result.content[0].text).toContain('First');
407-
expect(result.content[1].text).toContain('Second');
408-
expect(result.content[2].text).toContain('Third');
403+
expect(result).toHaveLength(3);
404+
expect((result[0] as { type: 'text'; text: string }).text).toContain('First');
405+
expect((result[1] as { type: 'text'; text: string }).text).toContain('Second');
406+
expect((result[2] as { type: 'text'; text: string }).text).toContain('Third');
409407
});
410408
});
411409

tests/unit/shared/utils/CredentialsManager.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
*/
44

55
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6-
import { CredentialsManager, type StorageLocation } from '@/shared/utils/CredentialsManager.js';
76
import { promises as fs } from 'fs';
87
import { tmpdir } from 'os';
98
import { join } from 'path';
10-
import * as keytar from 'keytar';
119

12-
// Test home directory path - defined before mock
13-
const testHomedir = join(tmpdir(), 'mimir-test-home');
10+
// Use vi.hoisted to define testHomedir BEFORE mocks are processed
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
const testHomedir = vi.hoisted(() =>
13+
require('path').join(require('os').tmpdir(), 'mimir-test-home')
14+
);
1415

1516
// Mock keytar
1617
vi.mock('keytar', () => ({
@@ -29,6 +30,10 @@ vi.mock('os', async () => {
2930
};
3031
});
3132

33+
// Import after mocks are set up
34+
import { CredentialsManager, type StorageLocation } from '@/shared/utils/CredentialsManager.js';
35+
import * as keytar from 'keytar';
36+
3237
describe('CredentialsManager', () => {
3338
let credentialsManager: CredentialsManager;
3439
let testDir: string;

tests/unit/shared/utils/bracketedPaste.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ describe('Bracketed Paste Utilities', () => {
213213

214214
it('should detect 11 char delta as paste', () => {
215215
const oldValue = 'Hello';
216-
const newValue = 'Hello123456'; // +11 chars
216+
const newValue = 'Hello12345678901'; // +11 chars (16 - 5 = 11)
217217
expect(detectPasteHeuristic(newValue, oldValue)).toBe(true);
218218
});
219219

0 commit comments

Comments
 (0)