tlc-claude-code 1.4.8 → 1.4.9
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/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notes API Module
|
|
3
|
+
* PROJECT.md and BUGS.md management
|
|
4
|
+
*/
|
|
5
|
+
import { promises as defaultFs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get notes (PROJECT.md) content
|
|
10
|
+
* @param {Object} options - Options
|
|
11
|
+
* @returns {Promise<Object>} Notes data
|
|
12
|
+
*/
|
|
13
|
+
export async function getNotes(options = {}) {
|
|
14
|
+
const fs = options.fs || defaultFs;
|
|
15
|
+
const basePath = options.basePath || process.cwd();
|
|
16
|
+
|
|
17
|
+
const filePath = path.join(basePath, 'PROJECT.md');
|
|
18
|
+
|
|
19
|
+
let content = '';
|
|
20
|
+
let exists = true;
|
|
21
|
+
let lastModified = null;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
25
|
+
} catch {
|
|
26
|
+
content = '';
|
|
27
|
+
exists = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (fs.stat) {
|
|
32
|
+
const stats = await fs.stat(filePath);
|
|
33
|
+
lastModified = stats.mtime;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore stat errors
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content,
|
|
41
|
+
type: 'project',
|
|
42
|
+
exists,
|
|
43
|
+
lastModified
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Update notes (PROJECT.md)
|
|
49
|
+
* @param {string} content - New content
|
|
50
|
+
* @param {Object} options - Options
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
export async function updateNotes(content, options = {}) {
|
|
54
|
+
const fs = options.fs || defaultFs;
|
|
55
|
+
const basePath = options.basePath || process.cwd();
|
|
56
|
+
const backup = options.backup || false;
|
|
57
|
+
const validate = options.validate || false;
|
|
58
|
+
|
|
59
|
+
if (validate && (!content || !content.trim())) {
|
|
60
|
+
throw new Error('Content cannot be empty');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filePath = path.join(basePath, 'PROJECT.md');
|
|
64
|
+
|
|
65
|
+
// Create backup if requested
|
|
66
|
+
if (backup) {
|
|
67
|
+
try {
|
|
68
|
+
const oldContent = await fs.readFile(filePath, 'utf-8');
|
|
69
|
+
const backupPath = path.join(basePath, `PROJECT.md.backup.${Date.now()}`);
|
|
70
|
+
await fs.writeFile(backupPath, oldContent);
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore backup errors for missing files
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await fs.writeFile(filePath, content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get bugs (BUGS.md) content
|
|
81
|
+
* @param {Object} options - Options
|
|
82
|
+
* @returns {Promise<Object>} Bugs data
|
|
83
|
+
*/
|
|
84
|
+
export async function getBugs(options = {}) {
|
|
85
|
+
const fs = options.fs || defaultFs;
|
|
86
|
+
const basePath = options.basePath || process.cwd();
|
|
87
|
+
const parse = options.parse || false;
|
|
88
|
+
|
|
89
|
+
const filePath = path.join(basePath, '.planning', 'BUGS.md');
|
|
90
|
+
|
|
91
|
+
let content = '';
|
|
92
|
+
let exists = true;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
96
|
+
} catch {
|
|
97
|
+
content = '';
|
|
98
|
+
exists = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = {
|
|
102
|
+
content,
|
|
103
|
+
exists
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (parse) {
|
|
107
|
+
result.entries = parseBugEntries(content);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse bug entries from BUGS.md content
|
|
115
|
+
* @param {string} content - BUGS.md content
|
|
116
|
+
* @returns {Array} Bug entries
|
|
117
|
+
*/
|
|
118
|
+
function parseBugEntries(content) {
|
|
119
|
+
const entries = [];
|
|
120
|
+
const lines = content.split('\n');
|
|
121
|
+
|
|
122
|
+
let currentBug = null;
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
// Match bug header: ## Bug: Title
|
|
126
|
+
const bugMatch = line.match(/^##\s+Bug:\s*(.+)$/);
|
|
127
|
+
if (bugMatch) {
|
|
128
|
+
if (currentBug) {
|
|
129
|
+
entries.push(currentBug);
|
|
130
|
+
}
|
|
131
|
+
currentBug = {
|
|
132
|
+
title: bugMatch[1].trim(),
|
|
133
|
+
severity: null,
|
|
134
|
+
status: null
|
|
135
|
+
};
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (currentBug) {
|
|
140
|
+
// Match severity: **Severity:** Value
|
|
141
|
+
const severityMatch = line.match(/^\*\*Severity:\*\*\s*(.+)$/);
|
|
142
|
+
if (severityMatch) {
|
|
143
|
+
currentBug.severity = severityMatch[1].trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Match status: **Status:** Value
|
|
147
|
+
const statusMatch = line.match(/^\*\*Status:\*\*\s*(.+)$/);
|
|
148
|
+
if (statusMatch) {
|
|
149
|
+
currentBug.status = statusMatch[1].trim();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (currentBug) {
|
|
155
|
+
entries.push(currentBug);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add a bug to BUGS.md
|
|
163
|
+
* @param {Object} bugData - Bug data
|
|
164
|
+
* @param {Object} options - Options
|
|
165
|
+
* @returns {Promise<Object>} Created bug
|
|
166
|
+
*/
|
|
167
|
+
export async function addBug(bugData, options = {}) {
|
|
168
|
+
if (!bugData.title) {
|
|
169
|
+
throw new Error('Title is required');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const fs = options.fs || defaultFs;
|
|
173
|
+
const basePath = options.basePath || process.cwd();
|
|
174
|
+
|
|
175
|
+
const filePath = path.join(basePath, '.planning', 'BUGS.md');
|
|
176
|
+
|
|
177
|
+
let content = '';
|
|
178
|
+
try {
|
|
179
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
180
|
+
} catch {
|
|
181
|
+
content = '# Bugs\n\n';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create bug entry
|
|
185
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
186
|
+
const severity = bugData.severity || 'Medium';
|
|
187
|
+
const description = bugData.description || '';
|
|
188
|
+
|
|
189
|
+
const bugEntry = `
|
|
190
|
+
## Bug: ${bugData.title}
|
|
191
|
+
**Severity:** ${severity}
|
|
192
|
+
**Status:** Open
|
|
193
|
+
**Date:** ${timestamp}
|
|
194
|
+
|
|
195
|
+
${description}
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const newContent = content + bugEntry;
|
|
199
|
+
await fs.writeFile(filePath, newContent);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
title: bugData.title,
|
|
203
|
+
severity,
|
|
204
|
+
status: 'Open',
|
|
205
|
+
date: timestamp
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create Notes API handlers
|
|
211
|
+
* @param {Object} options - Options
|
|
212
|
+
* @returns {Object} API handlers
|
|
213
|
+
*/
|
|
214
|
+
export function createNotesApi(options = {}) {
|
|
215
|
+
const { basePath = process.cwd(), fs: fileSystem = defaultFs } = options;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
async getNotes(opts = {}) {
|
|
219
|
+
return getNotes({ fs: fileSystem, basePath, ...opts });
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async updateNotes(content, opts = {}) {
|
|
223
|
+
return updateNotes(content, { fs: fileSystem, basePath, ...opts });
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async getBugs(opts = {}) {
|
|
227
|
+
return getBugs({ fs: fileSystem, basePath, ...opts });
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async addBug(bugData, opts = {}) {
|
|
231
|
+
return addBug(bugData, { fs: fileSystem, basePath, ...opts });
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notes API Module Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { getNotes, updateNotes, getBugs, addBug, createNotesApi } from './notes-api.js';
|
|
6
|
+
|
|
7
|
+
describe('notes-api', () => {
|
|
8
|
+
describe('getNotes', () => {
|
|
9
|
+
it('returns PROJECT.md content', async () => {
|
|
10
|
+
const mockFs = {
|
|
11
|
+
readFile: vi.fn().mockResolvedValue('# Project\n\nDescription')
|
|
12
|
+
};
|
|
13
|
+
const notes = await getNotes({ fs: mockFs, basePath: '/test' });
|
|
14
|
+
expect(notes.content).toContain('Project');
|
|
15
|
+
expect(notes.type).toBe('project');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('includes last modified time', async () => {
|
|
19
|
+
const mockFs = {
|
|
20
|
+
readFile: vi.fn().mockResolvedValue('content'),
|
|
21
|
+
stat: vi.fn().mockResolvedValue({ mtime: new Date() })
|
|
22
|
+
};
|
|
23
|
+
const notes = await getNotes({ fs: mockFs, basePath: '/test' });
|
|
24
|
+
expect(notes.lastModified).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('handles missing file', async () => {
|
|
28
|
+
const mockFs = {
|
|
29
|
+
readFile: vi.fn().mockRejectedValue(new Error('ENOENT'))
|
|
30
|
+
};
|
|
31
|
+
const notes = await getNotes({ fs: mockFs, basePath: '/test' });
|
|
32
|
+
expect(notes.content).toBe('');
|
|
33
|
+
expect(notes.exists).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('updateNotes', () => {
|
|
38
|
+
it('writes PROJECT.md content', async () => {
|
|
39
|
+
const mockFs = {
|
|
40
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
41
|
+
};
|
|
42
|
+
await updateNotes('# Updated', { fs: mockFs, basePath: '/test' });
|
|
43
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
|
44
|
+
expect.stringContaining('PROJECT.md'),
|
|
45
|
+
'# Updated'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('creates backup before update', async () => {
|
|
50
|
+
const mockFs = {
|
|
51
|
+
readFile: vi.fn().mockResolvedValue('old content'),
|
|
52
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
53
|
+
};
|
|
54
|
+
await updateNotes('new content', { fs: mockFs, basePath: '/test', backup: true });
|
|
55
|
+
expect(mockFs.writeFile).toHaveBeenCalledTimes(2); // backup + main
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('validates markdown format', async () => {
|
|
59
|
+
const mockFs = { writeFile: vi.fn() };
|
|
60
|
+
await expect(updateNotes('', { fs: mockFs, validate: true }))
|
|
61
|
+
.rejects.toThrow(/empty/i);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('getBugs', () => {
|
|
66
|
+
it('returns BUGS.md content', async () => {
|
|
67
|
+
const mockFs = {
|
|
68
|
+
readFile: vi.fn().mockResolvedValue('## Bug 1\nDescription')
|
|
69
|
+
};
|
|
70
|
+
const bugs = await getBugs({ fs: mockFs, basePath: '/test' });
|
|
71
|
+
expect(bugs.content).toContain('Bug 1');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('parses bug entries', async () => {
|
|
75
|
+
const mockFs = {
|
|
76
|
+
readFile: vi.fn().mockResolvedValue(`
|
|
77
|
+
## Bug: Login fails
|
|
78
|
+
**Severity:** High
|
|
79
|
+
**Status:** Open
|
|
80
|
+
|
|
81
|
+
## Bug: Slow load
|
|
82
|
+
**Severity:** Low
|
|
83
|
+
**Status:** Fixed
|
|
84
|
+
`)
|
|
85
|
+
};
|
|
86
|
+
const bugs = await getBugs({ fs: mockFs, basePath: '/test', parse: true });
|
|
87
|
+
expect(bugs.entries.length).toBe(2);
|
|
88
|
+
expect(bugs.entries[0].title).toBe('Login fails');
|
|
89
|
+
expect(bugs.entries[0].severity).toBe('High');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('addBug', () => {
|
|
94
|
+
it('appends bug to BUGS.md', async () => {
|
|
95
|
+
const mockFs = {
|
|
96
|
+
readFile: vi.fn().mockResolvedValue('# Bugs\n'),
|
|
97
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
98
|
+
};
|
|
99
|
+
await addBug({
|
|
100
|
+
title: 'New bug',
|
|
101
|
+
description: 'Bug description',
|
|
102
|
+
severity: 'High'
|
|
103
|
+
}, { fs: mockFs, basePath: '/test' });
|
|
104
|
+
const content = mockFs.writeFile.mock.calls[0][1];
|
|
105
|
+
expect(content).toContain('New bug');
|
|
106
|
+
expect(content).toContain('High');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('validates required fields', async () => {
|
|
110
|
+
await expect(addBug({ description: 'No title' }, {}))
|
|
111
|
+
.rejects.toThrow(/title.*required/i);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('adds timestamp', async () => {
|
|
115
|
+
const mockFs = {
|
|
116
|
+
readFile: vi.fn().mockResolvedValue(''),
|
|
117
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
118
|
+
};
|
|
119
|
+
await addBug({ title: 'Bug', severity: 'Low' }, { fs: mockFs, basePath: '/test' });
|
|
120
|
+
const content = mockFs.writeFile.mock.calls[0][1];
|
|
121
|
+
expect(content).toMatch(/\d{4}-\d{2}-\d{2}/); // date format
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('createNotesApi', () => {
|
|
126
|
+
it('creates API handlers', () => {
|
|
127
|
+
const api = createNotesApi({ basePath: '/test' });
|
|
128
|
+
expect(api.getNotes).toBeDefined();
|
|
129
|
+
expect(api.updateNotes).toBeDefined();
|
|
130
|
+
expect(api.getBugs).toBeDefined();
|
|
131
|
+
expect(api.addBug).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Status API
|
|
3
|
+
* Multi-LLM router status API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get router status
|
|
8
|
+
* @param {Object} options - Options
|
|
9
|
+
* @returns {Promise<Object>} Router status
|
|
10
|
+
*/
|
|
11
|
+
export async function getRouterStatus(options = {}) {
|
|
12
|
+
const { router } = options;
|
|
13
|
+
|
|
14
|
+
if (!router) {
|
|
15
|
+
return {
|
|
16
|
+
providers: [],
|
|
17
|
+
overall: 'unknown'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const providers = router.getProviders();
|
|
22
|
+
|
|
23
|
+
// Determine overall status
|
|
24
|
+
const hasError = providers.some(p => p.status === 'error');
|
|
25
|
+
const allActive = providers.every(p => p.status === 'active');
|
|
26
|
+
|
|
27
|
+
let overall;
|
|
28
|
+
if (providers.length === 0) {
|
|
29
|
+
overall = 'unknown';
|
|
30
|
+
} else if (allActive) {
|
|
31
|
+
overall = 'healthy';
|
|
32
|
+
} else if (hasError) {
|
|
33
|
+
overall = 'degraded';
|
|
34
|
+
} else {
|
|
35
|
+
overall = 'healthy';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
providers,
|
|
40
|
+
overall,
|
|
41
|
+
timestamp: new Date().toISOString()
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get provider statistics
|
|
47
|
+
* @param {Object} data - Data containing requests
|
|
48
|
+
* @returns {Object} Provider stats
|
|
49
|
+
*/
|
|
50
|
+
export function getProviderStats(data = {}) {
|
|
51
|
+
const { requests = [] } = data;
|
|
52
|
+
const stats = {};
|
|
53
|
+
|
|
54
|
+
// Group requests by provider
|
|
55
|
+
const byProvider = {};
|
|
56
|
+
for (const req of requests) {
|
|
57
|
+
if (!byProvider[req.provider]) {
|
|
58
|
+
byProvider[req.provider] = [];
|
|
59
|
+
}
|
|
60
|
+
byProvider[req.provider].push(req);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Calculate stats for each provider
|
|
64
|
+
for (const [provider, providerReqs] of Object.entries(byProvider)) {
|
|
65
|
+
const requestCount = providerReqs.length;
|
|
66
|
+
|
|
67
|
+
// Calculate error rate
|
|
68
|
+
const errors = providerReqs.filter(r => r.error === true).length;
|
|
69
|
+
const errorRate = requestCount > 0 ? errors / requestCount : 0;
|
|
70
|
+
|
|
71
|
+
// Calculate average latency
|
|
72
|
+
const latencies = providerReqs.filter(r => r.latency !== undefined).map(r => r.latency);
|
|
73
|
+
const avgLatency = latencies.length > 0
|
|
74
|
+
? latencies.reduce((sum, l) => sum + l, 0) / latencies.length
|
|
75
|
+
: 0;
|
|
76
|
+
|
|
77
|
+
stats[provider] = {
|
|
78
|
+
requests: requestCount,
|
|
79
|
+
errorRate,
|
|
80
|
+
avgLatency
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return stats;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Calculate costs from requests
|
|
89
|
+
* @param {Array} requests - Request array
|
|
90
|
+
* @param {Object} pricing - Pricing per provider
|
|
91
|
+
* @returns {Object} Cost breakdown
|
|
92
|
+
*/
|
|
93
|
+
export function calculateCosts(requests, pricing = {}) {
|
|
94
|
+
const byProvider = {};
|
|
95
|
+
let total = 0;
|
|
96
|
+
|
|
97
|
+
for (const req of requests) {
|
|
98
|
+
const provider = req.provider;
|
|
99
|
+
const providerPricing = pricing[provider];
|
|
100
|
+
|
|
101
|
+
if (!byProvider[provider]) {
|
|
102
|
+
byProvider[provider] = 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (providerPricing) {
|
|
106
|
+
const inputCost = (req.inputTokens || 0) * (providerPricing.input || 0) / 1000;
|
|
107
|
+
const outputCost = (req.outputTokens || 0) * (providerPricing.output || 0) / 1000;
|
|
108
|
+
const cost = inputCost + outputCost;
|
|
109
|
+
byProvider[provider] += cost;
|
|
110
|
+
total += cost;
|
|
111
|
+
} else {
|
|
112
|
+
// No pricing available
|
|
113
|
+
byProvider[provider] = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
total,
|
|
119
|
+
byProvider
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Filter requests by time range
|
|
125
|
+
* @param {Array} requests - Requests to filter
|
|
126
|
+
* @param {Object} range - Time range with start and optional end
|
|
127
|
+
* @returns {Array} Filtered requests
|
|
128
|
+
*/
|
|
129
|
+
export function filterByTimeRange(requests, range = {}) {
|
|
130
|
+
const { start, end } = range;
|
|
131
|
+
|
|
132
|
+
return requests.filter(req => {
|
|
133
|
+
const timestamp = req.timestamp;
|
|
134
|
+
|
|
135
|
+
if (start !== undefined && timestamp < start) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (end !== undefined && timestamp > end) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create Router API handler
|
|
149
|
+
* @param {Object} options - Options
|
|
150
|
+
* @returns {Object} API handlers
|
|
151
|
+
*/
|
|
152
|
+
export function createRouterApi(options = {}) {
|
|
153
|
+
const { router, requestStore } = options;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
async getStatus() {
|
|
157
|
+
return getRouterStatus({ router });
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
getStats(timeRange) {
|
|
161
|
+
let requests = requestStore?.getRequests() || [];
|
|
162
|
+
if (timeRange) {
|
|
163
|
+
requests = filterByTimeRange(requests, timeRange);
|
|
164
|
+
}
|
|
165
|
+
return getProviderStats({ requests });
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
getCosts(pricing, timeRange) {
|
|
169
|
+
let requests = requestStore?.getRequests() || [];
|
|
170
|
+
if (timeRange) {
|
|
171
|
+
requests = filterByTimeRange(requests, timeRange);
|
|
172
|
+
}
|
|
173
|
+
return calculateCosts(requests, pricing);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Status API Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { getRouterStatus, getProviderStats, calculateCosts, filterByTimeRange, createRouterApi } from './router-api.js';
|
|
6
|
+
|
|
7
|
+
describe('router-api', () => {
|
|
8
|
+
describe('getRouterStatus', () => {
|
|
9
|
+
it('returns all provider statuses', async () => {
|
|
10
|
+
const mockRouter = {
|
|
11
|
+
getProviders: vi.fn().mockReturnValue([
|
|
12
|
+
{ name: 'openai', status: 'active' },
|
|
13
|
+
{ name: 'anthropic', status: 'active' }
|
|
14
|
+
])
|
|
15
|
+
};
|
|
16
|
+
const status = await getRouterStatus({ router: mockRouter });
|
|
17
|
+
expect(status.providers.length).toBe(2);
|
|
18
|
+
expect(status.providers[0].name).toBe('openai');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('includes overall status', async () => {
|
|
22
|
+
const mockRouter = {
|
|
23
|
+
getProviders: vi.fn().mockReturnValue([
|
|
24
|
+
{ name: 'openai', status: 'active' }
|
|
25
|
+
])
|
|
26
|
+
};
|
|
27
|
+
const status = await getRouterStatus({ router: mockRouter });
|
|
28
|
+
expect(status.overall).toBe('healthy');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('reports degraded when providers down', async () => {
|
|
32
|
+
const mockRouter = {
|
|
33
|
+
getProviders: vi.fn().mockReturnValue([
|
|
34
|
+
{ name: 'openai', status: 'error' },
|
|
35
|
+
{ name: 'anthropic', status: 'active' }
|
|
36
|
+
])
|
|
37
|
+
};
|
|
38
|
+
const status = await getRouterStatus({ router: mockRouter });
|
|
39
|
+
expect(status.overall).toBe('degraded');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('getProviderStats', () => {
|
|
44
|
+
it('returns request counts', () => {
|
|
45
|
+
const stats = getProviderStats({
|
|
46
|
+
requests: [
|
|
47
|
+
{ provider: 'openai', timestamp: Date.now() },
|
|
48
|
+
{ provider: 'openai', timestamp: Date.now() },
|
|
49
|
+
{ provider: 'anthropic', timestamp: Date.now() }
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
expect(stats.openai.requests).toBe(2);
|
|
53
|
+
expect(stats.anthropic.requests).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('calculates error rates', () => {
|
|
57
|
+
const stats = getProviderStats({
|
|
58
|
+
requests: [
|
|
59
|
+
{ provider: 'openai', error: false },
|
|
60
|
+
{ provider: 'openai', error: true }
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
expect(stats.openai.errorRate).toBe(0.5);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('tracks latency', () => {
|
|
67
|
+
const stats = getProviderStats({
|
|
68
|
+
requests: [
|
|
69
|
+
{ provider: 'openai', latency: 100 },
|
|
70
|
+
{ provider: 'openai', latency: 200 }
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
expect(stats.openai.avgLatency).toBe(150);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('calculateCosts', () => {
|
|
78
|
+
it('calculates total costs', () => {
|
|
79
|
+
const requests = [
|
|
80
|
+
{ provider: 'openai', inputTokens: 1000, outputTokens: 500 },
|
|
81
|
+
{ provider: 'anthropic', inputTokens: 2000, outputTokens: 1000 }
|
|
82
|
+
];
|
|
83
|
+
const costs = calculateCosts(requests, {
|
|
84
|
+
openai: { input: 0.01, output: 0.03 },
|
|
85
|
+
anthropic: { input: 0.008, output: 0.024 }
|
|
86
|
+
});
|
|
87
|
+
expect(costs.total).toBeGreaterThan(0);
|
|
88
|
+
expect(costs.byProvider.openai).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('handles missing pricing', () => {
|
|
92
|
+
const requests = [{ provider: 'unknown', inputTokens: 1000 }];
|
|
93
|
+
const costs = calculateCosts(requests, {});
|
|
94
|
+
expect(costs.byProvider.unknown).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('filterByTimeRange', () => {
|
|
99
|
+
it('filters by date range', () => {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const requests = [
|
|
102
|
+
{ timestamp: now - 1000 },
|
|
103
|
+
{ timestamp: now - 100000 },
|
|
104
|
+
{ timestamp: now - 1000000 }
|
|
105
|
+
];
|
|
106
|
+
const filtered = filterByTimeRange(requests, { start: now - 50000 });
|
|
107
|
+
expect(filtered.length).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('supports end date', () => {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const requests = [
|
|
113
|
+
{ timestamp: now - 1000 },
|
|
114
|
+
{ timestamp: now - 5000 }
|
|
115
|
+
];
|
|
116
|
+
const filtered = filterByTimeRange(requests, {
|
|
117
|
+
start: now - 10000,
|
|
118
|
+
end: now - 3000
|
|
119
|
+
});
|
|
120
|
+
expect(filtered.length).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('createRouterApi', () => {
|
|
125
|
+
it('creates API handler', () => {
|
|
126
|
+
const api = createRouterApi({});
|
|
127
|
+
expect(api.getStatus).toBeDefined();
|
|
128
|
+
expect(api.getStats).toBeDefined();
|
|
129
|
+
expect(api.getCosts).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|