tlc-claude-code 1.2.26 → 1.2.28
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/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PlanView.d.ts +7 -0
- package/dashboard/dist/components/PlanView.js +74 -2
- package/dashboard/dist/components/PlanView.test.js +70 -1
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +157 -2
- package/server/index.js +38 -21
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- package/server/lib/session-summary.test.js +156 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { ReviewOrchestrator } from './review-orchestrator.js';
|
|
6
|
+
|
|
7
|
+
// Create mock adapter
|
|
8
|
+
const createMockAdapter = (name, issues = [], options = {}) => ({
|
|
9
|
+
name,
|
|
10
|
+
canAfford: vi.fn(() => options.canAfford !== false),
|
|
11
|
+
getUsage: vi.fn(() => options.usage || { daily: 0, monthly: 0, requests: 0 }),
|
|
12
|
+
review: vi.fn(() => Promise.resolve({
|
|
13
|
+
issues,
|
|
14
|
+
suggestions: options.suggestions || [],
|
|
15
|
+
score: options.score || 80,
|
|
16
|
+
model: name,
|
|
17
|
+
tokensUsed: 100,
|
|
18
|
+
cost: options.cost || 0.01,
|
|
19
|
+
})),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('ReviewOrchestrator', () => {
|
|
23
|
+
let testDir;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-review-test-'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('constructor', () => {
|
|
34
|
+
it('initializes with adapters', () => {
|
|
35
|
+
const adapters = [createMockAdapter('claude')];
|
|
36
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
37
|
+
expect(orchestrator.adapters).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('accepts options', () => {
|
|
41
|
+
const adapters = [createMockAdapter('claude')];
|
|
42
|
+
const orchestrator = new ReviewOrchestrator(adapters, { consensusType: 'unanimous' });
|
|
43
|
+
expect(orchestrator.options.consensusType).toBe('unanimous');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('reviewFile', () => {
|
|
48
|
+
it('reviews a single file', async () => {
|
|
49
|
+
const filePath = path.join(testDir, 'test.js');
|
|
50
|
+
fs.writeFileSync(filePath, 'const x = 1;');
|
|
51
|
+
|
|
52
|
+
const adapters = [createMockAdapter('claude', [{ id: 'A', severity: 'high', message: 'Issue' }])];
|
|
53
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
54
|
+
|
|
55
|
+
const result = await orchestrator.reviewFile(filePath);
|
|
56
|
+
|
|
57
|
+
expect(result.file).toBe(filePath);
|
|
58
|
+
expect(result.issues).toHaveLength(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns error for non-existent file', async () => {
|
|
62
|
+
const adapters = [createMockAdapter('claude')];
|
|
63
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
64
|
+
|
|
65
|
+
const result = await orchestrator.reviewFile('/nonexistent/file.js');
|
|
66
|
+
|
|
67
|
+
expect(result.error).toBeTruthy();
|
|
68
|
+
expect(result.issues).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('skips files over size limit', async () => {
|
|
72
|
+
const filePath = path.join(testDir, 'large.js');
|
|
73
|
+
fs.writeFileSync(filePath, 'x'.repeat(200 * 1024)); // 200KB
|
|
74
|
+
|
|
75
|
+
const adapters = [createMockAdapter('claude')];
|
|
76
|
+
const orchestrator = new ReviewOrchestrator(adapters, { maxFileSizeKB: 100 });
|
|
77
|
+
|
|
78
|
+
const result = await orchestrator.reviewFile(filePath);
|
|
79
|
+
|
|
80
|
+
expect(result.warning).toContain('too large');
|
|
81
|
+
expect(result.issues).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('tracks costs', async () => {
|
|
85
|
+
const filePath = path.join(testDir, 'test.js');
|
|
86
|
+
fs.writeFileSync(filePath, 'const x = 1;');
|
|
87
|
+
|
|
88
|
+
const adapters = [
|
|
89
|
+
createMockAdapter('claude', [], { cost: 0.01 }),
|
|
90
|
+
createMockAdapter('openai', [], { cost: 0.02 }),
|
|
91
|
+
];
|
|
92
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
93
|
+
|
|
94
|
+
const result = await orchestrator.reviewFile(filePath);
|
|
95
|
+
|
|
96
|
+
expect(result.costs.total).toBeCloseTo(0.03, 2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('includes models used', async () => {
|
|
100
|
+
const filePath = path.join(testDir, 'test.js');
|
|
101
|
+
fs.writeFileSync(filePath, 'const x = 1;');
|
|
102
|
+
|
|
103
|
+
const adapters = [
|
|
104
|
+
createMockAdapter('claude'),
|
|
105
|
+
createMockAdapter('openai'),
|
|
106
|
+
];
|
|
107
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
108
|
+
|
|
109
|
+
const result = await orchestrator.reviewFile(filePath);
|
|
110
|
+
|
|
111
|
+
expect(result.models).toContain('claude');
|
|
112
|
+
expect(result.models).toContain('openai');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('reviewFiles', () => {
|
|
117
|
+
it('reviews multiple files', async () => {
|
|
118
|
+
fs.writeFileSync(path.join(testDir, 'a.js'), 'code a');
|
|
119
|
+
fs.writeFileSync(path.join(testDir, 'b.js'), 'code b');
|
|
120
|
+
|
|
121
|
+
const adapters = [createMockAdapter('claude', [{ id: 'A', message: 'Issue' }])];
|
|
122
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
123
|
+
|
|
124
|
+
const result = await orchestrator.reviewFiles([
|
|
125
|
+
path.join(testDir, 'a.js'),
|
|
126
|
+
path.join(testDir, 'b.js'),
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
expect(result.files).toHaveLength(2);
|
|
130
|
+
expect(result.fileResults).toHaveLength(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('aggregates costs across files', async () => {
|
|
134
|
+
fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
|
|
135
|
+
fs.writeFileSync(path.join(testDir, 'b.js'), 'code');
|
|
136
|
+
|
|
137
|
+
const adapters = [createMockAdapter('claude', [], { cost: 0.01 })];
|
|
138
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
139
|
+
|
|
140
|
+
const result = await orchestrator.reviewFiles([
|
|
141
|
+
path.join(testDir, 'a.js'),
|
|
142
|
+
path.join(testDir, 'b.js'),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
expect(result.totalCost).toBeCloseTo(0.02, 2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('counts total issues', async () => {
|
|
149
|
+
fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
|
|
150
|
+
fs.writeFileSync(path.join(testDir, 'b.js'), 'code');
|
|
151
|
+
|
|
152
|
+
const adapters = [createMockAdapter('claude', [
|
|
153
|
+
{ id: 'A', message: 'Issue A' },
|
|
154
|
+
{ id: 'B', message: 'Issue B' },
|
|
155
|
+
])];
|
|
156
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
157
|
+
|
|
158
|
+
const result = await orchestrator.reviewFiles([
|
|
159
|
+
path.join(testDir, 'a.js'),
|
|
160
|
+
path.join(testDir, 'b.js'),
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
expect(result.totalIssues).toBe(4); // 2 issues per file
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('reviewDirectory', () => {
|
|
168
|
+
it('reviews all files in directory', async () => {
|
|
169
|
+
fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
|
|
170
|
+
fs.writeFileSync(path.join(testDir, 'b.js'), 'code');
|
|
171
|
+
|
|
172
|
+
const adapters = [createMockAdapter('claude')];
|
|
173
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
174
|
+
|
|
175
|
+
const result = await orchestrator.reviewDirectory(testDir);
|
|
176
|
+
|
|
177
|
+
expect(result.files).toHaveLength(2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('returns error for invalid directory', async () => {
|
|
181
|
+
const adapters = [createMockAdapter('claude')];
|
|
182
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
183
|
+
|
|
184
|
+
const result = await orchestrator.reviewDirectory('/nonexistent');
|
|
185
|
+
|
|
186
|
+
expect(result.error).toBeTruthy();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns warning for empty directory', async () => {
|
|
190
|
+
const adapters = [createMockAdapter('claude')];
|
|
191
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
192
|
+
|
|
193
|
+
const result = await orchestrator.reviewDirectory(testDir);
|
|
194
|
+
|
|
195
|
+
expect(result.warning).toContain('No files');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('respects extension filter', async () => {
|
|
199
|
+
fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
|
|
200
|
+
fs.writeFileSync(path.join(testDir, 'b.ts'), 'code');
|
|
201
|
+
fs.writeFileSync(path.join(testDir, 'c.css'), 'code');
|
|
202
|
+
|
|
203
|
+
const adapters = [createMockAdapter('claude')];
|
|
204
|
+
const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
|
|
205
|
+
|
|
206
|
+
const result = await orchestrator.reviewDirectory(testDir, { extensions: ['.js'] });
|
|
207
|
+
|
|
208
|
+
expect(result.files).toHaveLength(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('summarizeResults', () => {
|
|
213
|
+
it('calculates average confidence', () => {
|
|
214
|
+
const adapters = [createMockAdapter('claude')];
|
|
215
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
216
|
+
|
|
217
|
+
// Provide pre-formed results with known confidence values
|
|
218
|
+
const fileResults = [
|
|
219
|
+
{
|
|
220
|
+
file: 'a.js',
|
|
221
|
+
issues: [
|
|
222
|
+
{ id: 'A', message: 'Issue', confidence: 0.8 },
|
|
223
|
+
{ id: 'B', message: 'Issue 2', confidence: 0.6 },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const summary = orchestrator.summarizeResults(fileResults, ['claude'], { byModel: {}, total: 0 });
|
|
229
|
+
|
|
230
|
+
expect(summary.averageConfidence).toBeCloseTo(0.7, 1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('handles files with no issues', () => {
|
|
234
|
+
const adapters = [createMockAdapter('claude')];
|
|
235
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
236
|
+
|
|
237
|
+
const fileResults = [{ file: 'a.js', issues: [] }];
|
|
238
|
+
const summary = orchestrator.summarizeResults(fileResults, ['claude'], { byModel: {}, total: 0 });
|
|
239
|
+
|
|
240
|
+
expect(summary.averageConfidence).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('getAvailableModels', () => {
|
|
245
|
+
it('returns all models when budgetAware is false', () => {
|
|
246
|
+
const adapters = [
|
|
247
|
+
createMockAdapter('claude', [], { canAfford: false }),
|
|
248
|
+
createMockAdapter('openai', [], { canAfford: true }),
|
|
249
|
+
];
|
|
250
|
+
const orchestrator = new ReviewOrchestrator(adapters, { budgetAware: false });
|
|
251
|
+
|
|
252
|
+
const models = orchestrator.getAvailableModels();
|
|
253
|
+
expect(models).toEqual(['claude', 'openai']);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('returns only affordable models when budgetAware is true', () => {
|
|
257
|
+
const adapters = [
|
|
258
|
+
createMockAdapter('claude', [], { canAfford: false }),
|
|
259
|
+
createMockAdapter('openai', [], { canAfford: true }),
|
|
260
|
+
];
|
|
261
|
+
const orchestrator = new ReviewOrchestrator(adapters, { budgetAware: true });
|
|
262
|
+
|
|
263
|
+
const models = orchestrator.getAvailableModels();
|
|
264
|
+
expect(models).toEqual(['openai']);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('getUsageSummary', () => {
|
|
269
|
+
it('returns usage for all adapters', () => {
|
|
270
|
+
const adapters = [
|
|
271
|
+
createMockAdapter('claude', [], { usage: { daily: 1, monthly: 10, requests: 5 } }),
|
|
272
|
+
createMockAdapter('openai', [], { usage: { daily: 2, monthly: 20, requests: 10 } }),
|
|
273
|
+
];
|
|
274
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
275
|
+
|
|
276
|
+
const usage = orchestrator.getUsageSummary();
|
|
277
|
+
|
|
278
|
+
expect(usage.claude.daily).toBe(1);
|
|
279
|
+
expect(usage.openai.daily).toBe(2);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('aggregateSuggestions', () => {
|
|
284
|
+
it('combines unique suggestions', () => {
|
|
285
|
+
const adapters = [createMockAdapter('claude')];
|
|
286
|
+
const orchestrator = new ReviewOrchestrator(adapters);
|
|
287
|
+
|
|
288
|
+
const reviews = [
|
|
289
|
+
{ suggestions: ['A', 'B'] },
|
|
290
|
+
{ suggestions: ['B', 'C'] },
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
const suggestions = orchestrator.aggregateSuggestions(reviews);
|
|
294
|
+
expect(suggestions).toHaveLength(3);
|
|
295
|
+
expect(suggestions).toContain('A');
|
|
296
|
+
expect(suggestions).toContain('B');
|
|
297
|
+
expect(suggestions).toContain('C');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Reporter - Generate review reports in multiple formats
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate markdown report from review results
|
|
7
|
+
* @param {Object} results - Review results from orchestrator
|
|
8
|
+
* @returns {string} Markdown report
|
|
9
|
+
*/
|
|
10
|
+
function generateMarkdown(results) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
|
|
13
|
+
// Header
|
|
14
|
+
lines.push('# Code Review Report');
|
|
15
|
+
lines.push('');
|
|
16
|
+
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
17
|
+
lines.push(`**Files Reviewed:** ${results.files?.length || 0}`);
|
|
18
|
+
lines.push(`**Models Used:** ${results.models?.join(', ') || 'none'}`);
|
|
19
|
+
lines.push('');
|
|
20
|
+
|
|
21
|
+
// Summary
|
|
22
|
+
lines.push('## Summary');
|
|
23
|
+
lines.push('');
|
|
24
|
+
lines.push(`| Metric | Value |`);
|
|
25
|
+
lines.push(`|--------|-------|`);
|
|
26
|
+
lines.push(`| Total Issues | ${results.totalIssues || 0} |`);
|
|
27
|
+
lines.push(`| High Severity | ${countBySeverity(results, 'high')} |`);
|
|
28
|
+
lines.push(`| Medium Severity | ${countBySeverity(results, 'medium')} |`);
|
|
29
|
+
lines.push(`| Low Severity | ${countBySeverity(results, 'low')} |`);
|
|
30
|
+
lines.push(`| Average Confidence | ${formatPercent(results.averageConfidence)} |`);
|
|
31
|
+
lines.push(`| Total Cost | $${formatCost(results.totalCost)} |`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
|
|
34
|
+
// Model Agreement
|
|
35
|
+
if (results.modelAgreement) {
|
|
36
|
+
lines.push('## Model Agreement');
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push(`| Issue | Models Agreed | Confidence |`);
|
|
39
|
+
lines.push(`|-------|---------------|------------|`);
|
|
40
|
+
for (const issue of results.consensusIssues || []) {
|
|
41
|
+
const voters = issue.voters?.join(', ') || '';
|
|
42
|
+
lines.push(`| ${escapeMarkdown(issue.message || issue.id)} | ${voters} | ${formatPercent(issue.confidence)} |`);
|
|
43
|
+
}
|
|
44
|
+
lines.push('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Issues by File
|
|
48
|
+
if (results.fileResults && results.fileResults.length > 0) {
|
|
49
|
+
lines.push('## Issues by File');
|
|
50
|
+
lines.push('');
|
|
51
|
+
|
|
52
|
+
for (const fileResult of results.fileResults) {
|
|
53
|
+
if (fileResult.issues && fileResult.issues.length > 0) {
|
|
54
|
+
lines.push(`### ${fileResult.file}`);
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push(`| Line | Severity | Message | Confidence |`);
|
|
57
|
+
lines.push(`|------|----------|---------|------------|`);
|
|
58
|
+
for (const issue of fileResult.issues) {
|
|
59
|
+
lines.push(`| ${issue.line || '-'} | ${issue.severity || 'unknown'} | ${escapeMarkdown(issue.message)} | ${formatPercent(issue.confidence)} |`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Cost Breakdown
|
|
67
|
+
if (results.costs) {
|
|
68
|
+
lines.push('## Cost Breakdown');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(`| Model | Cost |`);
|
|
71
|
+
lines.push(`|-------|------|`);
|
|
72
|
+
for (const [model, cost] of Object.entries(results.costs.byModel || {})) {
|
|
73
|
+
lines.push(`| ${model} | $${formatCost(cost)} |`);
|
|
74
|
+
}
|
|
75
|
+
lines.push(`| **Total** | **$${formatCost(results.costs.total)}** |`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate JSON report from review results
|
|
84
|
+
* @param {Object} results - Review results from orchestrator
|
|
85
|
+
* @returns {string} JSON report
|
|
86
|
+
*/
|
|
87
|
+
function generateJSON(results) {
|
|
88
|
+
const report = {
|
|
89
|
+
meta: {
|
|
90
|
+
generated: new Date().toISOString(),
|
|
91
|
+
filesReviewed: results.files?.length || 0,
|
|
92
|
+
modelsUsed: results.models || [],
|
|
93
|
+
},
|
|
94
|
+
summary: {
|
|
95
|
+
totalIssues: results.totalIssues || 0,
|
|
96
|
+
bySeverity: {
|
|
97
|
+
high: countBySeverity(results, 'high'),
|
|
98
|
+
medium: countBySeverity(results, 'medium'),
|
|
99
|
+
low: countBySeverity(results, 'low'),
|
|
100
|
+
},
|
|
101
|
+
averageConfidence: results.averageConfidence || 0,
|
|
102
|
+
totalCost: results.totalCost || 0,
|
|
103
|
+
},
|
|
104
|
+
consensusIssues: results.consensusIssues || [],
|
|
105
|
+
fileResults: results.fileResults || [],
|
|
106
|
+
costs: results.costs || { byModel: {}, total: 0 },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return JSON.stringify(report, null, 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate HTML report from review results
|
|
114
|
+
* @param {Object} results - Review results from orchestrator
|
|
115
|
+
* @returns {string} HTML report
|
|
116
|
+
*/
|
|
117
|
+
function generateHTML(results) {
|
|
118
|
+
const severityColors = {
|
|
119
|
+
high: '#dc3545',
|
|
120
|
+
medium: '#ffc107',
|
|
121
|
+
low: '#28a745',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return `<!DOCTYPE html>
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="UTF-8">
|
|
128
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
129
|
+
<title>Code Review Report</title>
|
|
130
|
+
<style>
|
|
131
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2rem; background: #f5f5f5; }
|
|
132
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
133
|
+
h1 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 0.5rem; }
|
|
134
|
+
h2 { color: #555; margin-top: 2rem; }
|
|
135
|
+
h3 { color: #666; }
|
|
136
|
+
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
|
137
|
+
th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #ddd; }
|
|
138
|
+
th { background: #f8f9fa; font-weight: 600; }
|
|
139
|
+
tr:hover { background: #f8f9fa; }
|
|
140
|
+
.severity-high { color: ${severityColors.high}; font-weight: bold; }
|
|
141
|
+
.severity-medium { color: ${severityColors.medium}; font-weight: bold; }
|
|
142
|
+
.severity-low { color: ${severityColors.low}; font-weight: bold; }
|
|
143
|
+
.confidence { color: #6c757d; }
|
|
144
|
+
.meta { color: #6c757d; font-size: 0.9rem; margin-bottom: 1rem; }
|
|
145
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin: 1rem 0; }
|
|
146
|
+
.summary-card { background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center; }
|
|
147
|
+
.summary-card .value { font-size: 2rem; font-weight: bold; color: #007bff; }
|
|
148
|
+
.summary-card .label { color: #6c757d; font-size: 0.9rem; }
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="container">
|
|
153
|
+
<h1>Code Review Report</h1>
|
|
154
|
+
<div class="meta">
|
|
155
|
+
<p><strong>Generated:</strong> ${new Date().toISOString()}</p>
|
|
156
|
+
<p><strong>Files Reviewed:</strong> ${results.files?.length || 0}</p>
|
|
157
|
+
<p><strong>Models:</strong> ${results.models?.join(', ') || 'none'}</p>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<h2>Summary</h2>
|
|
161
|
+
<div class="summary-grid">
|
|
162
|
+
<div class="summary-card">
|
|
163
|
+
<div class="value">${results.totalIssues || 0}</div>
|
|
164
|
+
<div class="label">Total Issues</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="summary-card">
|
|
167
|
+
<div class="value" style="color: ${severityColors.high}">${countBySeverity(results, 'high')}</div>
|
|
168
|
+
<div class="label">High Severity</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="summary-card">
|
|
171
|
+
<div class="value" style="color: ${severityColors.medium}">${countBySeverity(results, 'medium')}</div>
|
|
172
|
+
<div class="label">Medium Severity</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="summary-card">
|
|
175
|
+
<div class="value" style="color: ${severityColors.low}">${countBySeverity(results, 'low')}</div>
|
|
176
|
+
<div class="label">Low Severity</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="summary-card">
|
|
179
|
+
<div class="value">${formatPercent(results.averageConfidence)}</div>
|
|
180
|
+
<div class="label">Avg Confidence</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="summary-card">
|
|
183
|
+
<div class="value">$${formatCost(results.totalCost)}</div>
|
|
184
|
+
<div class="label">Total Cost</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
${results.fileResults && results.fileResults.length > 0 ? `
|
|
189
|
+
<h2>Issues by File</h2>
|
|
190
|
+
${results.fileResults.map(fileResult => fileResult.issues?.length > 0 ? `
|
|
191
|
+
<h3>${escapeHTML(fileResult.file)}</h3>
|
|
192
|
+
<table>
|
|
193
|
+
<thead>
|
|
194
|
+
<tr><th>Line</th><th>Severity</th><th>Message</th><th>Confidence</th></tr>
|
|
195
|
+
</thead>
|
|
196
|
+
<tbody>
|
|
197
|
+
${fileResult.issues.map(issue => `
|
|
198
|
+
<tr>
|
|
199
|
+
<td>${issue.line || '-'}</td>
|
|
200
|
+
<td class="severity-${issue.severity || 'low'}">${issue.severity || 'unknown'}</td>
|
|
201
|
+
<td>${escapeHTML(issue.message)}</td>
|
|
202
|
+
<td class="confidence">${formatPercent(issue.confidence)}</td>
|
|
203
|
+
</tr>
|
|
204
|
+
`).join('')}
|
|
205
|
+
</tbody>
|
|
206
|
+
</table>
|
|
207
|
+
` : '').join('')}
|
|
208
|
+
` : ''}
|
|
209
|
+
|
|
210
|
+
${results.costs ? `
|
|
211
|
+
<h2>Cost Breakdown</h2>
|
|
212
|
+
<table>
|
|
213
|
+
<thead>
|
|
214
|
+
<tr><th>Model</th><th>Cost</th></tr>
|
|
215
|
+
</thead>
|
|
216
|
+
<tbody>
|
|
217
|
+
${Object.entries(results.costs.byModel || {}).map(([model, cost]) => `
|
|
218
|
+
<tr><td>${model}</td><td>$${formatCost(cost)}</td></tr>
|
|
219
|
+
`).join('')}
|
|
220
|
+
<tr style="font-weight: bold;"><td>Total</td><td>$${formatCost(results.costs.total)}</td></tr>
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
` : ''}
|
|
224
|
+
</div>
|
|
225
|
+
</body>
|
|
226
|
+
</html>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Generate report in specified format
|
|
231
|
+
* @param {Object} results - Review results
|
|
232
|
+
* @param {string} format - Output format (md, json, html)
|
|
233
|
+
* @returns {string} Formatted report
|
|
234
|
+
*/
|
|
235
|
+
function generateReport(results, format = 'md') {
|
|
236
|
+
switch (format.toLowerCase()) {
|
|
237
|
+
case 'json':
|
|
238
|
+
return generateJSON(results);
|
|
239
|
+
case 'html':
|
|
240
|
+
return generateHTML(results);
|
|
241
|
+
case 'md':
|
|
242
|
+
case 'markdown':
|
|
243
|
+
default:
|
|
244
|
+
return generateMarkdown(results);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Helper functions
|
|
249
|
+
function countBySeverity(results, severity) {
|
|
250
|
+
let count = 0;
|
|
251
|
+
for (const fileResult of results.fileResults || []) {
|
|
252
|
+
for (const issue of fileResult.issues || []) {
|
|
253
|
+
if (issue.severity === severity) count++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return count;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function formatPercent(value) {
|
|
260
|
+
if (value === undefined || value === null) return '0%';
|
|
261
|
+
return `${Math.round(value * 100)}%`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatCost(value) {
|
|
265
|
+
if (value === undefined || value === null) return '0.00';
|
|
266
|
+
return value.toFixed(4);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function escapeMarkdown(text) {
|
|
270
|
+
if (!text) return '';
|
|
271
|
+
return text.replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function escapeHTML(text) {
|
|
275
|
+
if (!text) return '';
|
|
276
|
+
return text
|
|
277
|
+
.replace(/&/g, '&')
|
|
278
|
+
.replace(/</g, '<')
|
|
279
|
+
.replace(/>/g, '>')
|
|
280
|
+
.replace(/"/g, '"');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
generateReport,
|
|
285
|
+
generateMarkdown,
|
|
286
|
+
generateJSON,
|
|
287
|
+
generateHTML,
|
|
288
|
+
};
|