onbuzz 3.6.1 → 3.6.3

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.
Files changed (84) hide show
  1. package/package.json +1 -1
  2. package/src/__test-utils__/fixtures/malformedJson.js +31 -0
  3. package/src/__test-utils__/globalSetup.js +9 -0
  4. package/src/__test-utils__/globalTeardown.js +12 -0
  5. package/src/__test-utils__/mockFactories.js +101 -0
  6. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
  7. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
  8. package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
  9. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
  10. package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
  11. package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
  12. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
  13. package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
  14. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
  15. package/src/core/__tests__/agentPool.test.js +601 -0
  16. package/src/core/__tests__/agentScheduler.test.js +576 -0
  17. package/src/core/__tests__/contextManager.test.js +252 -0
  18. package/src/core/__tests__/flowExecutor.test.js +262 -0
  19. package/src/core/__tests__/messageProcessor.test.js +627 -0
  20. package/src/core/__tests__/orchestrator.test.js +257 -0
  21. package/src/core/__tests__/stateManager.test.js +375 -0
  22. package/src/core/agentPool.js +11 -1
  23. package/src/index.js +25 -9
  24. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
  25. package/src/services/__tests__/agentActivityService.test.js +319 -0
  26. package/src/services/__tests__/apiKeyManager.test.js +206 -0
  27. package/src/services/__tests__/benchmarkService.test.js +184 -0
  28. package/src/services/__tests__/budgetService.test.js +211 -0
  29. package/src/services/__tests__/contextInjectionService.test.js +205 -0
  30. package/src/services/__tests__/conversationCompactionService.test.js +280 -0
  31. package/src/services/__tests__/credentialVault.test.js +469 -0
  32. package/src/services/__tests__/errorHandler.test.js +314 -0
  33. package/src/services/__tests__/fileAttachmentService.test.js +278 -0
  34. package/src/services/__tests__/flowContextService.test.js +199 -0
  35. package/src/services/__tests__/memoryService.test.js +450 -0
  36. package/src/services/__tests__/modelRouterService.test.js +388 -0
  37. package/src/services/__tests__/modelsService.test.js +261 -0
  38. package/src/services/__tests__/portRegistry.test.js +123 -0
  39. package/src/services/__tests__/projectDetector.test.js +34 -0
  40. package/src/services/__tests__/promptService.test.js +242 -0
  41. package/src/services/__tests__/qualityInspector.test.js +97 -0
  42. package/src/services/__tests__/scheduleService.test.js +308 -0
  43. package/src/services/__tests__/serviceRegistry.test.js +74 -0
  44. package/src/services/__tests__/skillsService.test.js +402 -0
  45. package/src/services/__tests__/tokenCountingService.test.js +48 -0
  46. package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
  47. package/src/tools/__tests__/agentDelayTool.test.js +342 -0
  48. package/src/tools/__tests__/asyncToolManager.test.js +344 -0
  49. package/src/tools/__tests__/baseTool.test.js +420 -0
  50. package/src/tools/__tests__/codeMapTool.test.js +348 -0
  51. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
  52. package/src/tools/__tests__/fileSystemTool.test.js +717 -0
  53. package/src/tools/__tests__/fileTreeTool.test.js +274 -0
  54. package/src/tools/__tests__/helpTool.test.js +204 -0
  55. package/src/tools/__tests__/jobDoneTool.test.js +296 -0
  56. package/src/tools/__tests__/memoryTool.test.js +297 -0
  57. package/src/tools/__tests__/seekTool.test.js +282 -0
  58. package/src/tools/__tests__/skillsTool.test.js +226 -0
  59. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
  60. package/src/tools/__tests__/taskManagerTool.test.js +725 -0
  61. package/src/tools/__tests__/terminalTool.test.js +384 -0
  62. package/src/tools/__tests__/userPromptTool.test.js +297 -0
  63. package/src/tools/__tests__/webTool.e2e.test.js +25 -11
  64. package/src/tools/webTool.js +6 -12
  65. package/src/types/__tests__/agent.test.js +499 -0
  66. package/src/types/__tests__/contextReference.test.js +606 -0
  67. package/src/types/__tests__/conversation.test.js +555 -0
  68. package/src/types/__tests__/toolCommand.test.js +584 -0
  69. package/src/types/contextReference.js +1 -1
  70. package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
  71. package/src/utilities/__tests__/configManager.test.js +397 -0
  72. package/src/utilities/__tests__/constants.test.js +49 -0
  73. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
  74. package/src/utilities/__tests__/fileProcessor.test.js +104 -0
  75. package/src/utilities/__tests__/jsonRepair.test.js +104 -0
  76. package/src/utilities/__tests__/logger.test.js +129 -0
  77. package/src/utilities/__tests__/platformUtils.test.js +87 -0
  78. package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
  79. package/src/utilities/__tests__/tagParser.test.js +887 -0
  80. package/src/utilities/__tests__/toolConstants.test.js +94 -0
  81. package/src/utilities/tagParser.js +2 -2
  82. package/src/tools/browserTool.js +0 -897
  83. package/src/utilities/platformUtils.test.js +0 -98
  84. /package/src/tools/{filesystemTool.js → fileSystemTool.js} +0 -0
