tlc-claude-code 1.2.27 → 1.2.29

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 (179) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  3. package/dashboard/dist/components/ActivityFeed.js +42 -0
  4. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  5. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  6. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  7. package/dashboard/dist/components/BranchSelector.js +49 -0
  8. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  9. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  10. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  11. package/dashboard/dist/components/CommandPalette.js +118 -0
  12. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  13. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  14. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  15. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  17. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  18. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  19. package/dashboard/dist/components/DeviceFrame.js +52 -0
  20. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  21. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  22. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  23. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  25. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  26. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  27. package/dashboard/dist/components/FocusIndicator.js +47 -0
  28. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  29. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  30. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  31. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  33. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  34. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  35. package/dashboard/dist/components/LogSearch.js +43 -0
  36. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  37. package/dashboard/dist/components/LogSearch.test.js +100 -0
  38. package/dashboard/dist/components/LogStream.d.ts +21 -0
  39. package/dashboard/dist/components/LogStream.js +123 -0
  40. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  41. package/dashboard/dist/components/LogStream.test.js +159 -0
  42. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  43. package/dashboard/dist/components/PreviewPanel.js +73 -0
  44. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  45. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  46. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  47. package/dashboard/dist/components/ProjectCard.js +19 -0
  48. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  49. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  50. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  51. package/dashboard/dist/components/ProjectDetail.js +65 -0
  52. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  53. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  54. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  55. package/dashboard/dist/components/ProjectList.js +62 -0
  56. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  57. package/dashboard/dist/components/ProjectList.test.js +93 -0
  58. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  59. package/dashboard/dist/components/SettingsPanel.js +154 -0
  60. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  61. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  62. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  63. package/dashboard/dist/components/StatusBar.js +47 -0
  64. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  65. package/dashboard/dist/components/StatusBar.test.js +123 -0
  66. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  67. package/dashboard/dist/components/TaskBoard.js +102 -0
  68. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  69. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  70. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  71. package/dashboard/dist/components/TaskCard.js +29 -0
  72. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  73. package/dashboard/dist/components/TaskCard.test.js +109 -0
  74. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  75. package/dashboard/dist/components/TaskDetail.js +41 -0
  76. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  77. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  78. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  79. package/dashboard/dist/components/TaskFilter.js +138 -0
  80. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  81. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  82. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  83. package/dashboard/dist/components/TeamPanel.js +24 -0
  84. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  85. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  86. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  87. package/dashboard/dist/components/TeamPresence.js +31 -0
  88. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  89. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  90. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  91. package/dashboard/dist/components/layout/Header.js +11 -0
  92. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  93. package/dashboard/dist/components/layout/Header.test.js +35 -0
  94. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  95. package/dashboard/dist/components/layout/Shell.js +5 -0
  96. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  97. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  98. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  99. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  100. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  101. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  102. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  103. package/dashboard/dist/components/ui/Badge.js +13 -0
  104. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  105. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  106. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  107. package/dashboard/dist/components/ui/Button.js +14 -0
  108. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  109. package/dashboard/dist/components/ui/Button.test.js +81 -0
  110. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  111. package/dashboard/dist/components/ui/Card.js +20 -0
  112. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  113. package/dashboard/dist/components/ui/Card.test.js +82 -0
  114. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  115. package/dashboard/dist/components/ui/Input.js +8 -0
  116. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  117. package/dashboard/dist/components/ui/Input.test.js +68 -0
  118. package/dashboard/dist/styles/tokens.d.ts +150 -0
  119. package/dashboard/dist/styles/tokens.js +184 -0
  120. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  121. package/dashboard/dist/styles/tokens.test.js +95 -0
  122. package/dashboard/dist/test/setup.d.ts +1 -0
  123. package/dashboard/dist/test/setup.js +1 -0
  124. package/dashboard/package.json +3 -0
  125. package/package.json +15 -4
  126. package/scripts/capture-screenshots.js +170 -0
  127. package/scripts/docs-update.js +253 -0
  128. package/scripts/generate-screenshots.js +321 -0
  129. package/scripts/project-docs.js +377 -0
  130. package/scripts/vps-setup.sh +477 -0
  131. package/server/lib/adapters/base-adapter.js +114 -0
  132. package/server/lib/adapters/base-adapter.test.js +90 -0
  133. package/server/lib/adapters/claude-adapter.js +141 -0
  134. package/server/lib/adapters/claude-adapter.test.js +180 -0
  135. package/server/lib/adapters/deepseek-adapter.js +153 -0
  136. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  137. package/server/lib/adapters/openai-adapter.js +190 -0
  138. package/server/lib/adapters/openai-adapter.test.js +231 -0
  139. package/server/lib/budget-tracker.js +169 -0
  140. package/server/lib/budget-tracker.test.js +165 -0
  141. package/server/lib/claude-injector.js +85 -0
  142. package/server/lib/claude-injector.test.js +161 -0
  143. package/server/lib/consensus-engine.js +135 -0
  144. package/server/lib/consensus-engine.test.js +152 -0
  145. package/server/lib/context-builder.js +112 -0
  146. package/server/lib/context-builder.test.js +120 -0
  147. package/server/lib/file-collector.js +322 -0
  148. package/server/lib/file-collector.test.js +307 -0
  149. package/server/lib/memory-classifier.js +175 -0
  150. package/server/lib/memory-classifier.test.js +169 -0
  151. package/server/lib/memory-committer.js +138 -0
  152. package/server/lib/memory-committer.test.js +136 -0
  153. package/server/lib/memory-hooks.js +127 -0
  154. package/server/lib/memory-hooks.test.js +136 -0
  155. package/server/lib/memory-init.js +104 -0
  156. package/server/lib/memory-init.test.js +119 -0
  157. package/server/lib/memory-observer.js +149 -0
  158. package/server/lib/memory-observer.test.js +158 -0
  159. package/server/lib/memory-reader.js +243 -0
  160. package/server/lib/memory-reader.test.js +216 -0
  161. package/server/lib/memory-storage.js +120 -0
  162. package/server/lib/memory-storage.test.js +136 -0
  163. package/server/lib/memory-writer.js +176 -0
  164. package/server/lib/memory-writer.test.js +231 -0
  165. package/server/lib/overdrive-command.js +30 -6
  166. package/server/lib/overdrive-command.test.js +8 -1
  167. package/server/lib/pattern-detector.js +216 -0
  168. package/server/lib/pattern-detector.test.js +241 -0
  169. package/server/lib/relevance-scorer.js +175 -0
  170. package/server/lib/relevance-scorer.test.js +107 -0
  171. package/server/lib/review-command.js +238 -0
  172. package/server/lib/review-command.test.js +245 -0
  173. package/server/lib/review-orchestrator.js +273 -0
  174. package/server/lib/review-orchestrator.test.js +300 -0
  175. package/server/lib/review-reporter.js +288 -0
  176. package/server/lib/review-reporter.test.js +240 -0
  177. package/server/lib/session-summary.js +90 -0
  178. package/server/lib/session-summary.test.js +156 -0
  179. package/templates/docs-sync.yml +91 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Review Orchestrator - Orchestrate multi-model reviews with consensus
