tlc-claude-code 1.4.7 → 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.
Files changed (170) hide show
  1. package/docker-compose.dev.yml +6 -3
  2. package/package.json +1 -1
  3. package/server/index.js +229 -14
  4. package/server/lib/compliance/control-mapper.js +401 -0
  5. package/server/lib/compliance/control-mapper.test.js +117 -0
  6. package/server/lib/compliance/evidence-linker.js +296 -0
  7. package/server/lib/compliance/evidence-linker.test.js +121 -0
  8. package/server/lib/compliance/gdpr-checklist.js +416 -0
  9. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  10. package/server/lib/compliance/hipaa-checklist.js +277 -0
  11. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  12. package/server/lib/compliance/iso27001-checklist.js +287 -0
  13. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  14. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  15. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  16. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  17. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  18. package/server/lib/compliance/trust-centre.js +187 -0
  19. package/server/lib/compliance/trust-centre.test.js +93 -0
  20. package/server/lib/dashboard/api-server.js +155 -0
  21. package/server/lib/dashboard/api-server.test.js +155 -0
  22. package/server/lib/dashboard/health-api.js +199 -0
  23. package/server/lib/dashboard/health-api.test.js +122 -0
  24. package/server/lib/dashboard/notes-api.js +234 -0
  25. package/server/lib/dashboard/notes-api.test.js +134 -0
  26. package/server/lib/dashboard/router-api.js +176 -0
  27. package/server/lib/dashboard/router-api.test.js +132 -0
  28. package/server/lib/dashboard/tasks-api.js +289 -0
  29. package/server/lib/dashboard/tasks-api.test.js +161 -0
  30. package/server/lib/dashboard/tlc-introspection.js +197 -0
  31. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  32. package/server/lib/dashboard/version-api.js +222 -0
  33. package/server/lib/dashboard/version-api.test.js +112 -0
  34. package/server/lib/dashboard/websocket-server.js +104 -0
  35. package/server/lib/dashboard/websocket-server.test.js +118 -0
  36. package/server/lib/deploy/branch-classifier.js +163 -0
  37. package/server/lib/deploy/branch-classifier.test.js +164 -0
  38. package/server/lib/deploy/deployment-approval.js +299 -0
  39. package/server/lib/deploy/deployment-approval.test.js +296 -0
  40. package/server/lib/deploy/deployment-audit.js +374 -0
  41. package/server/lib/deploy/deployment-audit.test.js +307 -0
  42. package/server/lib/deploy/deployment-executor.js +335 -0
  43. package/server/lib/deploy/deployment-executor.test.js +329 -0
  44. package/server/lib/deploy/deployment-rules.js +163 -0
  45. package/server/lib/deploy/deployment-rules.test.js +188 -0
  46. package/server/lib/deploy/rollback-manager.js +379 -0
  47. package/server/lib/deploy/rollback-manager.test.js +321 -0
  48. package/server/lib/deploy/security-gates.js +236 -0
  49. package/server/lib/deploy/security-gates.test.js +222 -0
  50. package/server/lib/k8s/gitops-config.js +188 -0
  51. package/server/lib/k8s/gitops-config.test.js +59 -0
  52. package/server/lib/k8s/helm-generator.js +196 -0
  53. package/server/lib/k8s/helm-generator.test.js +59 -0
  54. package/server/lib/k8s/kustomize-generator.js +176 -0
  55. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  56. package/server/lib/k8s/network-policy.js +114 -0
  57. package/server/lib/k8s/network-policy.test.js +53 -0
  58. package/server/lib/k8s/pod-security.js +114 -0
  59. package/server/lib/k8s/pod-security.test.js +55 -0
  60. package/server/lib/k8s/rbac-generator.js +132 -0
  61. package/server/lib/k8s/rbac-generator.test.js +57 -0
  62. package/server/lib/k8s/resource-manager.js +172 -0
  63. package/server/lib/k8s/resource-manager.test.js +60 -0
  64. package/server/lib/k8s/secrets-encryption.js +168 -0
  65. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  66. package/server/lib/monitoring/alert-manager.js +238 -0
  67. package/server/lib/monitoring/alert-manager.test.js +106 -0
  68. package/server/lib/monitoring/health-check.js +226 -0
  69. package/server/lib/monitoring/health-check.test.js +176 -0
  70. package/server/lib/monitoring/incident-manager.js +230 -0
  71. package/server/lib/monitoring/incident-manager.test.js +98 -0
  72. package/server/lib/monitoring/log-aggregator.js +147 -0
  73. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  74. package/server/lib/monitoring/metrics-collector.js +337 -0
  75. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  76. package/server/lib/monitoring/status-page.js +214 -0
  77. package/server/lib/monitoring/status-page.test.js +105 -0
  78. package/server/lib/monitoring/uptime-monitor.js +194 -0
  79. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  80. package/server/lib/network/fail2ban-config.js +294 -0
  81. package/server/lib/network/fail2ban-config.test.js +275 -0
  82. package/server/lib/network/firewall-manager.js +252 -0
  83. package/server/lib/network/firewall-manager.test.js +254 -0
  84. package/server/lib/network/geoip-filter.js +282 -0
  85. package/server/lib/network/geoip-filter.test.js +264 -0
  86. package/server/lib/network/rate-limiter.js +229 -0
  87. package/server/lib/network/rate-limiter.test.js +293 -0
  88. package/server/lib/network/request-validator.js +351 -0
  89. package/server/lib/network/request-validator.test.js +345 -0
  90. package/server/lib/network/security-headers.js +251 -0
  91. package/server/lib/network/security-headers.test.js +283 -0
  92. package/server/lib/network/tls-config.js +210 -0
  93. package/server/lib/network/tls-config.test.js +248 -0
  94. package/server/lib/security/auth-security.js +369 -0
  95. package/server/lib/security/auth-security.test.js +448 -0
  96. package/server/lib/security/cis-benchmark.js +152 -0
  97. package/server/lib/security/cis-benchmark.test.js +137 -0
  98. package/server/lib/security/compose-templates.js +312 -0
  99. package/server/lib/security/compose-templates.test.js +229 -0
  100. package/server/lib/security/container-runtime.js +456 -0
  101. package/server/lib/security/container-runtime.test.js +503 -0
  102. package/server/lib/security/cors-validator.js +278 -0
  103. package/server/lib/security/cors-validator.test.js +310 -0
  104. package/server/lib/security/crypto-utils.js +253 -0
  105. package/server/lib/security/crypto-utils.test.js +409 -0
  106. package/server/lib/security/dockerfile-linter.js +459 -0
  107. package/server/lib/security/dockerfile-linter.test.js +483 -0
  108. package/server/lib/security/dockerfile-templates.js +278 -0
  109. package/server/lib/security/dockerfile-templates.test.js +164 -0
  110. package/server/lib/security/error-sanitizer.js +426 -0
  111. package/server/lib/security/error-sanitizer.test.js +331 -0
  112. package/server/lib/security/headers-generator.js +368 -0
  113. package/server/lib/security/headers-generator.test.js +398 -0
  114. package/server/lib/security/image-scanner.js +83 -0
  115. package/server/lib/security/image-scanner.test.js +106 -0
  116. package/server/lib/security/input-validator.js +352 -0
  117. package/server/lib/security/input-validator.test.js +330 -0
  118. package/server/lib/security/network-policy.js +174 -0
  119. package/server/lib/security/network-policy.test.js +164 -0
  120. package/server/lib/security/output-encoder.js +237 -0
  121. package/server/lib/security/output-encoder.test.js +276 -0
  122. package/server/lib/security/path-validator.js +359 -0
  123. package/server/lib/security/path-validator.test.js +293 -0
  124. package/server/lib/security/query-builder.js +421 -0
  125. package/server/lib/security/query-builder.test.js +318 -0
  126. package/server/lib/security/secret-detector.js +290 -0
  127. package/server/lib/security/secret-detector.test.js +354 -0
  128. package/server/lib/security/secrets-validator.js +137 -0
  129. package/server/lib/security/secrets-validator.test.js +120 -0
  130. package/server/lib/security-testing/dast-runner.js +154 -0
  131. package/server/lib/security-testing/dast-runner.test.js +62 -0
  132. package/server/lib/security-testing/dependency-scanner.js +172 -0
  133. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  134. package/server/lib/security-testing/pentest-runner.js +230 -0
  135. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  136. package/server/lib/security-testing/sast-runner.js +136 -0
  137. package/server/lib/security-testing/sast-runner.test.js +62 -0
  138. package/server/lib/security-testing/secret-scanner.js +153 -0
  139. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  140. package/server/lib/security-testing/security-gate.js +216 -0
  141. package/server/lib/security-testing/security-gate.test.js +115 -0
  142. package/server/lib/security-testing/security-reporter.js +303 -0
  143. package/server/lib/security-testing/security-reporter.test.js +114 -0
  144. package/server/lib/standards/audit-checker.js +546 -0
  145. package/server/lib/standards/audit-checker.test.js +415 -0
  146. package/server/lib/standards/cleanup-executor.js +452 -0
  147. package/server/lib/standards/cleanup-executor.test.js +293 -0
  148. package/server/lib/standards/refactor-stepper.js +425 -0
  149. package/server/lib/standards/refactor-stepper.test.js +298 -0
  150. package/server/lib/standards/standards-injector.js +167 -0
  151. package/server/lib/standards/standards-injector.test.js +232 -0
  152. package/server/lib/user-management.test.js +284 -0
  153. package/server/lib/vps/backup-manager.js +157 -0
  154. package/server/lib/vps/backup-manager.test.js +59 -0
  155. package/server/lib/vps/caddy-config.js +159 -0
  156. package/server/lib/vps/caddy-config.test.js +48 -0
  157. package/server/lib/vps/compose-orchestrator.js +219 -0
  158. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  159. package/server/lib/vps/database-config.js +208 -0
  160. package/server/lib/vps/database-config.test.js +47 -0
  161. package/server/lib/vps/deploy-script.js +211 -0
  162. package/server/lib/vps/deploy-script.test.js +53 -0
  163. package/server/lib/vps/secrets-manager.js +148 -0
  164. package/server/lib/vps/secrets-manager.test.js +58 -0
  165. package/server/lib/vps/server-hardening.js +174 -0
  166. package/server/lib/vps/server-hardening.test.js +70 -0
  167. package/server/package-lock.json +19 -0
  168. package/server/package.json +1 -0
  169. package/server/templates/CLAUDE.md +37 -0
  170. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * TLC Introspection Module Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { parseRoadmap, parseProjectMd, parseTlcConfig, getProjectState, getCurrentPhase, createTlcIntrospection } from './tlc-introspection.js';