@@ -0,0 +1,271 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock eslint
5
+ const mockLintText = jest.fn();
6
+ const MockESLint = jest.fn().mockImplementation(() => ({
7
+ lintText: mockLintText
8
+ }));
9
+ jest.unstable_mockModule('eslint', () => ({
10
+ ESLint: MockESLint
11
+ }));
12
+
13
+ // Mock fs/promises
14
+ const mockAccess = jest.fn();
15
+ jest.unstable_mockModule('fs/promises', () => ({
16
+ default: { access: mockAccess },
17
+ access: mockAccess
18
+ }));
19
+
20
+ // Mock constants
21
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
22
+ STATIC_ANALYSIS: {
23
+ SEVERITY: {
24
+ CRITICAL: 'critical',
25
+ ERROR: 'error',
26
+ WARNING: 'warning',
27
+ INFO: 'info',
28
+ SUGGESTION: 'suggestion'
29
+ },
30
+ CATEGORY: {
31
+ SYNTAX: 'syntax',
32
+ TYPE: 'type',
33
+ IMPORT: 'import',
34
+ STYLE: 'style',
35
+ SECURITY: 'security',
36
+ PERFORMANCE: 'performance',
37
+ BEST_PRACTICE: 'best_practice'
38
+ }
39
+ }
40
+ }));
41
+
42
+ const { default: ESLintAnalyzer } = await import('../ESLintAnalyzer.js');
43
+
44
+ describe('ESLintAnalyzer', () => {
45
+ let analyzer;
46
+ let logger;
47
+
48
+ beforeEach(() => {
49
+ logger = createMockLogger();
50
+ analyzer = new ESLintAnalyzer(logger);
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ // ── Constructor ──
55
+ test('constructor initializes with logger and cache', () => {
56
+ expect(analyzer.logger).toBe(logger);
57
+ expect(analyzer.eslintCache).toBeInstanceOf(Map);
58
+ });
59
+
60
+ test('constructor works without logger', () => {
61
+ const a = new ESLintAnalyzer();
62
+ expect(a.logger).toBeNull();
63
+ });
64
+
65
+ // ── getSupportedExtensions ──
66
+ test('getSupportedExtensions returns JS extensions', () => {
67
+ const exts = analyzer.getSupportedExtensions();
68
+ expect(exts).toContain('.js');
69
+ expect(exts).toContain('.jsx');
70
+ expect(exts).toContain('.mjs');
71
+ expect(exts).toContain('.cjs');
72
+ });
73
+
74
+ // ── supportsAutoFix ──
75
+ test('supportsAutoFix returns true', () => {
76
+ expect(analyzer.supportsAutoFix()).toBe(true);
77
+ });
78
+
79
+ // ── analyze ──
80
+ test('analyze returns diagnostics for linted code', async () => {
81
+ mockLintText.mockResolvedValue([{
82
+ messages: [
83
+ { line: 1, column: 5, severity: 2, ruleId: 'no-undef', message: 'x is not defined' },
84
+ { line: 2, column: 1, severity: 1, ruleId: 'semi', message: 'Missing semicolon' }
85
+ ]
86
+ }]);
87
+
88
+ const result = await analyzer.analyze('test.js', 'x\ny');
89
+ expect(result.length).toBe(2);
90
+ expect(result[0].severity).toBe('error');
91
+ expect(result[0].rule).toBe('no-undef');
92
+ expect(result[1].severity).toBe('warning');
93
+ });
94
+
95
+ test('analyze returns empty array on no results', async () => {
96
+ mockLintText.mockResolvedValue([]);
97
+
98
+ const result = await analyzer.analyze('test.js', 'const x = 1;');
99
+ expect(result).toEqual([]);
100
+ });
101
+
102
+ test('analyze returns empty array on null results', async () => {
103
+ mockLintText.mockResolvedValue(null);
104
+
105
+ const result = await analyzer.analyze('test.js', 'const x = 1;');
106
+ expect(result).toEqual([]);
107
+ });
108
+
109
+ test('analyze returns empty array on ESLint error', async () => {
110
+ mockLintText.mockRejectedValue(new Error('ESLint config error'));
111
+
112
+ const result = await analyzer.analyze('test.js', 'const x = 1;');
113
+ expect(result).toEqual([]);
114
+ expect(logger.error).toHaveBeenCalled();
115
+ });
116
+
117
+ // ── fix ──
118
+ test('fix returns fixed content when changes made', async () => {
119
+ mockLintText.mockResolvedValue([{
120
+ output: 'const x = 1;\n',
121
+ fixableErrorCount: 1,
122
+ fixableWarningCount: 0,
123
+ errorCount: 1,
124
+ warningCount: 0
125
+ }]);
126
+
127
+ const result = await analyzer.fix('test.js', 'const x = 1\n');
128
+ expect(result.fixed).toBe(true);
129
+ expect(result.content).toBe('const x = 1;\n');
130
+ expect(result.fixedCount).toBe(1);
131
+ });
132
+
133
+ test('fix returns original content when no changes', async () => {
134
+ mockLintText.mockResolvedValue([{
135
+ output: undefined,
136
+ fixableErrorCount: 0,
137
+ fixableWarningCount: 0,
138
+ errorCount: 0,
139
+ warningCount: 0
140
+ }]);
141
+
142
+ const result = await analyzer.fix('test.js', 'const x = 1;\n');
143
+ expect(result.fixed).toBe(false);
144
+ expect(result.content).toBe('const x = 1;\n');
145
+ });
146
+
147
+ test('fix returns default when no results', async () => {
148
+ mockLintText.mockResolvedValue([]);
149
+
150
+ const result = await analyzer.fix('test.js', 'const x = 1;');
151
+ expect(result.fixed).toBe(false);
152
+ expect(result.fixedCount).toBe(0);
153
+ });
154
+
155
+ test('fix throws on ESLint error', async () => {
156
+ mockLintText.mockRejectedValue(new Error('ESLint error'));
157
+
158
+ await expect(analyzer.fix('test.js', 'x')).rejects.toThrow('ESLint fix failed');
159
+ });
160
+
161
+ // ── formatMessage ──
162
+ test('formatMessage maps severity 2 to error', () => {
163
+ const msg = { line: 10, column: 5, severity: 2, ruleId: 'no-eval', message: 'eval is bad' };
164
+ const result = analyzer.formatMessage(msg, 'test.js');
165
+ expect(result.severity).toBe('error');
166
+ expect(result.file).toBe('test.js');
167
+ expect(result.line).toBe(10);
168
+ expect(result.source).toBe('eslint');
169
+ });
170
+
171
+ test('formatMessage maps severity 1 to warning', () => {
172
+ const msg = { severity: 1, ruleId: 'semi', message: 'Missing semicolon' };
173
+ const result = analyzer.formatMessage(msg, 'test.js');
174
+ expect(result.severity).toBe('warning');
175
+ });
176
+
177
+ test('formatMessage handles fixable message', () => {
178
+ const msg = { severity: 1, ruleId: 'semi', message: 'Missing', fix: { range: [0, 1], text: ';' } };
179
+ const result = analyzer.formatMessage(msg, 'test.js');
180
+ expect(result.fixable).toBe(true);
181
+ });
182
+
183
+ test('formatMessage handles missing ruleId', () => {
184
+ const msg = { severity: 2, message: 'Error' };
185
+ const result = analyzer.formatMessage(msg, 'test.js');
186
+ expect(result.rule).toBe('eslint');
187
+ });
188
+
189
+ // ── categorizeRule ──
190
+ test('categorizeRule returns STYLE for null ruleId', () => {
191
+ expect(analyzer.categorizeRule(null)).toBe('style');
192
+ });
193
+
194
+ test('categorizeRule detects security rules', () => {
195
+ expect(analyzer.categorizeRule('no-eval')).toBe('security');
196
+ expect(analyzer.categorizeRule('no-implied-eval')).toBe('security');
197
+ expect(analyzer.categorizeRule('security/detect-xss')).toBe('security');
198
+ });
199
+
200
+ test('categorizeRule detects performance rules', () => {
201
+ expect(analyzer.categorizeRule('no-await-in-loop')).toBe('performance');
202
+ expect(analyzer.categorizeRule('prefer-promise-reject-errors')).toBe('performance');
203
+ });
204
+
205
+ test('categorizeRule detects import rules', () => {
206
+ expect(analyzer.categorizeRule('import/no-unresolved')).toBe('import');
207
+ expect(analyzer.categorizeRule('no-undef')).toBe('import');
208
+ });
209
+
210
+ test('categorizeRule detects best practice rules', () => {
211
+ expect(analyzer.categorizeRule('no-unused-vars')).toBe('best_practice');
212
+ expect(analyzer.categorizeRule('no-unreachable')).toBe('best_practice');
213
+ expect(analyzer.categorizeRule('no-var')).toBe('best_practice');
214
+ });
215
+
216
+ test('categorizeRule defaults to style', () => {
217
+ expect(analyzer.categorizeRule('some-other-rule')).toBe('style');
218
+ });
219
+
220
+ // ── describeChanges ──
221
+ test('describeChanges detects modified lines', () => {
222
+ const original = 'line1\nline2\nline3';
223
+ const fixed = 'line1\nLINE2\nline3';
224
+ const changes = analyzer.describeChanges(original, fixed);
225
+ expect(changes.length).toBe(1);
226
+ expect(changes[0].type).toBe('modified');
227
+ expect(changes[0].line).toBe(2);
228
+ });
229
+
230
+ test('describeChanges detects added lines', () => {
231
+ const original = 'line1';
232
+ const fixed = 'line1\nline2';
233
+ const changes = analyzer.describeChanges(original, fixed);
234
+ expect(changes.length).toBe(1);
235
+ expect(changes[0].type).toBe('added');
236
+ });
237
+
238
+ test('describeChanges detects removed lines', () => {
239
+ const original = 'line1\nline2';
240
+ const fixed = 'line1';
241
+ const changes = analyzer.describeChanges(original, fixed);
242
+ expect(changes.length).toBe(1);
243
+ expect(changes[0].type).toBe('removed');
244
+ });
245
+
246
+ // ── getESLintConfig ──
247
+ test('getESLintConfig returns base config without workingDir', async () => {
248
+ const config = await analyzer.getESLintConfig(null, null);
249
+ expect(config.env).toBeDefined();
250
+ expect(config.rules).toBeDefined();
251
+ });
252
+
253
+ test('getESLintConfig adds react settings for react framework', async () => {
254
+ const config = await analyzer.getESLintConfig(null, 'react');
255
+ expect(config.parserOptions.ecmaFeatures.jsx).toBe(true);
256
+ });
257
+
258
+ test('getESLintConfig returns empty when project config found', async () => {
259
+ mockAccess.mockResolvedValueOnce(undefined);
260
+
261
+ const config = await analyzer.getESLintConfig('/project', null);
262
+ expect(Object.keys(config).length).toBe(0);
263
+ });
264
+
265
+ test('getESLintConfig returns full config when no project config', async () => {
266
+ mockAccess.mockRejectedValue(new Error('ENOENT'));
267
+
268
+ const config = await analyzer.getESLintConfig('/project', null);
269
+ expect(config.rules).toBeDefined();
270
+ });
271
+ });
@@ -0,0 +1,40 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+ import JavaScriptAnalyzer from '../JavaScriptAnalyzer.js';
4
+
5
+ describe('JavaScriptAnalyzer', () => {
6
+ let logger;
7
+
8
+ beforeEach(() => {
9
+ logger = createMockLogger();
10
+ });
11
+
12
+ test('constructor creates instance', () => {
13
+ const analyzer = new JavaScriptAnalyzer(logger);
14
+ expect(analyzer).toBeInstanceOf(JavaScriptAnalyzer);
15
+ expect(analyzer.logger).toBe(logger);
16
+ });
17
+
18
+ test('analyze with valid JS returns array', async () => {
19
+ const analyzer = new JavaScriptAnalyzer(logger);
20
+ const result = await analyzer.analyze('test.js', 'const x = 1;\nconsole.log(x);');
21
+ expect(Array.isArray(result)).toBe(true);
22
+ });
23
+
24
+ test('analyze with syntax error JS returns diagnostics', async () => {
25
+ const analyzer = new JavaScriptAnalyzer(logger);
26
+ const result = await analyzer.analyze('broken.js', 'function( { {{');
27
+ expect(Array.isArray(result)).toBe(true);
28
+ expect(result.length).toBeGreaterThan(0);
29
+ // Each diagnostic should have standard fields
30
+ const first = result[0];
31
+ expect(first).toHaveProperty('severity');
32
+ expect(first).toHaveProperty('message');
33
+ });
34
+
35
+ test('analyze returns array even for empty content', async () => {
36
+ const analyzer = new JavaScriptAnalyzer(logger);
37
+ const result = await analyzer.analyze('empty.js', '');
38
+ expect(Array.isArray(result)).toBe(true);
39
+ });
40
+ });
@@ -0,0 +1,197 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock prettier
5
+ const mockFormat = jest.fn();
6
+ const mockResolveConfig = jest.fn();
7
+ jest.unstable_mockModule('prettier', () => ({
8
+ default: {
9
+ format: mockFormat,
10
+ resolveConfig: mockResolveConfig
11
+ },
12
+ format: mockFormat,
13
+ resolveConfig: mockResolveConfig
14
+ }));
15
+
16
+ const { default: PrettierFormatter } = await import('../PrettierFormatter.js');
17
+
18
+ describe('PrettierFormatter', () => {
19
+ let formatter;
20
+ let logger;
21
+
22
+ beforeEach(() => {
23
+ logger = createMockLogger();
24
+ formatter = new PrettierFormatter(logger);
25
+ formatter.configCache.clear();
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ // ── Constructor ──
30
+ test('constructor initializes with logger and cache', () => {
31
+ expect(formatter.logger).toBe(logger);
32
+ expect(formatter.configCache).toBeInstanceOf(Map);
33
+ });
34
+
35
+ test('constructor works without logger', () => {
36
+ const f = new PrettierFormatter();
37
+ expect(f.logger).toBeNull();
38
+ });
39
+
40
+ // ── getSupportedExtensions ──
41
+ test('getSupportedExtensions returns all supported file types', () => {
42
+ const exts = formatter.getSupportedExtensions();
43
+ expect(exts).toContain('.js');
44
+ expect(exts).toContain('.ts');
45
+ expect(exts).toContain('.css');
46
+ expect(exts).toContain('.json');
47
+ expect(exts).toContain('.html');
48
+ expect(exts).toContain('.md');
49
+ expect(exts).toContain('.yaml');
50
+ });
51
+
52
+ // ── isSupported ──
53
+ test('isSupported returns true for supported extensions', () => {
54
+ expect(formatter.isSupported('test.js')).toBe(true);
55
+ expect(formatter.isSupported('test.ts')).toBe(true);
56
+ expect(formatter.isSupported('test.css')).toBe(true);
57
+ });
58
+
59
+ test('isSupported returns false for unsupported extensions', () => {
60
+ expect(formatter.isSupported('test.py')).toBe(false);
61
+ expect(formatter.isSupported('test.go')).toBe(false);
62
+ });
63
+
64
+ // ── getParser ──
65
+ test('getParser returns correct parser for known extensions', () => {
66
+ expect(formatter.getParser('test.js')).toBe('babel');
67
+ expect(formatter.getParser('test.jsx')).toBe('babel');
68
+ expect(formatter.getParser('test.ts')).toBe('typescript');
69
+ expect(formatter.getParser('test.tsx')).toBe('typescript');
70
+ expect(formatter.getParser('test.json')).toBe('json');
71
+ expect(formatter.getParser('test.css')).toBe('css');
72
+ expect(formatter.getParser('test.scss')).toBe('scss');
73
+ expect(formatter.getParser('test.less')).toBe('less');
74
+ expect(formatter.getParser('test.html')).toBe('html');
75
+ expect(formatter.getParser('test.vue')).toBe('vue');
76
+ expect(formatter.getParser('test.md')).toBe('markdown');
77
+ expect(formatter.getParser('test.yaml')).toBe('yaml');
78
+ expect(formatter.getParser('test.yml')).toBe('yaml');
79
+ });
80
+
81
+ test('getParser returns babel for unknown extension', () => {
82
+ expect(formatter.getParser('test.xyz')).toBe('babel');
83
+ });
84
+
85
+ // ── format ──
86
+ test('format returns formatted content when changes made', async () => {
87
+ mockFormat.mockResolvedValue('const x = 1;\n');
88
+
89
+ const result = await formatter.format('test.js', 'const x=1\n');
90
+ expect(result.formatted).toBe(true);
91
+ expect(result.content).toBe('const x = 1;\n');
92
+ expect(result.original).toBe('const x=1\n');
93
+ expect(result.changes.length).toBeGreaterThan(0);
94
+ expect(result.linesChanged).toBeGreaterThan(0);
95
+ });
96
+
97
+ test('format returns unchanged content when no changes needed', async () => {
98
+ const code = 'const x = 1;\n';
99
+ mockFormat.mockResolvedValue(code);
100
+
101
+ const result = await formatter.format('test.js', code);
102
+ expect(result.formatted).toBe(false);
103
+ expect(result.content).toBe(code);
104
+ expect(result.changes).toEqual([]);
105
+ expect(result.linesChanged).toBe(0);
106
+ });
107
+
108
+ test('format throws on prettier error', async () => {
109
+ mockFormat.mockRejectedValue(new Error('Parse error'));
110
+
111
+ await expect(formatter.format('test.js', 'invalid{{')).rejects.toThrow('Prettier formatting failed');
112
+ });
113
+
114
+ // ── check ──
115
+ test('check returns true when formatting needed', async () => {
116
+ mockFormat.mockResolvedValue('formatted\n');
117
+
118
+ const result = await formatter.check('test.js', 'original\n');
119
+ expect(result).toBe(true);
120
+ });
121
+
122
+ test('check returns false when no formatting needed', async () => {
123
+ const code = 'const x = 1;\n';
124
+ mockFormat.mockResolvedValue(code);
125
+
126
+ const result = await formatter.check('test.js', code);
127
+ expect(result).toBe(false);
128
+ });
129
+
130
+ test('check returns false on error', async () => {
131
+ mockFormat.mockRejectedValue(new Error('Parse error'));
132
+
133
+ const result = await formatter.check('test.js', 'invalid');
134
+ expect(result).toBe(false);
135
+ });
136
+
137
+ // ── getPrettierConfig ──
138
+ test('getPrettierConfig returns cached config on second call', async () => {
139
+ mockResolveConfig.mockResolvedValue(null);
140
+
141
+ const config1 = await formatter.getPrettierConfig('test.js', '/project');
142
+ const config2 = await formatter.getPrettierConfig('test.js', '/project');
143
+ expect(config1).toBe(config2);
144
+ });
145
+
146
+ test('getPrettierConfig uses project config when available', async () => {
147
+ mockResolveConfig.mockResolvedValue({ semi: false, singleQuote: false });
148
+
149
+ const config = await formatter.getPrettierConfig('test.js', '/project');
150
+ expect(config.semi).toBe(false); // project config overrides default
151
+ });
152
+
153
+ test('getPrettierConfig uses defaults when no project config', async () => {
154
+ const config = await formatter.getPrettierConfig('test.js', null);
155
+ expect(config.semi).toBe(true);
156
+ expect(config.singleQuote).toBe(true);
157
+ expect(config.tabWidth).toBe(2);
158
+ });
159
+
160
+ test('getPrettierConfig handles resolveConfig error', async () => {
161
+ mockResolveConfig.mockRejectedValue(new Error('config error'));
162
+
163
+ const config = await formatter.getPrettierConfig('test.js', '/project');
164
+ expect(config.semi).toBe(true); // Falls back to defaults
165
+ });
166
+
167
+ // ── describeChanges ──
168
+ test('describeChanges identifies modified lines', () => {
169
+ const original = 'line1\nline2\nline3';
170
+ const formatted = 'line1\nLINE2\nline3';
171
+ const changes = formatter.describeChanges(original, formatted);
172
+ expect(changes.length).toBe(1);
173
+ expect(changes[0].type).toBe('modified');
174
+ expect(changes[0].line).toBe(2);
175
+ });
176
+
177
+ test('describeChanges handles added lines', () => {
178
+ const original = 'line1';
179
+ const formatted = 'line1\nline2';
180
+ const changes = formatter.describeChanges(original, formatted);
181
+ expect(changes.some(c => c.type === 'added')).toBe(true);
182
+ });
183
+
184
+ test('describeChanges handles removed lines', () => {
185
+ const original = 'line1\nline2';
186
+ const formatted = 'line1';
187
+ const changes = formatter.describeChanges(original, formatted);
188
+ expect(changes.some(c => c.type === 'removed')).toBe(true);
189
+ });
190
+
191
+ // ── countChangedLines ──
192
+ test('countChangedLines returns correct count', () => {
193
+ expect(formatter.countChangedLines('a\nb\nc', 'a\nB\nc')).toBe(1);
194
+ expect(formatter.countChangedLines('a\nb', 'a\nb\nc')).toBe(1);
195
+ expect(formatter.countChangedLines('a', 'a')).toBe(0);
196
+ });
197
+ });