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.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Deployment Executor Tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import {
6
+ createDeployment,
7
+ executeDeployment,
8
+ runHealthChecks,
9
+ switchTraffic,
10
+ cleanupOldDeployment,
11
+ DEPLOYMENT_STATES,
12
+ DEPLOYMENT_STRATEGIES,
13
+ createDeploymentExecutor,
14
+ } from './deployment-executor.js';
15
+
16
+ describe('deployment-executor', () => {
17
+ describe('DEPLOYMENT_STATES', () => {
18
+ it('defines all state constants', () => {
19
+ expect(DEPLOYMENT_STATES.PENDING).toBe('pending');
20
+ expect(DEPLOYMENT_STATES.BUILDING).toBe('building');
21
+ expect(DEPLOYMENT_STATES.DEPLOYING).toBe('deploying');
22
+ expect(DEPLOYMENT_STATES.HEALTH_CHECK).toBe('health_check');
23
+ expect(DEPLOYMENT_STATES.SWITCHING).toBe('switching');
24
+ expect(DEPLOYMENT_STATES.COMPLETED).toBe('completed');
25
+ expect(DEPLOYMENT_STATES.FAILED).toBe('failed');
26
+ expect(DEPLOYMENT_STATES.ROLLED_BACK).toBe('rolled_back');
27
+ });
28
+ });
29
+
30
+ describe('DEPLOYMENT_STRATEGIES', () => {
31
+ it('defines all strategy constants', () => {
32
+ expect(DEPLOYMENT_STRATEGIES.ROLLING).toBe('rolling');
33
+ expect(DEPLOYMENT_STRATEGIES.BLUE_GREEN).toBe('blue-green');
34
+ expect(DEPLOYMENT_STRATEGIES.CANARY).toBe('canary');
35
+ expect(DEPLOYMENT_STRATEGIES.RECREATE).toBe('recreate');
36
+ });
37
+ });
38
+
39
+ describe('createDeployment', () => {
40
+ it('creates deployment with required fields', () => {
41
+ const deployment = createDeployment({
42
+ branch: 'main',
43
+ commitSha: 'abc123',
44
+ strategy: 'blue-green',
45
+ });
46
+
47
+ expect(deployment.id).toBeDefined();
48
+ expect(deployment.branch).toBe('main');
49
+ expect(deployment.commitSha).toBe('abc123');
50
+ expect(deployment.strategy).toBe('blue-green');
51
+ expect(deployment.state).toBe('pending');
52
+ expect(deployment.createdAt).toBeDefined();
53
+ });
54
+
55
+ it('defaults to rolling strategy', () => {
56
+ const deployment = createDeployment({
57
+ branch: 'feature/x',
58
+ commitSha: 'abc123',
59
+ });
60
+ expect(deployment.strategy).toBe('rolling');
61
+ });
62
+
63
+ it('tracks state transitions', () => {
64
+ const deployment = createDeployment({
65
+ branch: 'main',
66
+ commitSha: 'abc123',
67
+ });
68
+ expect(deployment.stateHistory).toEqual([
69
+ expect.objectContaining({ state: 'pending' }),
70
+ ]);
71
+ });
72
+ });
73
+
74
+ describe('executeDeployment', () => {
75
+ it('executes rolling deployment', async () => {
76
+ const mockBuild = vi.fn().mockResolvedValue({ success: true });
77
+ const mockDeploy = vi.fn().mockResolvedValue({ success: true });
78
+ const mockHealth = vi.fn().mockResolvedValue({ healthy: true });
79
+
80
+ const deployment = createDeployment({
81
+ branch: 'feature/x',
82
+ commitSha: 'abc123',
83
+ strategy: 'rolling',
84
+ });
85
+
86
+ const result = await executeDeployment(deployment, {
87
+ build: mockBuild,
88
+ deploy: mockDeploy,
89
+ healthCheck: mockHealth,
90
+ });
91
+
92
+ expect(result.state).toBe('completed');
93
+ expect(mockBuild).toHaveBeenCalled();
94
+ expect(mockDeploy).toHaveBeenCalled();
95
+ expect(mockHealth).toHaveBeenCalled();
96
+ });
97
+
98
+ it('executes blue-green deployment', async () => {
99
+ const mockBuild = vi.fn().mockResolvedValue({ success: true });
100
+ const mockDeploy = vi.fn().mockResolvedValue({ success: true, slot: 'green' });
101
+ const mockHealth = vi.fn().mockResolvedValue({ healthy: true });
102
+ const mockSwitch = vi.fn().mockResolvedValue({ success: true });
103
+
104
+ const deployment = createDeployment({
105
+ branch: 'main',
106
+ commitSha: 'abc123',
107
+ strategy: 'blue-green',
108
+ });
109
+
110
+ const result = await executeDeployment(deployment, {
111
+ build: mockBuild,
112
+ deploy: mockDeploy,
113
+ healthCheck: mockHealth,
114
+ switchTraffic: mockSwitch,
115
+ });
116
+
117
+ expect(result.state).toBe('completed');
118
+ expect(mockSwitch).toHaveBeenCalled();
119
+ });
120
+
121
+ it('fails on build error', async () => {
122
+ const mockBuild = vi.fn().mockResolvedValue({ success: false, error: 'Build failed' });
123
+
124
+ const deployment = createDeployment({
125
+ branch: 'main',
126
+ commitSha: 'abc123',
127
+ });
128
+
129
+ const result = await executeDeployment(deployment, {
130
+ build: mockBuild,
131
+ });
132
+
133
+ expect(result.state).toBe('failed');
134
+ expect(result.error).toContain('Build failed');
135
+ });
136
+
137
+ it('fails on health check failure', async () => {
138
+ const mockBuild = vi.fn().mockResolvedValue({ success: true });
139
+ const mockDeploy = vi.fn().mockResolvedValue({ success: true });
140
+ const mockHealth = vi.fn().mockResolvedValue({ healthy: false, reason: 'Timeout' });
141
+
142
+ const deployment = createDeployment({
143
+ branch: 'main',
144
+ commitSha: 'abc123',
145
+ });
146
+
147
+ const result = await executeDeployment(deployment, {
148
+ build: mockBuild,
149
+ deploy: mockDeploy,
150
+ healthCheck: mockHealth,
151
+ });
152
+
153
+ expect(result.state).toBe('failed');
154
+ });
155
+
156
+ it('emits state change events', async () => {
157
+ const mockBuild = vi.fn().mockResolvedValue({ success: true });
158
+ const mockDeploy = vi.fn().mockResolvedValue({ success: true });
159
+ const mockHealth = vi.fn().mockResolvedValue({ healthy: true });
160
+ const onStateChange = vi.fn();
161
+
162
+ const deployment = createDeployment({
163
+ branch: 'main',
164
+ commitSha: 'abc123',
165
+ });
166
+
167
+ await executeDeployment(deployment, {
168
+ build: mockBuild,
169
+ deploy: mockDeploy,
170
+ healthCheck: mockHealth,
171
+ onStateChange,
172
+ });
173
+
174
+ expect(onStateChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'building' }));
175
+ expect(onStateChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'deploying' }));
176
+ expect(onStateChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'completed' }));
177
+ });
178
+ });
179
+
180
+ describe('runHealthChecks', () => {
181
+ it('runs multiple health check endpoints', async () => {
182
+ const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
183
+
184
+ const result = await runHealthChecks({
185
+ endpoints: ['http://localhost:3000/health', 'http://localhost:3000/ready'],
186
+ fetch: mockFetch,
187
+ });
188
+
189
+ expect(result.healthy).toBe(true);
190
+ expect(mockFetch).toHaveBeenCalledTimes(2);
191
+ });
192
+
193
+ it('fails if any endpoint unhealthy', async () => {
194
+ const mockFetch = vi.fn()
195
+ .mockResolvedValueOnce({ ok: true, status: 200 })
196
+ .mockResolvedValueOnce({ ok: false, status: 503 });
197
+
198
+ const result = await runHealthChecks({
199
+ endpoints: ['http://localhost:3000/health', 'http://localhost:3000/ready'],
200
+ fetch: mockFetch,
201
+ });
202
+
203
+ expect(result.healthy).toBe(false);
204
+ });
205
+
206
+ it('retries failed checks', async () => {
207
+ const mockFetch = vi.fn()
208
+ .mockResolvedValueOnce({ ok: false, status: 503 })
209
+ .mockResolvedValueOnce({ ok: true, status: 200 });
210
+
211
+ const result = await runHealthChecks({
212
+ endpoints: ['http://localhost:3000/health'],
213
+ fetch: mockFetch,
214
+ retries: 2,
215
+ retryDelay: 10,
216
+ });
217
+
218
+ expect(result.healthy).toBe(true);
219
+ expect(mockFetch).toHaveBeenCalledTimes(2);
220
+ });
221
+
222
+ it('times out slow health checks', async () => {
223
+ const mockFetch = vi.fn().mockImplementation(() =>
224
+ new Promise((resolve) => setTimeout(() => resolve({ ok: true }), 1000))
225
+ );
226
+
227
+ const result = await runHealthChecks({
228
+ endpoints: ['http://localhost:3000/health'],
229
+ fetch: mockFetch,
230
+ timeout: 50,
231
+ });
232
+
233
+ expect(result.healthy).toBe(false);
234
+ expect(result.reason).toContain('timeout');
235
+ });
236
+ });
237
+
238
+ describe('switchTraffic', () => {
239
+ it('switches traffic to new slot', async () => {
240
+ const mockSwitch = vi.fn().mockResolvedValue({ success: true });
241
+
242
+ const result = await switchTraffic({
243
+ fromSlot: 'blue',
244
+ toSlot: 'green',
245
+ switchFn: mockSwitch,
246
+ });
247
+
248
+ expect(result.success).toBe(true);
249
+ expect(mockSwitch).toHaveBeenCalledWith('blue', 'green');
250
+ });
251
+
252
+ it('handles switch failure', async () => {
253
+ const mockSwitch = vi.fn().mockRejectedValue(new Error('Switch failed'));
254
+
255
+ const result = await switchTraffic({
256
+ fromSlot: 'blue',
257
+ toSlot: 'green',
258
+ switchFn: mockSwitch,
259
+ });
260
+
261
+ expect(result.success).toBe(false);
262
+ expect(result.error).toContain('Switch failed');
263
+ });
264
+ });
265
+
266
+ describe('cleanupOldDeployment', () => {
267
+ it('removes old deployment resources', async () => {
268
+ const mockCleanup = vi.fn().mockResolvedValue({ success: true });
269
+
270
+ const result = await cleanupOldDeployment({
271
+ slot: 'blue',
272
+ cleanupFn: mockCleanup,
273
+ });
274
+
275
+ expect(result.success).toBe(true);
276
+ expect(mockCleanup).toHaveBeenCalledWith('blue');
277
+ });
278
+
279
+ it('handles cleanup failure gracefully', async () => {
280
+ const mockCleanup = vi.fn().mockRejectedValue(new Error('Cleanup failed'));
281
+
282
+ const result = await cleanupOldDeployment({
283
+ slot: 'blue',
284
+ cleanupFn: mockCleanup,
285
+ });
286
+
287
+ // Should not throw, just log warning
288
+ expect(result.success).toBe(false);
289
+ });
290
+ });
291
+
292
+ describe('createDeploymentExecutor', () => {
293
+ it('creates executor with config', () => {
294
+ const executor = createDeploymentExecutor();
295
+ expect(executor.execute).toBeDefined();
296
+ expect(executor.getDeployment).toBeDefined();
297
+ expect(executor.cancel).toBeDefined();
298
+ });
299
+
300
+ it('tracks active deployments', async () => {
301
+ const executor = createDeploymentExecutor({
302
+ build: vi.fn().mockResolvedValue({ success: true }),
303
+ deploy: vi.fn().mockResolvedValue({ success: true }),
304
+ healthCheck: vi.fn().mockResolvedValue({ healthy: true }),
305
+ });
306
+
307
+ const deployment = await executor.start({
308
+ branch: 'main',
309
+ commitSha: 'abc123',
310
+ });
311
+
312
+ expect(executor.getDeployment(deployment.id)).toBeDefined();
313
+ });
314
+
315
+ it('lists all deployments', async () => {
316
+ const executor = createDeploymentExecutor({
317
+ build: vi.fn().mockResolvedValue({ success: true }),
318
+ deploy: vi.fn().mockResolvedValue({ success: true }),
319
+ healthCheck: vi.fn().mockResolvedValue({ healthy: true }),
320
+ });
321
+
322
+ await executor.start({ branch: 'main', commitSha: 'abc123' });
323
+ await executor.start({ branch: 'dev', commitSha: 'def456' });
324
+
325
+ const deployments = executor.list();
326
+ expect(deployments).toHaveLength(2);
327
+ });
328
+ });
329
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Deployment Rules
3
+ *
4
+ * Defines and manages deployment rules for different tiers (feature, dev, stable).
5
+ */
6
+
7
+ /**
8
+ * Default deployment rules for each tier
9
+ */
10
+ export const DEFAULT_RULES = {
11
+ feature: {
12
+ autoDeploy: true,
13
+ securityGates: ['sast', 'dependencies'],
14
+ deploymentStrategy: 'rolling',
15
+ requiresApproval: false,
16
+ requires2FA: false,
17
+ },
18
+ dev: {
19
+ autoDeploy: true,
20
+ securityGates: ['sast', 'dast', 'container', 'dependencies'],
21
+ deploymentStrategy: 'rolling',
22
+ requiresApproval: false,
23
+ requires2FA: false,
24
+ },
25
+ stable: {
26
+ autoDeploy: false,
27
+ securityGates: ['sast', 'dast', 'container', 'dependencies', 'pentest'],
28
+ deploymentStrategy: 'blue-green',
29
+ requiresApproval: true,
30
+ requires2FA: true,
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Valid deployment strategies
36
+ */
37
+ const VALID_STRATEGIES = ['rolling', 'blue-green', 'canary', 'recreate'];
38
+
39
+ /**
40
+ * Load deployment rules from config object
41
+ * @param {object} config - Configuration object with optional deployment.rules
42
+ * @returns {object} Merged rules with defaults
43
+ */
44
+ export function loadDeploymentRules(config) {
45
+ if (!config || !config.deployment || !config.deployment.rules) {
46
+ return DEFAULT_RULES;
47
+ }
48
+ return mergeWithDefaults(config.deployment.rules);
49
+ }
50
+
51
+ /**
52
+ * Validate rules schema
53
+ * @param {object} rules - Rules object to validate
54
+ * @returns {{ valid: boolean, errors: string[] }}
55
+ */
56
+ export function validateRules(rules) {
57
+ const errors = [];
58
+
59
+ for (const [tier, tierRules] of Object.entries(rules)) {
60
+ if (tierRules.autoDeploy !== undefined && typeof tierRules.autoDeploy !== 'boolean') {
61
+ errors.push(`${tier}.autoDeploy must be a boolean`);
62
+ }
63
+
64
+ if (tierRules.securityGates !== undefined && !Array.isArray(tierRules.securityGates)) {
65
+ errors.push(`${tier}.securityGates must be an array`);
66
+ }
67
+
68
+ if (
69
+ tierRules.deploymentStrategy !== undefined &&
70
+ !VALID_STRATEGIES.includes(tierRules.deploymentStrategy)
71
+ ) {
72
+ errors.push(
73
+ `${tier}.deploymentStrategy must be one of: ${VALID_STRATEGIES.join(', ')}`
74
+ );
75
+ }
76
+
77
+ if (tierRules.requiresApproval !== undefined && typeof tierRules.requiresApproval !== 'boolean') {
78
+ errors.push(`${tier}.requiresApproval must be a boolean`);
79
+ }
80
+
81
+ if (tierRules.requires2FA !== undefined && typeof tierRules.requires2FA !== 'boolean') {
82
+ errors.push(`${tier}.requires2FA must be a boolean`);
83
+ }
84
+ }
85
+
86
+ return {
87
+ valid: errors.length === 0,
88
+ errors,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Get rules for a specific tier
94
+ * @param {string} tier - Tier name (feature, dev, stable)
95
+ * @param {object} customRules - Optional custom rules to use instead of defaults
96
+ * @returns {object} Rules for the specified tier
97
+ */
98
+ export function getRulesForTier(tier, customRules = null) {
99
+ const rules = customRules ? mergeWithDefaults(customRules) : DEFAULT_RULES;
100
+ return rules[tier] || DEFAULT_RULES.feature;
101
+ }
102
+
103
+ /**
104
+ * Merge custom rules with defaults
105
+ * @param {object} custom - Custom rules to merge
106
+ * @returns {object} Merged rules
107
+ */
108
+ export function mergeWithDefaults(custom) {
109
+ if (!custom || Object.keys(custom).length === 0) {
110
+ return DEFAULT_RULES;
111
+ }
112
+
113
+ const merged = {};
114
+
115
+ for (const tier of Object.keys(DEFAULT_RULES)) {
116
+ if (custom[tier]) {
117
+ merged[tier] = {
118
+ ...DEFAULT_RULES[tier],
119
+ ...custom[tier],
120
+ };
121
+ } else {
122
+ merged[tier] = { ...DEFAULT_RULES[tier] };
123
+ }
124
+ }
125
+
126
+ return merged;
127
+ }
128
+
129
+ /**
130
+ * Factory function to create a deployment rules manager
131
+ * @param {object} config - Optional configuration object
132
+ * @returns {{ getRules: Function, validate: Function, getForTier: Function }}
133
+ */
134
+ export function createDeploymentRules(config = null) {
135
+ const rules = loadDeploymentRules(config);
136
+
137
+ return {
138
+ /**
139
+ * Get all rules
140
+ * @returns {object} All deployment rules
141
+ */
142
+ getRules() {
143
+ return rules;
144
+ },
145
+
146
+ /**
147
+ * Validate the current rules
148
+ * @returns {{ valid: boolean, errors: string[] }}
149
+ */
150
+ validate() {
151
+ return validateRules(rules);
152
+ },
153
+
154
+ /**
155
+ * Get rules for a specific tier
156
+ * @param {string} tier - Tier name
157
+ * @returns {object} Rules for the tier
158
+ */
159
+ getForTier(tier) {
160
+ return rules[tier] || DEFAULT_RULES.feature;
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Deployment Rules Tests
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ loadDeploymentRules,
7
+ validateRules,
8
+ getRulesForTier,
9
+ mergeWithDefaults,
10
+ DEFAULT_RULES,
11
+ createDeploymentRules,
12
+ } from './deployment-rules.js';
13
+
14
+ describe('deployment-rules', () => {
15
+ describe('DEFAULT_RULES', () => {
16
+ it('defines rules for all tiers', () => {
17
+ expect(DEFAULT_RULES.feature).toBeDefined();
18
+ expect(DEFAULT_RULES.dev).toBeDefined();
19
+ expect(DEFAULT_RULES.stable).toBeDefined();
20
+ });
21
+
22
+ it('feature tier auto-deploys with basic checks', () => {
23
+ expect(DEFAULT_RULES.feature.autoDeploy).toBe(true);
24
+ expect(DEFAULT_RULES.feature.securityGates).toContain('sast');
25
+ expect(DEFAULT_RULES.feature.securityGates).toContain('dependencies');
26
+ });
27
+
28
+ it('dev tier auto-deploys with full checks', () => {
29
+ expect(DEFAULT_RULES.dev.autoDeploy).toBe(true);
30
+ expect(DEFAULT_RULES.dev.securityGates).toContain('sast');
31
+ expect(DEFAULT_RULES.dev.securityGates).toContain('dast');
32
+ expect(DEFAULT_RULES.dev.securityGates).toContain('container');
33
+ });
34
+
35
+ it('stable tier requires approval', () => {
36
+ expect(DEFAULT_RULES.stable.autoDeploy).toBe(false);
37
+ expect(DEFAULT_RULES.stable.requiresApproval).toBe(true);
38
+ expect(DEFAULT_RULES.stable.requires2FA).toBe(true);
39
+ });
40
+ });
41
+
42
+ describe('loadDeploymentRules', () => {
43
+ it('loads rules from config object', () => {
44
+ const config = {
45
+ deployment: {
46
+ rules: {
47
+ feature: { autoDeploy: false },
48
+ },
49
+ },
50
+ };
51
+ const rules = loadDeploymentRules(config);
52
+ expect(rules.feature.autoDeploy).toBe(false);
53
+ });
54
+
55
+ it('returns defaults when no config', () => {
56
+ const rules = loadDeploymentRules({});
57
+ expect(rules).toEqual(DEFAULT_RULES);
58
+ });
59
+
60
+ it('returns defaults for null config', () => {
61
+ const rules = loadDeploymentRules(null);
62
+ expect(rules).toEqual(DEFAULT_RULES);
63
+ });
64
+ });
65
+
66
+ describe('validateRules', () => {
67
+ it('validates correct rule structure', () => {
68
+ const rules = {
69
+ feature: {
70
+ autoDeploy: true,
71
+ securityGates: ['sast'],
72
+ deploymentStrategy: 'rolling',
73
+ },
74
+ };
75
+ const result = validateRules(rules);
76
+ expect(result.valid).toBe(true);
77
+ expect(result.errors).toHaveLength(0);
78
+ });
79
+
80
+ it('rejects invalid autoDeploy type', () => {
81
+ const rules = {
82
+ feature: { autoDeploy: 'yes' },
83
+ };
84
+ const result = validateRules(rules);
85
+ expect(result.valid).toBe(false);
86
+ expect(result.errors.some(e => e.includes('autoDeploy'))).toBe(true);
87
+ });
88
+
89
+ it('rejects invalid security gates', () => {
90
+ const rules = {
91
+ feature: { securityGates: 'sast' },
92
+ };
93
+ const result = validateRules(rules);
94
+ expect(result.valid).toBe(false);
95
+ });
96
+
97
+ it('rejects unknown deployment strategy', () => {
98
+ const rules = {
99
+ feature: { deploymentStrategy: 'teleport' },
100
+ };
101
+ const result = validateRules(rules);
102
+ expect(result.valid).toBe(false);
103
+ });
104
+
105
+ it('allows valid deployment strategies', () => {
106
+ const strategies = ['rolling', 'blue-green', 'canary', 'recreate'];
107
+ for (const strategy of strategies) {
108
+ const rules = { feature: { deploymentStrategy: strategy } };
109
+ const result = validateRules(rules);
110
+ expect(result.valid).toBe(true);
111
+ }
112
+ });
113
+ });
114
+
115
+ describe('getRulesForTier', () => {
116
+ it('returns rules for specified tier', () => {
117
+ const rules = getRulesForTier('stable');
118
+ expect(rules.requiresApproval).toBe(true);
119
+ });
120
+
121
+ it('returns feature rules for unknown tier', () => {
122
+ const rules = getRulesForTier('unknown');
123
+ expect(rules).toEqual(DEFAULT_RULES.feature);
124
+ });
125
+
126
+ it('accepts custom rules', () => {
127
+ const customRules = {
128
+ dev: { autoDeploy: false },
129
+ };
130
+ const rules = getRulesForTier('dev', customRules);
131
+ expect(rules.autoDeploy).toBe(false);
132
+ });
133
+ });
134
+
135
+ describe('mergeWithDefaults', () => {
136
+ it('merges custom rules with defaults', () => {
137
+ const custom = {
138
+ feature: { autoDeploy: false },
139
+ };
140
+ const merged = mergeWithDefaults(custom);
141
+ expect(merged.feature.autoDeploy).toBe(false);
142
+ expect(merged.feature.securityGates).toEqual(DEFAULT_RULES.feature.securityGates);
143
+ expect(merged.dev).toEqual(DEFAULT_RULES.dev);
144
+ });
145
+
146
+ it('preserves nested arrays', () => {
147
+ const custom = {
148
+ dev: { securityGates: ['sast'] },
149
+ };
150
+ const merged = mergeWithDefaults(custom);
151
+ expect(merged.dev.securityGates).toEqual(['sast']);
152
+ });
153
+
154
+ it('handles empty custom rules', () => {
155
+ const merged = mergeWithDefaults({});
156
+ expect(merged).toEqual(DEFAULT_RULES);
157
+ });
158
+ });
159
+
160
+ describe('createDeploymentRules', () => {
161
+ it('creates rules manager', () => {
162
+ const manager = createDeploymentRules();
163
+ expect(manager.getRules).toBeDefined();
164
+ expect(manager.validate).toBeDefined();
165
+ expect(manager.getForTier).toBeDefined();
166
+ });
167
+
168
+ it('loads rules on creation', () => {
169
+ const config = {
170
+ deployment: {
171
+ rules: {
172
+ feature: { autoDeploy: false },
173
+ },
174
+ },
175
+ };
176
+ const manager = createDeploymentRules(config);
177
+ expect(manager.getForTier('feature').autoDeploy).toBe(false);
178
+ });
179
+
180
+ it('exposes all rules', () => {
181
+ const manager = createDeploymentRules();
182
+ const rules = manager.getRules();
183
+ expect(rules.feature).toBeDefined();
184
+ expect(rules.dev).toBeDefined();
185
+ expect(rules.stable).toBeDefined();
186
+ });
187
+ });
188
+ });