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.
- package/README.md +9 -4
- 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/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 +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -0
- 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
- 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
|
+
});
|