3
+ */
4
+
5
+ const { ConsensusEngine } = require('./consensus-engine.js');
6
+ const { collectFiles, readFileContent } = require('./file-collector.js');
7
+
8
+ /**
9
+ * Orchestrate code review across multiple models
10
+ */
11
+ class ReviewOrchestrator {
12
+ constructor(adapters, options = {}) {
13
+ this.adapters = adapters;
14
+ this.options = {
15
+ consensusType: 'majority',
16
+ requireMinimum: 1,
17
+ budgetAware: true,
18
+ maxFileSizeKB: 100,
19
+ ...options,
20
+ };
21
+ this.consensusEngine = new ConsensusEngine(adapters, this.options);
22
+ }
23
+
24
+ /**
25
+ * Review a single file
26
+ * @param {string} filePath - File path
27
+ * @param {Object} context - Additional context
28
+ * @returns {Promise<Object>} Review result
29
+ */
30
+ async reviewFile(filePath, context = {}) {
31
+ const { content, error } = readFileContent(filePath);
32
+
33
+ if (error) {
34
+ return {
35
+ file: filePath,
36
+ error,
37
+ issues: [],
38
+ costs: { byModel: {}, total: 0 },
39
+ };
40
+ }
41
+
42
+ // Check file size
43
+ const sizeKB = Buffer.byteLength(content, 'utf-8') / 1024;
44
+ if (sizeKB > this.options.maxFileSizeKB) {
45
+ return {
46
+ file: filePath,
47
+ warning: `File too large: ${sizeKB.toFixed(1)}KB > ${this.options.maxFileSizeKB}KB limit`,
48
+ issues: [],
49
+ costs: { byModel: {}, total: 0 },
50
+ };
51
+ }
52
+
53
+ try {
54
+ const result = await this.consensusEngine.review(content, {
55
+ ...context,
56
+ file: filePath,
57
+ });
58
+
59
+ return {
60
+ file: filePath,
61
+ issues: result.consensus?.issues || [],
62
+ suggestions: this.aggregateSuggestions(result.reviews),
63
+ costs: result.costs,
64
+ models: result.reviews.map(r => r.model),
65
+ warnings: result.warnings,
66
+ consensusType: result.consensusType,
67
+ };
68
+ } catch (err) {
69
+ return {
70
+ file: filePath,
71
+ error: err.message,
72
+ issues: [],
73
+ costs: { byModel: {}, total: 0 },
74
+ };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Review multiple files
80
+ * @param {string[]} files - Array of file paths
81
+ * @param {Object} context - Additional context
82
+ * @returns {Promise<Object>} Aggregated review result
83
+ */
84
+ async reviewFiles(files, context = {}) {
85
+ const fileResults = [];
86
+ const allModels = new Set();
87
+ let totalCost = 0;
88
+ const costsByModel = {};
89
+
90
+ for (const file of files) {
91
+ const result = await this.reviewFile(file, context);
92
+ fileResults.push(result);
93
+
94
+ // Aggregate models
95
+ if (result.models) {
96
+ result.models.forEach(m => allModels.add(m));
97
+ }
98
+
99
+ // Aggregate costs
100
+ if (result.costs) {
101
+ totalCost += result.costs.total || 0;
102
+ for (const [model, cost] of Object.entries(result.costs.byModel || {})) {
103
+ costsByModel[model] = (costsByModel[model] || 0) + cost;
104
+ }
105
+ }
106
+ }
107
+
108
+ return this.summarizeResults(fileResults, Array.from(allModels), {
109
+ byModel: costsByModel,
110
+ total: totalCost,
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Review a directory
116
+ * @param {string} dir - Directory path
117
+ * @param {Object} options - Collection options
118
+ * @returns {Promise<Object>} Aggregated review result
119
+ */
120
+ async reviewDirectory(dir, options = {}) {
121
+ const { files, stats } = collectFiles(dir, options);
122
+
123
+ if (stats.error) {
124
+ return {
125
+ files: [],
126
+ models: [],
127
+ error: stats.error,
128
+ fileResults: [],
129
+ totalIssues: 0,
130
+ averageConfidence: 0,
131
+ totalCost: 0,
132
+ costs: { byModel: {}, total: 0 },
133
+ };
134
+ }
135
+
136
+ if (files.length === 0) {
137
+ return {
138
+ files: [],
139
+ models: [],
140
+ warning: 'No files found to review',
141
+ fileResults: [],
142
+ totalIssues: 0,
143
+ averageConfidence: 0,
144
+ totalCost: 0,
145
+ costs: { byModel: {}, total: 0 },
146
+ };
147
+ }
148
+
149
+ return this.reviewFiles(files, { directory: dir });
150
+ }
151
+
152
+ /**
153
+ * Summarize review results
154
+ * @param {Array} fileResults - Results per file
155
+ * @param {Array} models - Models used
156
+ * @param {Object} costs - Cost summary
157
+ * @returns {Object} Summary
158
+ */
159
+ summarizeResults(fileResults, models, costs) {
160
+ // Collect all issues across files
161
+ const allIssues = [];
162
+ for (const result of fileResults) {
163
+ for (const issue of result.issues || []) {
164
+ allIssues.push({ ...issue, file: result.file });
165
+ }
166
+ }
167
+
168
+ // Calculate average confidence
169
+ let totalConfidence = 0;
170
+ let confidenceCount = 0;
171
+ for (const result of fileResults) {
172
+ for (const issue of result.issues || []) {
173
+ if (issue.confidence !== undefined) {
174
+ totalConfidence += issue.confidence;
175
+ confidenceCount++;
176
+ }
177
+ }
178
+ }
179
+ const averageConfidence = confidenceCount > 0 ? totalConfidence / confidenceCount : 0;
180
+
181
+ // Get consensus issues (across all files)
182
+ const consensusIssues = this.getConsensusIssues(fileResults);
183
+
184
+ return {
185
+ files: fileResults.map(r => r.file),
186
+ models,
187
+ fileResults,
188
+ totalIssues: allIssues.length,
189
+ averageConfidence,
190
+ totalCost: costs.total,
191
+ costs,
192
+ consensusIssues,
193
+ modelAgreement: models.length > 1,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Get issues that appear across multiple files
199
+ * @param {Array} fileResults - Results per file
200
+ * @returns {Array} Consensus issues
201
+ */
202
+ getConsensusIssues(fileResults) {
203
+ const issueMap = new Map();
204
+
205
+ for (const result of fileResults) {
206
+ for (const issue of result.issues || []) {
207
+ // Key by message (normalized)
208
+ const key = (issue.message || '').toLowerCase().trim();
209
+ if (!key) continue;
210
+
211
+ if (!issueMap.has(key)) {
212
+ issueMap.set(key, {
213
+ id: issue.id || key,
214
+ message: issue.message,
215
+ severity: issue.severity,
216
+ voters: issue.voters || [],
217
+ confidence: issue.confidence || 0,
218
+ files: [],
219
+ });
220
+ }
221
+ const entry = issueMap.get(key);
222
+ entry.files.push(result.file);
223
+ }
224
+ }
225
+
226
+ return Array.from(issueMap.values())
227
+ .sort((a, b) => b.confidence - a.confidence);
228
+ }
229
+
230
+ /**
231
+ * Aggregate suggestions from all reviews
232
+ * @param {Array} reviews - Review results
233
+ * @returns {Array} Unique suggestions
234
+ */
235
+ aggregateSuggestions(reviews) {
236
+ const suggestions = new Set();
237
+ for (const review of reviews || []) {
238
+ for (const suggestion of review.suggestions || []) {
239
+ suggestions.add(suggestion);
240
+ }
241
+ }
242
+ return Array.from(suggestions);
243
+ }
244
+
245
+ /**
246
+ * Get available models (within budget)
247
+ * @returns {Array} Available adapter names
248
+ */
249
+ getAvailableModels() {
250
+ if (!this.options.budgetAware) {
251
+ return this.adapters.map(a => a.name);
252
+ }
253
+ return this.adapters
254
+ .filter(a => a.canAfford())
255
+ .map(a => a.name);
256
+ }
257
+
258
+ /**
259
+ * Get usage summary across all adapters
260
+ * @returns {Object} Usage by model
261
+ */
262
+ getUsageSummary() {
263
+ const usage = {};
264
+ for (const adapter of this.adapters) {
265
+ usage[adapter.name] = adapter.getUsage();
266
+ }
267
+ return usage;
268
+ }
269
+ }
270
+
271
+ module.exports = {
272
+ ReviewOrchestrator,
273
+ };
@@ -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
+ });