tlc-claude-code 1.4.4 → 1.4.6
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/App.js +28 -2
- package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
- package/dashboard/dist/api/health-diagnostics.js +85 -0
- package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
- package/dashboard/dist/api/health-diagnostics.test.js +126 -0
- package/dashboard/dist/api/index.d.ts +5 -0
- package/dashboard/dist/api/index.js +5 -0
- package/dashboard/dist/api/notes-api.d.ts +18 -0
- package/dashboard/dist/api/notes-api.js +68 -0
- package/dashboard/dist/api/notes-api.test.d.ts +1 -0
- package/dashboard/dist/api/notes-api.test.js +113 -0
- package/dashboard/dist/api/safeFetch.d.ts +50 -0
- package/dashboard/dist/api/safeFetch.js +135 -0
- package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
- package/dashboard/dist/api/safeFetch.test.js +215 -0
- package/dashboard/dist/api/tasks-api.d.ts +32 -0
- package/dashboard/dist/api/tasks-api.js +98 -0
- package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
- package/dashboard/dist/api/tasks-api.test.js +383 -0
- package/dashboard/dist/components/BugsPane.d.ts +20 -0
- package/dashboard/dist/components/BugsPane.js +210 -0
- package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
- package/dashboard/dist/components/BugsPane.test.js +256 -0
- package/dashboard/dist/components/HealthPane.d.ts +3 -1
- package/dashboard/dist/components/HealthPane.js +44 -6
- package/dashboard/dist/components/HealthPane.test.js +105 -2
- package/dashboard/dist/components/RouterPane.d.ts +4 -3
- package/dashboard/dist/components/RouterPane.js +60 -57
- package/dashboard/dist/components/RouterPane.test.js +150 -96
- package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
- package/dashboard/dist/components/UpdateBanner.js +30 -0
- package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
- package/dashboard/dist/components/UpdateBanner.test.js +96 -0
- package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
- package/dashboard/dist/components/ui/EmptyState.js +58 -0
- package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
- package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
- package/dashboard/dist/components/ui/ErrorState.js +80 -0
- package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
- package/dashboard/package.json +3 -0
- package/package.json +4 -1
- package/server/dashboard/index.html +284 -13
- package/server/dashboard/login.html +262 -0
- package/server/index.js +304 -0
- package/server/lib/api-provider.js +104 -186
- package/server/lib/api-provider.test.js +238 -336
- package/server/lib/cli-detector.js +90 -166
- package/server/lib/cli-detector.test.js +114 -269
- package/server/lib/cli-provider.js +142 -212
- package/server/lib/cli-provider.test.js +196 -349
- package/server/lib/debug.test.js +3 -3
- package/server/lib/devserver-router-api.js +54 -249
- package/server/lib/devserver-router-api.test.js +126 -426
- package/server/lib/introspect.js +309 -0
- package/server/lib/introspect.test.js +286 -0
- package/server/lib/model-router.js +107 -245
- package/server/lib/model-router.test.js +122 -313
- package/server/lib/output-schemas.js +146 -269
- package/server/lib/output-schemas.test.js +106 -307
- package/server/lib/plan-parser.js +59 -16
- package/server/lib/provider-interface.js +99 -153
- package/server/lib/provider-interface.test.js +228 -394
- package/server/lib/provider-queue.js +164 -158
- package/server/lib/provider-queue.test.js +186 -315
- package/server/lib/router-config.js +99 -221
- package/server/lib/router-config.test.js +83 -237
- package/server/lib/router-setup-command.js +94 -419
- package/server/lib/router-setup-command.test.js +96 -375
- package/server/lib/router-status-api.js +93 -0
- package/server/lib/router-status-api.test.js +270 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLC Self-Awareness Module
|
|
3
|
+
*
|
|
4
|
+
* Scans the TLC codebase and generates a manifest showing:
|
|
5
|
+
* - All modules and their test coverage
|
|
6
|
+
* - All API endpoints
|
|
7
|
+
* - All dashboard panels and their API dependencies
|
|
8
|
+
* - Detected mismatches/issues
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scan server/lib/*.js files and check for test coverage
|
|
16
|
+
* @param {string} serverDir - Path to server directory
|
|
17
|
+
* @returns {Promise<Array<{name: string, hasTests: boolean, testCount: number}>>}
|
|
18
|
+
*/
|
|
19
|
+
async function scanModules(serverDir) {
|
|
20
|
+
const libDir = path.join(serverDir, 'lib');
|
|
21
|
+
const modules = [];
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(libDir)) {
|
|
24
|
+
return modules;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const files = fs.readdirSync(libDir);
|
|
28
|
+
const jsFiles = files.filter(f => f.endsWith('.js') && !f.endsWith('.test.js'));
|
|
29
|
+
|
|
30
|
+
for (const file of jsFiles) {
|
|
31
|
+
const name = file.replace('.js', '');
|
|
32
|
+
const testFile = path.join(libDir, `${name}.test.js`);
|
|
33
|
+
const hasTests = fs.existsSync(testFile);
|
|
34
|
+
let testCount = 0;
|
|
35
|
+
|
|
36
|
+
if (hasTests) {
|
|
37
|
+
const testContent = fs.readFileSync(testFile, 'utf-8');
|
|
38
|
+
// Count it() or test() calls
|
|
39
|
+
const itMatches = testContent.match(/\bit\s*\(/g) || [];
|
|
40
|
+
const testMatches = testContent.match(/\btest\s*\(/g) || [];
|
|
41
|
+
testCount = itMatches.length + testMatches.length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
modules.push({
|
|
45
|
+
name,
|
|
46
|
+
hasTests,
|
|
47
|
+
testCount,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Scan server/index.js to extract API routes
|
|
56
|
+
* @param {string} serverDir - Path to server directory
|
|
57
|
+
* @returns {Promise<Array<{method: string, path: string, handler: string}>>}
|
|
58
|
+
*/
|
|
59
|
+
async function scanAPIs(serverDir) {
|
|
60
|
+
const indexFile = path.join(serverDir, 'index.js');
|
|
61
|
+
const apis = [];
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(indexFile)) {
|
|
64
|
+
return apis;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const content = fs.readFileSync(indexFile, 'utf-8');
|
|
68
|
+
|
|
69
|
+
// Match patterns like: app.get('/api/status', ...
|
|
70
|
+
// app.post('/api/test', ...
|
|
71
|
+
// app.patch('/api/agents/:id', ...
|
|
72
|
+
// app.delete('/api/agents/:id', ...
|
|
73
|
+
const routeRegex = /app\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
74
|
+
|
|
75
|
+
let match;
|
|
76
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
77
|
+
const method = match[1].toUpperCase();
|
|
78
|
+
const routePath = match[2];
|
|
79
|
+
|
|
80
|
+
// Extract handler name if possible (function name or inline)
|
|
81
|
+
// Look for the pattern after the path
|
|
82
|
+
const afterPath = content.slice(match.index + match[0].length, match.index + match[0].length + 200);
|
|
83
|
+
let handler = 'anonymous';
|
|
84
|
+
|
|
85
|
+
// Try to find (req, res) => or function name
|
|
86
|
+
const handlerMatch = afterPath.match(/,\s*(?:async\s+)?(\w+)\s*\)|,\s*\(?(?:async\s+)?\(?\s*(?:req|request)/);
|
|
87
|
+
if (handlerMatch && handlerMatch[1] && handlerMatch[1] !== 'req' && handlerMatch[1] !== 'request') {
|
|
88
|
+
handler = handlerMatch[1];
|
|
89
|
+
} else if (afterPath.match(/,\s*\(?(?:async\s+)?\(?\s*(?:req|request)/)) {
|
|
90
|
+
handler = 'inline';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
apis.push({
|
|
94
|
+
method,
|
|
95
|
+
path: routePath,
|
|
96
|
+
handler,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return apis.sort((a, b) => a.path.localeCompare(b.path));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scan dashboard/index.html to extract panels and their API calls
|
|
105
|
+
* @param {string} serverDir - Path to server directory
|
|
106
|
+
* @returns {Promise<Array<{panelId: string, apiCalls: string[]}>>}
|
|
107
|
+
*/
|
|
108
|
+
async function scanDashboard(serverDir) {
|
|
109
|
+
const dashboardFile = path.join(serverDir, 'dashboard', 'index.html');
|
|
110
|
+
const panels = [];
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(dashboardFile)) {
|
|
113
|
+
return panels;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const content = fs.readFileSync(dashboardFile, 'utf-8');
|
|
117
|
+
|
|
118
|
+
// Extract panel IDs from HTML (id="panel-xxx" or class="panel" id="xxx")
|
|
119
|
+
const panelRegex = /id\s*=\s*["']panel-(\w+)["']/gi;
|
|
120
|
+
const panelIds = new Set();
|
|
121
|
+
let match;
|
|
122
|
+
while ((match = panelRegex.exec(content)) !== null) {
|
|
123
|
+
panelIds.add(match[1]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Extract all fetch() API calls
|
|
127
|
+
const fetchRegex = /fetch\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
128
|
+
const allApiCalls = new Set();
|
|
129
|
+
while ((match = fetchRegex.exec(content)) !== null) {
|
|
130
|
+
const url = match[1];
|
|
131
|
+
// Only include /api/ paths
|
|
132
|
+
if (url.startsWith('/api/')) {
|
|
133
|
+
allApiCalls.add(url);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// For simplicity, associate all API calls with a general "dashboard" panel
|
|
138
|
+
// and also create entries for each detected panel
|
|
139
|
+
const apiCallsArray = Array.from(allApiCalls).sort();
|
|
140
|
+
|
|
141
|
+
// Add main dashboard panel with all calls
|
|
142
|
+
if (apiCallsArray.length > 0) {
|
|
143
|
+
panels.push({
|
|
144
|
+
panelId: 'dashboard',
|
|
145
|
+
apiCalls: apiCallsArray,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Try to associate specific API calls with panels based on context
|
|
150
|
+
// Look for functions that call fetch within panel-related code
|
|
151
|
+
const functionApiMap = extractFunctionApiCalls(content);
|
|
152
|
+
|
|
153
|
+
// Map specific panels to their likely API calls
|
|
154
|
+
const panelApiMap = {
|
|
155
|
+
'projects': ['/api/status', '/api/progress'],
|
|
156
|
+
'tasks': ['/api/tasks'],
|
|
157
|
+
'agents': ['/api/agents', '/api/agents-stats'],
|
|
158
|
+
'logs': ['/api/logs'],
|
|
159
|
+
'github': ['/api/changelog'],
|
|
160
|
+
'health': ['/api/health'],
|
|
161
|
+
'router': ['/api/router/status'],
|
|
162
|
+
'preview': [],
|
|
163
|
+
'chat': [],
|
|
164
|
+
'settings': [],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
for (const panelId of panelIds) {
|
|
168
|
+
const knownCalls = panelApiMap[panelId] || [];
|
|
169
|
+
const actualCalls = knownCalls.filter(c => apiCallsArray.some(a => a.startsWith(c.replace(':id', ''))));
|
|
170
|
+
|
|
171
|
+
panels.push({
|
|
172
|
+
panelId,
|
|
173
|
+
apiCalls: actualCalls,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return panels.sort((a, b) => a.panelId.localeCompare(b.panelId));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Helper to extract function names and their fetch calls
|
|
182
|
+
*/
|
|
183
|
+
function extractFunctionApiCalls(content) {
|
|
184
|
+
const map = {};
|
|
185
|
+
|
|
186
|
+
// Match async function name() { ... fetch('/api/xxx') ... }
|
|
187
|
+
const funcRegex = /(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{([^}]+)\}/gi;
|
|
188
|
+
let match;
|
|
189
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
190
|
+
const funcName = match[1];
|
|
191
|
+
const funcBody = match[2];
|
|
192
|
+
const fetchMatch = funcBody.match(/fetch\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
193
|
+
if (fetchMatch && fetchMatch[1].startsWith('/api/')) {
|
|
194
|
+
map[funcName] = fetchMatch[1];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return map;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Detect mismatches between modules, APIs, and dashboard
|
|
203
|
+
* @param {Array} modules - Result from scanModules
|
|
204
|
+
* @param {Array} apis - Result from scanAPIs
|
|
205
|
+
* @param {Array} panels - Result from scanDashboard
|
|
206
|
+
* @returns {string[]} Array of issue descriptions
|
|
207
|
+
*/
|
|
208
|
+
function detectMismatches(modules, apis, panels) {
|
|
209
|
+
const issues = [];
|
|
210
|
+
|
|
211
|
+
// 1. Modules without tests
|
|
212
|
+
const untestedModules = modules.filter(m => !m.hasTests);
|
|
213
|
+
for (const mod of untestedModules) {
|
|
214
|
+
issues.push(`Module '${mod.name}' has no tests`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 2. Dashboard panels calling non-existent APIs
|
|
218
|
+
const apiPaths = new Set(apis.map(a => a.path));
|
|
219
|
+
|
|
220
|
+
// Normalize API path by removing :param patterns
|
|
221
|
+
const normalizeApiPath = (path) => {
|
|
222
|
+
return path.replace(/\/:[^/]+/g, '/:id');
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const normalizedApiPaths = new Set(apis.map(a => normalizeApiPath(a.path)));
|
|
226
|
+
|
|
227
|
+
for (const panel of panels) {
|
|
228
|
+
for (const apiCall of panel.apiCalls) {
|
|
229
|
+
const normalizedCall = normalizeApiPath(apiCall);
|
|
230
|
+
// Check if the API exists (exact match or parameterized match)
|
|
231
|
+
const exists = apiPaths.has(apiCall) || normalizedApiPaths.has(normalizedCall);
|
|
232
|
+
if (!exists) {
|
|
233
|
+
issues.push(`Panel '${panel.panelId}' calls missing API: ${apiCall}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return issues;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate the manifest markdown content
|
|
243
|
+
* @param {string} serverDir - Path to server directory
|
|
244
|
+
* @returns {Promise<string>} Markdown content
|
|
245
|
+
*/
|
|
246
|
+
async function generateManifest(serverDir) {
|
|
247
|
+
const modules = await scanModules(serverDir);
|
|
248
|
+
const apis = await scanAPIs(serverDir);
|
|
249
|
+
const panels = await scanDashboard(serverDir);
|
|
250
|
+
const issues = detectMismatches(modules, apis, panels);
|
|
251
|
+
|
|
252
|
+
const timestamp = new Date().toISOString();
|
|
253
|
+
|
|
254
|
+
let markdown = `# TLC Manifest (auto-generated)
|
|
255
|
+
Generated: ${timestamp}
|
|
256
|
+
|
|
257
|
+
## Modules
|
|
258
|
+
| Module | Has Tests | Test Count |
|
|
259
|
+
|--------|-----------|------------|
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
for (const mod of modules) {
|
|
263
|
+
markdown += `| ${mod.name} | ${mod.hasTests ? 'Yes' : 'No'} | ${mod.testCount} |\n`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
markdown += `
|
|
267
|
+
## API Endpoints
|
|
268
|
+
| Method | Path | Handler |
|
|
269
|
+
|--------|------|---------|
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
for (const api of apis) {
|
|
273
|
+
markdown += `| ${api.method} | ${api.path} | ${api.handler} |\n`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
markdown += `
|
|
277
|
+
## Dashboard Panels
|
|
278
|
+
| Panel | API Calls |
|
|
279
|
+
|-------|-----------|
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
for (const panel of panels) {
|
|
283
|
+
const calls = panel.apiCalls.length > 0 ? panel.apiCalls.join(', ') : 'None';
|
|
284
|
+
markdown += `| ${panel.panelId} | ${calls} |\n`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
markdown += `
|
|
288
|
+
## Issues Detected
|
|
289
|
+
`;
|
|
290
|
+
|
|
291
|
+
if (issues.length === 0) {
|
|
292
|
+
markdown += '- No issues detected\n';
|
|
293
|
+
} else {
|
|
294
|
+
for (const issue of issues) {
|
|
295
|
+
markdown += `- ${issue}\n`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return markdown;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Export for CommonJS
|
|
303
|
+
module.exports = {
|
|
304
|
+
scanModules,
|
|
305
|
+
scanAPIs,
|
|
306
|
+
scanDashboard,
|
|
307
|
+
generateManifest,
|
|
308
|
+
detectMismatches,
|
|
309
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
scanModules,
|
|
4
|
+
scanAPIs,
|
|
5
|
+
scanDashboard,
|
|
6
|
+
generateManifest,
|
|
7
|
+
detectMismatches,
|
|
8
|
+
} from './introspect.js';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const serverDir = path.resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
// Longer timeout for filesystem operations with many files
|
|
16
|
+
const SCAN_TIMEOUT = 30000;
|
|
17
|
+
|
|
18
|
+
describe('introspect', () => {
|
|
19
|
+
describe('scanModules', () => {
|
|
20
|
+
it('returns array of module objects', async () => {
|
|
21
|
+
const modules = await scanModules(serverDir);
|
|
22
|
+
|
|
23
|
+
expect(Array.isArray(modules)).toBe(true);
|
|
24
|
+
expect(modules.length).toBeGreaterThan(0);
|
|
25
|
+
}, SCAN_TIMEOUT);
|
|
26
|
+
|
|
27
|
+
it('each module has name, hasTests, testCount properties', async () => {
|
|
28
|
+
const modules = await scanModules(serverDir);
|
|
29
|
+
|
|
30
|
+
modules.forEach(mod => {
|
|
31
|
+
expect(mod).toHaveProperty('name');
|
|
32
|
+
expect(mod).toHaveProperty('hasTests');
|
|
33
|
+
expect(mod).toHaveProperty('testCount');
|
|
34
|
+
expect(typeof mod.name).toBe('string');
|
|
35
|
+
expect(typeof mod.hasTests).toBe('boolean');
|
|
36
|
+
expect(typeof mod.testCount).toBe('number');
|
|
37
|
+
});
|
|
38
|
+
}, SCAN_TIMEOUT);
|
|
39
|
+
|
|
40
|
+
it('detects modules with existing test files', async () => {
|
|
41
|
+
const modules = await scanModules(serverDir);
|
|
42
|
+
|
|
43
|
+
// agent-registry has tests (we know this exists)
|
|
44
|
+
const agentRegistry = modules.find(m => m.name === 'agent-registry');
|
|
45
|
+
expect(agentRegistry).toBeDefined();
|
|
46
|
+
expect(agentRegistry.hasTests).toBe(true);
|
|
47
|
+
}, SCAN_TIMEOUT);
|
|
48
|
+
|
|
49
|
+
it('counts test cases in test files', async () => {
|
|
50
|
+
const modules = await scanModules(serverDir);
|
|
51
|
+
|
|
52
|
+
// agent-registry has multiple tests
|
|
53
|
+
const agentRegistry = modules.find(m => m.name === 'agent-registry');
|
|
54
|
+
expect(agentRegistry).toBeDefined();
|
|
55
|
+
expect(agentRegistry.testCount).toBeGreaterThan(0);
|
|
56
|
+
}, SCAN_TIMEOUT);
|
|
57
|
+
|
|
58
|
+
it('identifies modules without tests', async () => {
|
|
59
|
+
const modules = await scanModules(serverDir);
|
|
60
|
+
|
|
61
|
+
// Check if any modules lack tests (there might be some)
|
|
62
|
+
const withoutTests = modules.filter(m => !m.hasTests);
|
|
63
|
+
// This is just to verify the structure works
|
|
64
|
+
expect(Array.isArray(withoutTests)).toBe(true);
|
|
65
|
+
}, SCAN_TIMEOUT);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('scanAPIs', () => {
|
|
69
|
+
it('returns array of API endpoint objects', async () => {
|
|
70
|
+
const apis = await scanAPIs(serverDir);
|
|
71
|
+
|
|
72
|
+
expect(Array.isArray(apis)).toBe(true);
|
|
73
|
+
expect(apis.length).toBeGreaterThan(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('each API has method, path, handler properties', async () => {
|
|
77
|
+
const apis = await scanAPIs(serverDir);
|
|
78
|
+
|
|
79
|
+
apis.forEach(api => {
|
|
80
|
+
expect(api).toHaveProperty('method');
|
|
81
|
+
expect(api).toHaveProperty('path');
|
|
82
|
+
expect(api).toHaveProperty('handler');
|
|
83
|
+
expect(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).toContain(api.method);
|
|
84
|
+
expect(typeof api.path).toBe('string');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('extracts GET endpoints', async () => {
|
|
89
|
+
const apis = await scanAPIs(serverDir);
|
|
90
|
+
|
|
91
|
+
const getEndpoints = apis.filter(a => a.method === 'GET');
|
|
92
|
+
expect(getEndpoints.length).toBeGreaterThan(0);
|
|
93
|
+
|
|
94
|
+
// We know /api/status exists from reading index.js
|
|
95
|
+
const statusEndpoint = apis.find(a => a.path === '/api/status');
|
|
96
|
+
expect(statusEndpoint).toBeDefined();
|
|
97
|
+
expect(statusEndpoint.method).toBe('GET');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('extracts POST endpoints', async () => {
|
|
101
|
+
const apis = await scanAPIs(serverDir);
|
|
102
|
+
|
|
103
|
+
const postEndpoints = apis.filter(a => a.method === 'POST');
|
|
104
|
+
expect(postEndpoints.length).toBeGreaterThan(0);
|
|
105
|
+
|
|
106
|
+
// We know /api/test exists from reading index.js
|
|
107
|
+
const testEndpoint = apis.find(a => a.path === '/api/test');
|
|
108
|
+
expect(testEndpoint).toBeDefined();
|
|
109
|
+
expect(testEndpoint.method).toBe('POST');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('extracts PATCH endpoints', async () => {
|
|
113
|
+
const apis = await scanAPIs(serverDir);
|
|
114
|
+
|
|
115
|
+
const patchEndpoints = apis.filter(a => a.method === 'PATCH');
|
|
116
|
+
// We know /api/agents/:id PATCH exists
|
|
117
|
+
expect(patchEndpoints.length).toBeGreaterThan(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('extracts DELETE endpoints', async () => {
|
|
121
|
+
const apis = await scanAPIs(serverDir);
|
|
122
|
+
|
|
123
|
+
const deleteEndpoints = apis.filter(a => a.method === 'DELETE');
|
|
124
|
+
// We know /api/agents/:id DELETE exists
|
|
125
|
+
expect(deleteEndpoints.length).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('scanDashboard', () => {
|
|
130
|
+
it('returns array of panel objects', async () => {
|
|
131
|
+
const panels = await scanDashboard(serverDir);
|
|
132
|
+
|
|
133
|
+
expect(Array.isArray(panels)).toBe(true);
|
|
134
|
+
expect(panels.length).toBeGreaterThan(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('each panel has panelId and apiCalls properties', async () => {
|
|
138
|
+
const panels = await scanDashboard(serverDir);
|
|
139
|
+
|
|
140
|
+
panels.forEach(panel => {
|
|
141
|
+
expect(panel).toHaveProperty('panelId');
|
|
142
|
+
expect(panel).toHaveProperty('apiCalls');
|
|
143
|
+
expect(typeof panel.panelId).toBe('string');
|
|
144
|
+
expect(Array.isArray(panel.apiCalls)).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('extracts fetch API calls from dashboard', async () => {
|
|
149
|
+
const panels = await scanDashboard(serverDir);
|
|
150
|
+
|
|
151
|
+
// Collect all API calls
|
|
152
|
+
const allCalls = panels.flatMap(p => p.apiCalls);
|
|
153
|
+
expect(allCalls.length).toBeGreaterThan(0);
|
|
154
|
+
|
|
155
|
+
// We know /api/status is called from the dashboard
|
|
156
|
+
expect(allCalls).toContain('/api/status');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('identifies panels with their API dependencies', async () => {
|
|
160
|
+
const panels = await scanDashboard(serverDir);
|
|
161
|
+
|
|
162
|
+
// Check that we have multiple panels identified
|
|
163
|
+
const panelIds = panels.map(p => p.panelId);
|
|
164
|
+
expect(panelIds.length).toBeGreaterThan(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('generateManifest', () => {
|
|
169
|
+
it('creates valid markdown content', async () => {
|
|
170
|
+
const manifest = await generateManifest(serverDir);
|
|
171
|
+
|
|
172
|
+
expect(typeof manifest).toBe('string');
|
|
173
|
+
expect(manifest.length).toBeGreaterThan(0);
|
|
174
|
+
}, SCAN_TIMEOUT);
|
|
175
|
+
|
|
176
|
+
it('includes auto-generated header with timestamp', async () => {
|
|
177
|
+
const manifest = await generateManifest(serverDir);
|
|
178
|
+
|
|
179
|
+
expect(manifest).toContain('# TLC Manifest (auto-generated)');
|
|
180
|
+
expect(manifest).toContain('Generated:');
|
|
181
|
+
}, SCAN_TIMEOUT);
|
|
182
|
+
|
|
183
|
+
it('includes Modules section with table', async () => {
|
|
184
|
+
const manifest = await generateManifest(serverDir);
|
|
185
|
+
|
|
186
|
+
expect(manifest).toContain('## Modules');
|
|
187
|
+
expect(manifest).toContain('| Module | Has Tests | Test Count |');
|
|
188
|
+
expect(manifest).toContain('|--------|-----------|------------|');
|
|
189
|
+
}, SCAN_TIMEOUT);
|
|
190
|
+
|
|
191
|
+
it('includes API Endpoints section with table', async () => {
|
|
192
|
+
const manifest = await generateManifest(serverDir);
|
|
193
|
+
|
|
194
|
+
expect(manifest).toContain('## API Endpoints');
|
|
195
|
+
expect(manifest).toContain('| Method | Path | Handler |');
|
|
196
|
+
expect(manifest).toContain('|--------|------|---------|');
|
|
197
|
+
}, SCAN_TIMEOUT);
|
|
198
|
+
|
|
199
|
+
it('includes Dashboard Panels section with table', async () => {
|
|
200
|
+
const manifest = await generateManifest(serverDir);
|
|
201
|
+
|
|
202
|
+
expect(manifest).toContain('## Dashboard Panels');
|
|
203
|
+
expect(manifest).toContain('| Panel | API Calls |');
|
|
204
|
+
expect(manifest).toContain('|-------|-----------|');
|
|
205
|
+
}, SCAN_TIMEOUT);
|
|
206
|
+
|
|
207
|
+
it('includes Issues Detected section', async () => {
|
|
208
|
+
const manifest = await generateManifest(serverDir);
|
|
209
|
+
|
|
210
|
+
expect(manifest).toContain('## Issues Detected');
|
|
211
|
+
}, SCAN_TIMEOUT);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('detectMismatches', () => {
|
|
215
|
+
it('returns array of mismatch issues', async () => {
|
|
216
|
+
const modules = await scanModules(serverDir);
|
|
217
|
+
const apis = await scanAPIs(serverDir);
|
|
218
|
+
const panels = await scanDashboard(serverDir);
|
|
219
|
+
|
|
220
|
+
const mismatches = detectMismatches(modules, apis, panels);
|
|
221
|
+
|
|
222
|
+
expect(Array.isArray(mismatches)).toBe(true);
|
|
223
|
+
}, SCAN_TIMEOUT);
|
|
224
|
+
|
|
225
|
+
it('detects panels calling non-existent APIs', () => {
|
|
226
|
+
const modules = [];
|
|
227
|
+
const apis = [
|
|
228
|
+
{ method: 'GET', path: '/api/status', handler: 'getStatus' },
|
|
229
|
+
];
|
|
230
|
+
const panels = [
|
|
231
|
+
{ panelId: 'test-panel', apiCalls: ['/api/status', '/api/missing'] },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const mismatches = detectMismatches(modules, apis, panels);
|
|
235
|
+
|
|
236
|
+
expect(mismatches.length).toBeGreaterThan(0);
|
|
237
|
+
expect(mismatches.some(m => m.includes('/api/missing'))).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('detects modules without tests', () => {
|
|
241
|
+
const modules = [
|
|
242
|
+
{ name: 'tested-module', hasTests: true, testCount: 5 },
|
|
243
|
+
{ name: 'untested-module', hasTests: false, testCount: 0 },
|
|
244
|
+
];
|
|
245
|
+
const apis = [];
|
|
246
|
+
const panels = [];
|
|
247
|
+
|
|
248
|
+
const mismatches = detectMismatches(modules, apis, panels);
|
|
249
|
+
|
|
250
|
+
expect(mismatches.some(m => m.includes('untested-module') && m.includes('no tests'))).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('detects APIs not called by any dashboard panel', () => {
|
|
254
|
+
const modules = [];
|
|
255
|
+
const apis = [
|
|
256
|
+
{ method: 'GET', path: '/api/status', handler: 'getStatus' },
|
|
257
|
+
{ method: 'GET', path: '/api/orphan', handler: 'getOrphan' },
|
|
258
|
+
];
|
|
259
|
+
const panels = [
|
|
260
|
+
{ panelId: 'test-panel', apiCalls: ['/api/status'] },
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const mismatches = detectMismatches(modules, apis, panels);
|
|
264
|
+
|
|
265
|
+
// This might flag orphan APIs (APIs not used by dashboard)
|
|
266
|
+
// Or might not - depends on whether we consider this a mismatch
|
|
267
|
+
expect(Array.isArray(mismatches)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('returns empty array when no mismatches found', () => {
|
|
271
|
+
const modules = [
|
|
272
|
+
{ name: 'tested-module', hasTests: true, testCount: 5 },
|
|
273
|
+
];
|
|
274
|
+
const apis = [
|
|
275
|
+
{ method: 'GET', path: '/api/status', handler: 'getStatus' },
|
|
276
|
+
];
|
|
277
|
+
const panels = [
|
|
278
|
+
{ panelId: 'test-panel', apiCalls: ['/api/status'] },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const mismatches = detectMismatches(modules, apis, panels);
|
|
282
|
+
|
|
283
|
+
expect(Array.isArray(mismatches)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|