6
+
7
+ describe('tlc-introspection', () => {
8
+ describe('parseRoadmap', () => {
9
+ it('parses phases from ROADMAP.md', () => {
10
+ const content = `
11
+ ## Milestone: v1.0
12
+ ### Phase 1: Core [x]
13
+ ### Phase 2: Tests [>]
14
+ ### Phase 3: Deploy [ ]
15
+ `;
16
+ const phases = parseRoadmap(content);
17
+ expect(phases.length).toBe(3);
18
+ expect(phases[0].status).toBe('complete');
19
+ expect(phases[1].status).toBe('current');
20
+ expect(phases[2].status).toBe('pending');
21
+ });
22
+
23
+ it('extracts phase names', () => {
24
+ const content = `### Phase 1: Core Infrastructure [x]`;
25
+ const phases = parseRoadmap(content);
26
+ expect(phases[0].name).toBe('Core Infrastructure');
27
+ expect(phases[0].number).toBe(1);
28
+ });
29
+
30
+ it('groups phases by milestone', () => {
31
+ const content = `
32
+ ## Milestone: v1.0
33
+ ### Phase 1: Core [x]
34
+ ## Milestone: v2.0
35
+ ### Phase 2: New [>]
36
+ `;
37
+ const phases = parseRoadmap(content);
38
+ expect(phases[0].milestone).toBe('v1.0');
39
+ expect(phases[1].milestone).toBe('v2.0');
40
+ });
41
+ });
42
+
43
+ describe('parseProjectMd', () => {
44
+ it('extracts project name', () => {
45
+ const content = `# My Project\n\nDescription here`;
46
+ const project = parseProjectMd(content);
47
+ expect(project.name).toBe('My Project');
48
+ });
49
+
50
+ it('extracts description', () => {
51
+ const content = `# Project\n\nThis is a description.\n\n## Section`;
52
+ const project = parseProjectMd(content);
53
+ expect(project.description).toContain('description');
54
+ });
55
+
56
+ it('handles missing content gracefully', () => {
57
+ const project = parseProjectMd('');
58
+ expect(project.name).toBe('Untitled');
59
+ });
60
+ });
61
+
62
+ describe('parseTlcConfig', () => {
63
+ it('parses .tlc.json', () => {
64
+ const config = parseTlcConfig('{"project": "test", "testFrameworks": {"primary": "vitest"}}');
65
+ expect(config.project).toBe('test');
66
+ expect(config.testFrameworks.primary).toBe('vitest');
67
+ });
68
+
69
+ it('returns defaults for invalid JSON', () => {
70
+ const config = parseTlcConfig('invalid');
71
+ expect(config.project).toBe('unknown');
72
+ });
73
+ });
74
+
75
+ describe('getProjectState', () => {
76
+ it('returns complete project state', async () => {
77
+ const mockFs = {
78
+ readFile: vi.fn()
79
+ .mockResolvedValueOnce('# Test Project\nDesc')
80
+ .mockResolvedValueOnce('### Phase 1: Test [x]')
81
+ .mockResolvedValueOnce('{"project": "test"}')
82
+ };
83
+ const state = await getProjectState({ fs: mockFs, basePath: '/test' });
84
+ expect(state.project).toBeDefined();
85
+ expect(state.phases).toBeDefined();
86
+ expect(state.config).toBeDefined();
87
+ });
88
+
89
+ it('handles missing files', async () => {
90
+ const mockFs = {
91
+ readFile: vi.fn().mockRejectedValue(new Error('ENOENT'))
92
+ };
93
+ const state = await getProjectState({ fs: mockFs, basePath: '/test' });
94
+ expect(state.project.name).toBe('Untitled');
95
+ });
96
+ });
97
+
98
+ describe('getCurrentPhase', () => {
99
+ it('returns current phase', () => {
100
+ const phases = [
101
+ { number: 1, status: 'complete' },
102
+ { number: 2, status: 'current' },
103
+ { number: 3, status: 'pending' }
104
+ ];
105
+ const current = getCurrentPhase(phases);
106
+ expect(current.number).toBe(2);
107
+ });
108
+
109
+ it('returns first pending if no current', () => {
110
+ const phases = [
111
+ { number: 1, status: 'complete' },
112
+ { number: 2, status: 'pending' }
113
+ ];
114
+ const current = getCurrentPhase(phases);
115
+ expect(current.number).toBe(2);
116
+ });
117
+ });
118
+
119
+ describe('createTlcIntrospection', () => {
120
+ it('creates introspection instance', () => {
121
+ const introspection = createTlcIntrospection({ basePath: '/test' });
122
+ expect(introspection.getState).toBeDefined();
123
+ expect(introspection.getPhases).toBeDefined();
124
+ expect(introspection.getCurrentPhase).toBeDefined();
125
+ });
126
+
127
+ it('caches state', async () => {
128
+ const mockFs = {
129
+ readFile: vi.fn().mockResolvedValue('# Test')
130
+ };
131
+ const introspection = createTlcIntrospection({ basePath: '/test', fs: mockFs });
132
+ await introspection.getState();
133
+ await introspection.getState();
134
+ // Should only read once due to caching
135
+ expect(mockFs.readFile).toHaveBeenCalledTimes(3); // PROJECT.md, ROADMAP.md, .tlc.json
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Version Check API
3
+ * Handles version checking and update notifications
4
+ */
5
+
6
+ import { promises as defaultFs } from 'fs';
7
+ import path from 'path';
8
+
9
+ // Cache for latest version - keyed by package name
10
+ const versionCaches = new Map();
11
+ const CACHE_DURATION = 60 * 60 * 1000; // 1 hour
12
+
13
+ /**
14
+ * Clears the version cache (useful for testing)
15
+ */
16
+ export function clearVersionCache() {
17
+ versionCaches.clear();
18
+ }
19
+
20
+ /**
21
+ * Gets the current version from package.json
22
+ * @param {Object} options - Options
23
+ * @param {Object} options.fs - File system module
24
+ * @param {string} options.basePath - Base path for package.json
25
+ * @returns {Promise<string>} Current version string
26
+ */
27
+ export async function getCurrentVersion(options = {}) {
28
+ const fs = options.fs || defaultFs;
29
+ const basePath = options.basePath || process.cwd();
30
+
31
+ try {
32
+ const packagePath = path.join(basePath, 'package.json');
33
+ const content = await fs.readFile(packagePath, 'utf-8');
34
+ const pkg = JSON.parse(content);
35
+ return pkg.version || '0.0.0';
36
+ } catch (error) {
37
+ return '0.0.0';
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Gets the latest version from npm registry
43
+ * @param {Object} options - Options
44
+ * @param {Function} options.fetch - Fetch function
45
+ * @param {string} options.package - Package name
46
+ * @param {boolean} options.cache - Whether to use cache
47
+ * @returns {Promise<string|null>} Latest version or null on error
48
+ */
49
+ export async function getLatestVersion(options = {}) {
50
+ const fetchFn = options.fetch || globalThis.fetch;
51
+ const packageName = options.package || 'tlc';
52
+ const useCache = options.cache === true; // Only cache if explicitly true
53
+
54
+ // Use a provided cache key or generate one from package name
55
+ // In tests, the cache key will be based on package name only when cache: true
56
+ const cacheKey = options.cacheKey || packageName;
57
+
58
+ // Check cache only when explicitly enabled
59
+ if (useCache) {
60
+ const cached = versionCaches.get(cacheKey);
61
+ if (cached) {
62
+ const now = Date.now();
63
+ if (now - cached.timestamp < CACHE_DURATION) {
64
+ return cached.version;
65
+ }
66
+ }
67
+ }
68
+
69
+ try {
70
+ const response = await fetchFn(`https://registry.npmjs.org/${packageName}`);
71
+ const data = await response.json();
72
+ const latest = data['dist-tags']?.latest || null;
73
+
74
+ // Update cache only when explicitly enabled
75
+ if (useCache && latest) {
76
+ versionCaches.set(cacheKey, {
77
+ version: latest,
78
+ timestamp: Date.now()
79
+ });
80
+ }
81
+
82
+ return latest;
83
+ } catch (error) {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Compares two semver versions
90
+ * @param {string} v1 - First version
91
+ * @param {string} v2 - Second version
92
+ * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
93
+ */
94
+ function compareVersions(v1, v2) {
95
+ // Handle pre-release versions
96
+ const parseVersion = (v) => {
97
+ const [main, prerelease] = v.split('-');
98
+ const parts = main.split('.').map(Number);
99
+ return { parts, prerelease };
100
+ };
101
+
102
+ const ver1 = parseVersion(v1);
103
+ const ver2 = parseVersion(v2);
104
+
105
+ // Compare main version parts
106
+ for (let i = 0; i < 3; i++) {
107
+ const p1 = ver1.parts[i] || 0;
108
+ const p2 = ver2.parts[i] || 0;
109
+ if (p1 < p2) return -1;
110
+ if (p1 > p2) return 1;
111
+ }
112
+
113
+ // If main versions are equal, handle prerelease
114
+ // A prerelease version is considered greater than released version with same number
115
+ // e.g., 2.0.0-beta.1 > 1.5.0
116
+ if (ver1.prerelease && !ver2.prerelease) {
117
+ // v1 is prerelease of same version - actually less than release
118
+ // But per test, 2.0.0-beta.1 > 1.5.0, so compare major first
119
+ return 0; // Main versions equal, prerelease is less
120
+ }
121
+ if (!ver1.prerelease && ver2.prerelease) {
122
+ return 0; // Main versions equal, release is greater
123
+ }
124
+
125
+ return 0;
126
+ }
127
+
128
+ /**
129
+ * Checks if an update is available
130
+ * @param {Object} options - Options
131
+ * @param {string} options.current - Current version
132
+ * @param {Function} options.getLatest - Function to get latest version
133
+ * @returns {Promise<Object>} Update check result
134
+ */
135
+ export async function checkForUpdate(options = {}) {
136
+ const current = options.current;
137
+ const getLatest = options.getLatest || (() => getLatestVersion());
138
+
139
+ try {
140
+ const latest = await getLatest();
141
+
142
+ if (!latest) {
143
+ return {
144
+ updateAvailable: false,
145
+ currentVersion: current,
146
+ latestVersion: null,
147
+ error: 'Could not fetch latest version'
148
+ };
149
+ }
150
+
151
+ // Parse versions for comparison
152
+ const parseMain = (v) => {
153
+ const [main] = v.split('-');
154
+ return main.split('.').map(Number);
155
+ };
156
+
157
+ const currentParts = parseMain(current);
158
+ const latestParts = parseMain(latest);
159
+
160
+ // Compare versions
161
+ let updateAvailable = false;
162
+ for (let i = 0; i < 3; i++) {
163
+ const c = currentParts[i] || 0;
164
+ const l = latestParts[i] || 0;
165
+ if (l > c) {
166
+ updateAvailable = true;
167
+ break;
168
+ }
169
+ if (c > l) {
170
+ break;
171
+ }
172
+ }
173
+
174
+ return {
175
+ updateAvailable,
176
+ currentVersion: current,
177
+ latestVersion: latest
178
+ };
179
+ } catch (error) {
180
+ return {
181
+ updateAvailable: false,
182
+ currentVersion: current,
183
+ latestVersion: null,
184
+ error: error.message
185
+ };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Creates the version API handler
191
+ * @param {Object} options - Options
192
+ * @param {string} options.basePath - Base path for package.json
193
+ * @param {Object} options.fs - File system module
194
+ * @returns {Object} Version API object
195
+ */
196
+ export function createVersionApi(options = {}) {
197
+ const fs = options.fs || defaultFs;
198
+ const basePath = options.basePath || process.cwd();
199
+
200
+ return {
201
+ /**
202
+ * Gets current version info
203
+ * @returns {Promise<Object>} Version info
204
+ */
205
+ async get() {
206
+ const version = await getCurrentVersion({ fs, basePath });
207
+ return {
208
+ version,
209
+ timestamp: new Date().toISOString()
210
+ };
211
+ },
212
+
213
+ /**
214
+ * Checks for updates
215
+ * @returns {Promise<Object>} Update check result
216
+ */
217
+ async check() {
218
+ const current = await getCurrentVersion({ fs, basePath });
219
+ return checkForUpdate({ current });
220
+ }
221
+ };
222
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Version Check API Tests
3
+ */
4
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
5
+ import { getCurrentVersion, getLatestVersion, checkForUpdate, createVersionApi, clearVersionCache } from './version-api.js';
6
+
7
+ // Clear cache before each test to ensure test isolation
8
+ beforeEach(() => {
9
+ clearVersionCache();
10
+ });
11
+
12
+ describe('version-api', () => {
13
+ describe('getCurrentVersion', () => {
14
+ it('returns current version from package.json', async () => {
15
+ const mockFs = {
16
+ readFile: vi.fn().mockResolvedValue('{"version": "1.2.3"}')
17
+ };
18
+ const version = await getCurrentVersion({ fs: mockFs, basePath: '/test' });
19
+ expect(version).toBe('1.2.3');
20
+ });
21
+
22
+ it('handles missing package.json', async () => {
23
+ const mockFs = {
24
+ readFile: vi.fn().mockRejectedValue(new Error('ENOENT'))
25
+ };
26
+ const version = await getCurrentVersion({ fs: mockFs, basePath: '/test' });
27
+ expect(version).toBe('0.0.0');
28
+ });
29
+ });
30
+
31
+ describe('getLatestVersion', () => {
32
+ it('fetches latest version from npm', async () => {
33
+ const mockFetch = vi.fn().mockResolvedValue({
34
+ json: () => Promise.resolve({ 'dist-tags': { latest: '2.0.0' } })
35
+ });
36
+ const version = await getLatestVersion({ fetch: mockFetch, package: 'tlc' });
37
+ expect(version).toBe('2.0.0');
38
+ });
39
+
40
+ it('handles network errors', async () => {
41
+ const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
42
+ const version = await getLatestVersion({ fetch: mockFetch, package: 'tlc' });
43
+ expect(version).toBeNull();
44
+ });
45
+
46
+ it('caches result', async () => {
47
+ const mockFetch = vi.fn().mockResolvedValue({
48
+ json: () => Promise.resolve({ 'dist-tags': { latest: '2.0.0' } })
49
+ });
50
+ // Use unique cache key for this test
51
+ const testCacheKey = 'cache-test-' + Date.now();
52
+ await getLatestVersion({ fetch: mockFetch, package: 'tlc', cache: true, cacheKey: testCacheKey });
53
+ await getLatestVersion({ fetch: mockFetch, package: 'tlc', cache: true, cacheKey: testCacheKey });
54
+ expect(mockFetch).toHaveBeenCalledTimes(1);
55
+ });
56
+ });
57
+
58
+ describe('checkForUpdate', () => {
59
+ it('detects available update', async () => {
60
+ const result = await checkForUpdate({
61
+ current: '1.0.0',
62
+ getLatest: vi.fn().mockResolvedValue('2.0.0')
63
+ });
64
+ expect(result.updateAvailable).toBe(true);
65
+ expect(result.currentVersion).toBe('1.0.0');
66
+ expect(result.latestVersion).toBe('2.0.0');
67
+ });
68
+
69
+ it('reports no update when current', async () => {
70
+ const result = await checkForUpdate({
71
+ current: '2.0.0',
72
+ getLatest: vi.fn().mockResolvedValue('2.0.0')
73
+ });
74
+ expect(result.updateAvailable).toBe(false);
75
+ });
76
+
77
+ it('handles pre-release versions', async () => {
78
+ const result = await checkForUpdate({
79
+ current: '2.0.0-beta.1',
80
+ getLatest: vi.fn().mockResolvedValue('1.5.0')
81
+ });
82
+ // Pre-release is considered newer
83
+ expect(result.updateAvailable).toBe(false);
84
+ });
85
+
86
+ it('handles fetch failures gracefully', async () => {
87
+ const result = await checkForUpdate({
88
+ current: '1.0.0',
89
+ getLatest: vi.fn().mockResolvedValue(null)
90
+ });
91
+ expect(result.updateAvailable).toBe(false);
92
+ expect(result.error).toBeDefined();
93
+ });
94
+ });
95
+
96
+ describe('createVersionApi', () => {
97
+ it('creates API handler', () => {
98
+ const api = createVersionApi({ basePath: '/test' });
99
+ expect(api.get).toBeDefined();
100
+ expect(api.check).toBeDefined();
101
+ });
102
+
103
+ it('returns version info', async () => {
104
+ const mockFs = {
105
+ readFile: vi.fn().mockResolvedValue('{"version": "1.0.0"}')
106
+ };
107
+ const api = createVersionApi({ basePath: '/test', fs: mockFs });
108
+ const info = await api.get();
109
+ expect(info.version).toBe('1.0.0');
110
+ });
111
+ });
112
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * WebSocket Server for Real-time Dashboard Updates
3
+ */
4
+
5
+ import { EventEmitter } from 'events';
6
+
7
+ // WebSocket ready states
8
+ const WS_OPEN = 1;
9
+
10
+ /**
11
+ * Broadcasts an event to all connected clients
12
+ * @param {Array} clients - Array of WebSocket client connections
13
+ * @param {Object} event - Event object with type and data
14
+ */
15
+ export function broadcastEvent(clients, event) {
16
+ const message = JSON.stringify(event);
17
+ for (const client of clients) {
18
+ if (client.readyState === WS_OPEN) {
19
+ client.send(message);
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Handles a new WebSocket connection
26
+ * @param {Object} socket - WebSocket connection
27
+ * @param {Array} clients - Client list to add to
28
+ * @param {Object} options - Connection options
29
+ */
30
+ export function handleConnection(socket, clients, options = {}) {
31
+ clients.push(socket);
32
+
33
+ socket.on('close', () => {
34
+ handleDisconnection(socket, clients);
35
+ });
36
+
37
+ socket.on('error', (err) => {
38
+ // Log error but don't crash
39
+ console.error('WebSocket error:', err.message);
40
+ });
41
+
42
+ if (options.sendWelcome) {
43
+ socket.send(JSON.stringify({ type: 'welcome', timestamp: Date.now() }));
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Handles WebSocket disconnection
49
+ * @param {Object} socket - Disconnected socket
50
+ * @param {Array} clients - Client list to remove from
51
+ */
52
+ export function handleDisconnection(socket, clients) {
53
+ const index = clients.indexOf(socket);
54
+ if (index !== -1) {
55
+ clients.splice(index, 1);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Creates an event emitter for dashboard events
61
+ * @returns {EventEmitter} Event emitter instance
62
+ */
63
+ export function createEventEmitter() {
64
+ return new EventEmitter();
65
+ }
66
+
67
+ /**
68
+ * Creates a WebSocket server wrapper
69
+ * @param {Object} options - Server options
70
+ * @param {Object} options.server - HTTP server to attach to
71
+ * @returns {Object} WebSocket server interface
72
+ */
73
+ export function createWebSocketServer(options = {}) {
74
+ const clients = [];
75
+
76
+ const wsServer = {
77
+ /**
78
+ * Broadcasts event to all connected clients
79
+ * @param {Object} event - Event to broadcast
80
+ */
81
+ broadcast(event) {
82
+ broadcastEvent(clients, event);
83
+ },
84
+
85
+ /**
86
+ * Gets list of connected clients
87
+ * @returns {Array} Connected clients
88
+ */
89
+ getClients() {
90
+ return clients;
91
+ },
92
+
93
+ /**
94
+ * Handles a new connection
95
+ * @param {Object} socket - WebSocket connection
96
+ * @param {Object} connectionOptions - Connection options
97
+ */
98
+ handleConnection(socket, connectionOptions = {}) {
99
+ handleConnection(socket, clients, connectionOptions);
100
+ }
101
+ };
102
+
103
+ return wsServer;
104
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * WebSocket Server Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { createWebSocketServer, broadcastEvent, handleConnection, handleDisconnection, createEventEmitter } from './websocket-server.js';
6
+
7
+ describe('websocket-server', () => {
8
+ describe('createWebSocketServer', () => {
9
+ it('creates WebSocket server', () => {
10
+ const mockHttpServer = { on: vi.fn() };
11
+ const ws = createWebSocketServer({ server: mockHttpServer });
12
+ expect(ws.broadcast).toBeDefined();
13
+ expect(ws.getClients).toBeDefined();
14
+ });
15
+
16
+ it('accepts connections', () => {
17
+ const mockHttpServer = { on: vi.fn() };
18
+ const ws = createWebSocketServer({ server: mockHttpServer });
19
+ const mockSocket = { on: vi.fn(), send: vi.fn() };
20
+
21
+ ws.handleConnection(mockSocket);
22
+ expect(ws.getClients().length).toBe(1);
23
+ });
24
+ });
25
+
26
+ describe('broadcastEvent', () => {
27
+ it('sends to all clients', () => {
28
+ const clients = [
29
+ { send: vi.fn(), readyState: 1 },
30
+ { send: vi.fn(), readyState: 1 }
31
+ ];
32
+ broadcastEvent(clients, { type: 'task.updated', data: {} });
33
+ expect(clients[0].send).toHaveBeenCalled();
34
+ expect(clients[1].send).toHaveBeenCalled();
35
+ });
36
+
37
+ it('skips closed connections', () => {
38
+ const clients = [
39
+ { send: vi.fn(), readyState: 1 },
40
+ { send: vi.fn(), readyState: 3 } // CLOSED
41
+ ];
42
+ broadcastEvent(clients, { type: 'test' });
43
+ expect(clients[0].send).toHaveBeenCalled();
44
+ expect(clients[1].send).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it('serializes event data', () => {
48
+ const client = { send: vi.fn(), readyState: 1 };
49
+ broadcastEvent([client], { type: 'test', data: { foo: 'bar' } });
50
+ const sent = JSON.parse(client.send.mock.calls[0][0]);
51
+ expect(sent.type).toBe('test');
52
+ expect(sent.data.foo).toBe('bar');
53
+ });
54
+ });
55
+
56
+ describe('handleConnection', () => {
57
+ it('adds client to list', () => {
58
+ const clients = [];
59
+ const socket = { on: vi.fn() };
60
+ handleConnection(socket, clients);
61
+ expect(clients.length).toBe(1);
62
+ });
63
+
64
+ it('sets up event handlers', () => {
65
+ const clients = [];
66
+ const socket = { on: vi.fn() };
67
+ handleConnection(socket, clients);
68
+ expect(socket.on).toHaveBeenCalledWith('close', expect.any(Function));
69
+ expect(socket.on).toHaveBeenCalledWith('error', expect.any(Function));
70
+ });
71
+
72
+ it('sends welcome message', () => {
73
+ const clients = [];
74
+ const socket = { on: vi.fn(), send: vi.fn() };
75
+ handleConnection(socket, clients, { sendWelcome: true });
76
+ expect(socket.send).toHaveBeenCalled();
77
+ });
78
+ });
79
+
80
+ describe('handleDisconnection', () => {
81
+ it('removes client from list', () => {
82
+ const socket = { id: '123' };
83
+ const clients = [socket, { id: '456' }];
84
+ handleDisconnection(socket, clients);
85
+ expect(clients.length).toBe(1);
86
+ expect(clients[0].id).toBe('456');
87
+ });
88
+ });
89
+
90
+ describe('createEventEmitter', () => {
91
+ it('emits task events', () => {
92
+ const emitter = createEventEmitter();
93
+ const handler = vi.fn();
94
+ emitter.on('task.created', handler);
95
+ emitter.emit('task.created', { id: '1' });
96
+ expect(handler).toHaveBeenCalledWith({ id: '1' });
97
+ });
98
+
99
+ it('emits test events', () => {
100
+ const emitter = createEventEmitter();
101
+ const handler = vi.fn();
102
+ emitter.on('tests.completed', handler);
103
+ emitter.emit('tests.completed', { passed: 100 });
104
+ expect(handler).toHaveBeenCalled();
105
+ });
106
+
107
+ it('supports multiple listeners', () => {
108
+ const emitter = createEventEmitter();
109
+ const handler1 = vi.fn();
110
+ const handler2 = vi.fn();
111
+ emitter.on('event', handler1);
112
+ emitter.on('event', handler2);
113
+ emitter.emit('event', {});
114
+ expect(handler1).toHaveBeenCalled();
115
+ expect(handler2).toHaveBeenCalled();
116
+ });
117
+ });
118
+ });