tlc-claude-code 1.2.29 → 1.4.0

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 (182) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  14. package/dashboard/dist/components/UsagePane.js +51 -0
  15. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  16. package/dashboard/dist/components/UsagePane.test.js +142 -0
  17. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  18. package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
  19. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  20. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  21. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  22. package/dashboard/dist/components/WorkspacePane.js +17 -0
  23. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  24. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  25. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  26. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  27. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  28. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  29. package/package.json +1 -1
  30. package/server/lib/access-control-doc.js +541 -0
  31. package/server/lib/access-control-doc.test.js +672 -0
  32. package/server/lib/adr-generator.js +423 -0
  33. package/server/lib/adr-generator.test.js +586 -0
  34. package/server/lib/agent-progress-monitor.js +223 -0
  35. package/server/lib/agent-progress-monitor.test.js +202 -0
  36. package/server/lib/architecture-command.js +450 -0
  37. package/server/lib/architecture-command.test.js +754 -0
  38. package/server/lib/ast-analyzer.js +324 -0
  39. package/server/lib/ast-analyzer.test.js +437 -0
  40. package/server/lib/audit-attribution.js +191 -0
  41. package/server/lib/audit-attribution.test.js +359 -0
  42. package/server/lib/audit-classifier.js +202 -0
  43. package/server/lib/audit-classifier.test.js +209 -0
  44. package/server/lib/audit-command.js +275 -0
  45. package/server/lib/audit-command.test.js +325 -0
  46. package/server/lib/audit-exporter.js +380 -0
  47. package/server/lib/audit-exporter.test.js +464 -0
  48. package/server/lib/audit-logger.js +236 -0
  49. package/server/lib/audit-logger.test.js +364 -0
  50. package/server/lib/audit-query.js +257 -0
  51. package/server/lib/audit-query.test.js +352 -0
  52. package/server/lib/audit-storage.js +269 -0
  53. package/server/lib/audit-storage.test.js +272 -0
  54. package/server/lib/auth-system.test.js +4 -1
  55. package/server/lib/boundary-detector.js +427 -0
  56. package/server/lib/boundary-detector.test.js +320 -0
  57. package/server/lib/budget-alerts.js +138 -0
  58. package/server/lib/budget-alerts.test.js +235 -0
  59. package/server/lib/bulk-repo-init.js +342 -0
  60. package/server/lib/bulk-repo-init.test.js +388 -0
  61. package/server/lib/candidates-tracker.js +210 -0
  62. package/server/lib/candidates-tracker.test.js +300 -0
  63. package/server/lib/checkpoint-manager.js +251 -0
  64. package/server/lib/checkpoint-manager.test.js +474 -0
  65. package/server/lib/circular-detector.js +337 -0
  66. package/server/lib/circular-detector.test.js +353 -0
  67. package/server/lib/cohesion-analyzer.js +310 -0
  68. package/server/lib/cohesion-analyzer.test.js +447 -0
  69. package/server/lib/compliance-checklist.js +866 -0
  70. package/server/lib/compliance-checklist.test.js +476 -0
  71. package/server/lib/compliance-command.js +616 -0
  72. package/server/lib/compliance-command.test.js +551 -0
  73. package/server/lib/compliance-reporter.js +692 -0
  74. package/server/lib/compliance-reporter.test.js +707 -0
  75. package/server/lib/contract-testing.js +625 -0
  76. package/server/lib/contract-testing.test.js +342 -0
  77. package/server/lib/conversion-planner.js +469 -0
  78. package/server/lib/conversion-planner.test.js +361 -0
  79. package/server/lib/convert-command.js +351 -0
  80. package/server/lib/convert-command.test.js +608 -0
  81. package/server/lib/coupling-calculator.js +189 -0
  82. package/server/lib/coupling-calculator.test.js +509 -0
  83. package/server/lib/data-flow-doc.js +665 -0
  84. package/server/lib/data-flow-doc.test.js +659 -0
  85. package/server/lib/dependency-graph.js +367 -0
  86. package/server/lib/dependency-graph.test.js +516 -0
  87. package/server/lib/duplication-detector.js +349 -0
  88. package/server/lib/duplication-detector.test.js +401 -0
  89. package/server/lib/ephemeral-storage.js +249 -0
  90. package/server/lib/ephemeral-storage.test.js +254 -0
  91. package/server/lib/evidence-collector.js +627 -0
  92. package/server/lib/evidence-collector.test.js +901 -0
  93. package/server/lib/example-service.js +616 -0
  94. package/server/lib/example-service.test.js +397 -0
  95. package/server/lib/flow-diagram-generator.js +474 -0
  96. package/server/lib/flow-diagram-generator.test.js +446 -0
  97. package/server/lib/idp-manager.js +626 -0
  98. package/server/lib/idp-manager.test.js +587 -0
  99. package/server/lib/impact-scorer.js +184 -0
  100. package/server/lib/impact-scorer.test.js +211 -0
  101. package/server/lib/memory-exclusion.js +326 -0
  102. package/server/lib/memory-exclusion.test.js +241 -0
  103. package/server/lib/mermaid-generator.js +358 -0
  104. package/server/lib/mermaid-generator.test.js +301 -0
  105. package/server/lib/messaging-patterns.js +750 -0
  106. package/server/lib/messaging-patterns.test.js +213 -0
  107. package/server/lib/mfa-handler.js +452 -0
  108. package/server/lib/mfa-handler.test.js +490 -0
  109. package/server/lib/microservice-template.js +386 -0
  110. package/server/lib/microservice-template.test.js +325 -0
  111. package/server/lib/new-project-microservice.js +450 -0
  112. package/server/lib/new-project-microservice.test.js +600 -0
  113. package/server/lib/oauth-flow.js +375 -0
  114. package/server/lib/oauth-flow.test.js +487 -0
  115. package/server/lib/oauth-registry.js +190 -0
  116. package/server/lib/oauth-registry.test.js +306 -0
  117. package/server/lib/readme-generator.js +490 -0
  118. package/server/lib/readme-generator.test.js +493 -0
  119. package/server/lib/refactor-command.js +326 -0
  120. package/server/lib/refactor-command.test.js +528 -0
  121. package/server/lib/refactor-executor.js +254 -0
  122. package/server/lib/refactor-executor.test.js +305 -0
  123. package/server/lib/refactor-observer.js +292 -0
  124. package/server/lib/refactor-observer.test.js +422 -0
  125. package/server/lib/refactor-progress.js +193 -0
  126. package/server/lib/refactor-progress.test.js +251 -0
  127. package/server/lib/refactor-reporter.js +237 -0
  128. package/server/lib/refactor-reporter.test.js +247 -0
  129. package/server/lib/repo-dependency-tracker.js +261 -0
  130. package/server/lib/repo-dependency-tracker.test.js +350 -0
  131. package/server/lib/retention-policy.js +281 -0
  132. package/server/lib/retention-policy.test.js +486 -0
  133. package/server/lib/role-mapper.js +236 -0
  134. package/server/lib/role-mapper.test.js +395 -0
  135. package/server/lib/saml-provider.js +765 -0
  136. package/server/lib/saml-provider.test.js +643 -0
  137. package/server/lib/security-policy-generator.js +682 -0
  138. package/server/lib/security-policy-generator.test.js +544 -0
  139. package/server/lib/semantic-analyzer.js +198 -0
  140. package/server/lib/semantic-analyzer.test.js +474 -0
  141. package/server/lib/sensitive-detector.js +112 -0
  142. package/server/lib/sensitive-detector.test.js +209 -0
  143. package/server/lib/service-interaction-diagram.js +700 -0
  144. package/server/lib/service-interaction-diagram.test.js +638 -0
  145. package/server/lib/service-scaffold.js +486 -0
  146. package/server/lib/service-scaffold.test.js +373 -0
  147. package/server/lib/service-summary.js +553 -0
  148. package/server/lib/service-summary.test.js +619 -0
  149. package/server/lib/session-purge.js +460 -0
  150. package/server/lib/session-purge.test.js +312 -0
  151. package/server/lib/shared-kernel.js +578 -0
  152. package/server/lib/shared-kernel.test.js +255 -0
  153. package/server/lib/sso-command.js +544 -0
  154. package/server/lib/sso-command.test.js +552 -0
  155. package/server/lib/sso-session.js +492 -0
  156. package/server/lib/sso-session.test.js +670 -0
  157. package/server/lib/traefik-config.js +282 -0
  158. package/server/lib/traefik-config.test.js +312 -0
  159. package/server/lib/usage-command.js +218 -0
  160. package/server/lib/usage-command.test.js +391 -0
  161. package/server/lib/usage-formatter.js +192 -0
  162. package/server/lib/usage-formatter.test.js +267 -0
  163. package/server/lib/usage-history.js +122 -0
  164. package/server/lib/usage-history.test.js +206 -0
  165. package/server/lib/workspace-command.js +249 -0
  166. package/server/lib/workspace-command.test.js +264 -0
  167. package/server/lib/workspace-config.js +270 -0
  168. package/server/lib/workspace-config.test.js +312 -0
  169. package/server/lib/workspace-docs-command.js +547 -0
  170. package/server/lib/workspace-docs-command.test.js +692 -0
  171. package/server/lib/workspace-memory.js +451 -0
  172. package/server/lib/workspace-memory.test.js +403 -0
  173. package/server/lib/workspace-scanner.js +452 -0
  174. package/server/lib/workspace-scanner.test.js +677 -0
  175. package/server/lib/workspace-test-runner.js +315 -0
  176. package/server/lib/workspace-test-runner.test.js +294 -0
  177. package/server/lib/zero-retention-command.js +439 -0
  178. package/server/lib/zero-retention-command.test.js +448 -0
  179. package/server/lib/zero-retention.js +322 -0
  180. package/server/lib/zero-retention.test.js +258 -0
  181. package/server/package-lock.json +14 -0
  182. package/server/package.json +1 -0
@@ -0,0 +1,672 @@
1
+ /**
2
+ * Access Control Documenter Tests
3
+ * TDD: RED phase - Write failing tests first
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import {
8
+ createAccessControlDoc,
9
+ listUsers,
10
+ listRoles,
11
+ getRolePermissions,
12
+ getSSOMapping,
13
+ getAccessMatrix,
14
+ trackPermissionChange,
15
+ getPermissionHistory,
16
+ exportAsEvidence,
17
+ formatAccessReport,
18
+ detectOrphanedPermissions,
19
+ } from './access-control-doc.js';
20
+
21
+ describe('access-control-doc', () => {
22
+ describe('listUsers', () => {
23
+ it('returns all users with roles', () => {
24
+ const users = [
25
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
26
+ { id: '2', email: 'bob@example.com', name: 'Bob', role: 'engineer' },
27
+ { id: '3', email: 'carol@example.com', name: 'Carol', role: 'qa' },
28
+ ];
29
+
30
+ const result = listUsers(users);
31
+
32
+ expect(result).toHaveLength(3);
33
+ expect(result[0]).toEqual({
34
+ id: '1',
35
+ email: 'alice@example.com',
36
+ name: 'Alice',
37
+ role: 'admin',
38
+ });
39
+ expect(result[1]).toEqual({
40
+ id: '2',
41
+ email: 'bob@example.com',
42
+ name: 'Bob',
43
+ role: 'engineer',
44
+ });
45
+ });
46
+
47
+ it('returns empty array for empty users', () => {
48
+ expect(listUsers([])).toEqual([]);
49
+ expect(listUsers(null)).toEqual([]);
50
+ expect(listUsers(undefined)).toEqual([]);
51
+ });
52
+
53
+ it('filters out sensitive fields', () => {
54
+ const users = [
55
+ {
56
+ id: '1',
57
+ email: 'alice@example.com',
58
+ name: 'Alice',
59
+ role: 'admin',
60
+ passwordHash: 'secret',
61
+ passwordSalt: 'salt',
62
+ },
63
+ ];
64
+
65
+ const result = listUsers(users);
66
+
67
+ expect(result[0]).not.toHaveProperty('passwordHash');
68
+ expect(result[0]).not.toHaveProperty('passwordSalt');
69
+ });
70
+
71
+ it('sorts users by role then name', () => {
72
+ const users = [
73
+ { id: '2', email: 'bob@example.com', name: 'Bob', role: 'engineer' },
74
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
75
+ { id: '3', email: 'carol@example.com', name: 'Carol', role: 'engineer' },
76
+ ];
77
+
78
+ const result = listUsers(users);
79
+
80
+ expect(result[0].role).toBe('admin');
81
+ expect(result[1].name).toBe('Bob');
82
+ expect(result[2].name).toBe('Carol');
83
+ });
84
+ });
85
+
86
+ describe('listRoles', () => {
87
+ it('returns all roles with permissions', () => {
88
+ const result = listRoles();
89
+
90
+ expect(result).toHaveProperty('admin');
91
+ expect(result).toHaveProperty('engineer');
92
+ expect(result).toHaveProperty('qa');
93
+ expect(result).toHaveProperty('po');
94
+ });
95
+
96
+ it('admin has wildcard permission', () => {
97
+ const result = listRoles();
98
+
99
+ expect(result.admin.permissions).toContain('*');
100
+ });
101
+
102
+ it('engineer has expected permissions', () => {
103
+ const result = listRoles();
104
+
105
+ expect(result.engineer.permissions).toContain('read');
106
+ expect(result.engineer.permissions).toContain('write');
107
+ expect(result.engineer.permissions).toContain('deploy');
108
+ expect(result.engineer.permissions).toContain('claim');
109
+ expect(result.engineer.permissions).toContain('release');
110
+ });
111
+
112
+ it('qa has expected permissions', () => {
113
+ const result = listRoles();
114
+
115
+ expect(result.qa.permissions).toContain('read');
116
+ expect(result.qa.permissions).toContain('verify');
117
+ expect(result.qa.permissions).toContain('bug');
118
+ expect(result.qa.permissions).toContain('test');
119
+ });
120
+
121
+ it('po has expected permissions', () => {
122
+ const result = listRoles();
123
+
124
+ expect(result.po.permissions).toContain('read');
125
+ expect(result.po.permissions).toContain('plan');
126
+ expect(result.po.permissions).toContain('verify');
127
+ expect(result.po.permissions).toContain('approve');
128
+ });
129
+
130
+ it('includes role descriptions', () => {
131
+ const result = listRoles();
132
+
133
+ expect(result.admin).toHaveProperty('description');
134
+ expect(result.engineer).toHaveProperty('description');
135
+ });
136
+ });
137
+
138
+ describe('getRolePermissions', () => {
139
+ it('returns permissions for role', () => {
140
+ const result = getRolePermissions('engineer');
141
+
142
+ expect(result).toEqual(['read', 'write', 'deploy', 'claim', 'release']);
143
+ });
144
+
145
+ it('returns wildcard for admin', () => {
146
+ const result = getRolePermissions('admin');
147
+
148
+ expect(result).toEqual(['*']);
149
+ });
150
+
151
+ it('returns empty array for unknown role', () => {
152
+ const result = getRolePermissions('unknown');
153
+
154
+ expect(result).toEqual([]);
155
+ });
156
+
157
+ it('handles null/undefined role', () => {
158
+ expect(getRolePermissions(null)).toEqual([]);
159
+ expect(getRolePermissions(undefined)).toEqual([]);
160
+ });
161
+
162
+ it('expands admin wildcard when requested', () => {
163
+ const result = getRolePermissions('admin', { expand: true });
164
+
165
+ expect(result).toContain('read');
166
+ expect(result).toContain('write');
167
+ expect(result).toContain('deploy');
168
+ expect(result).toContain('verify');
169
+ expect(result).toContain('plan');
170
+ expect(result).toContain('approve');
171
+ });
172
+ });
173
+
174
+ describe('getSSOMapping', () => {
175
+ it('returns IdP group mappings', () => {
176
+ const config = {
177
+ sso: {
178
+ roleMappings: [
179
+ { pattern: '^admin$', role: 'admin', priority: 1 },
180
+ { pattern: '^dev-.*', role: 'engineer', priority: 2 },
181
+ ],
182
+ defaultRole: 'engineer',
183
+ },
184
+ };
185
+
186
+ const result = getSSOMapping(config);
187
+
188
+ expect(result.mappings).toHaveLength(2);
189
+ expect(result.defaultRole).toBe('engineer');
190
+ });
191
+
192
+ it('returns empty mappings when no SSO config', () => {
193
+ const result = getSSOMapping({});
194
+
195
+ expect(result.mappings).toEqual([]);
196
+ expect(result.defaultRole).toBeNull();
197
+ });
198
+
199
+ it('includes mapping details', () => {
200
+ const config = {
201
+ sso: {
202
+ roleMappings: [
203
+ {
204
+ pattern: '^admin$',
205
+ role: 'admin',
206
+ priority: 1,
207
+ description: 'Admin group',
208
+ },
209
+ ],
210
+ },
211
+ };
212
+
213
+ const result = getSSOMapping(config);
214
+
215
+ expect(result.mappings[0]).toHaveProperty('pattern');
216
+ expect(result.mappings[0]).toHaveProperty('role');
217
+ expect(result.mappings[0]).toHaveProperty('priority');
218
+ });
219
+
220
+ it('sorts mappings by priority', () => {
221
+ const config = {
222
+ sso: {
223
+ roleMappings: [
224
+ { pattern: '^qa$', role: 'qa', priority: 3 },
225
+ { pattern: '^admin$', role: 'admin', priority: 1 },
226
+ { pattern: '^dev$', role: 'engineer', priority: 2 },
227
+ ],
228
+ },
229
+ };
230
+
231
+ const result = getSSOMapping(config);
232
+
233
+ expect(result.mappings[0].role).toBe('admin');
234
+ expect(result.mappings[1].role).toBe('engineer');
235
+ expect(result.mappings[2].role).toBe('qa');
236
+ });
237
+ });
238
+
239
+ describe('getAccessMatrix', () => {
240
+ it('generates user/permission matrix', () => {
241
+ const users = [
242
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
243
+ { id: '2', email: 'bob@example.com', name: 'Bob', role: 'engineer' },
244
+ ];
245
+
246
+ const result = getAccessMatrix(users);
247
+
248
+ expect(result.users).toContain('alice@example.com');
249
+ expect(result.users).toContain('bob@example.com');
250
+ expect(result.permissions).toContain('read');
251
+ expect(result.permissions).toContain('write');
252
+ expect(result.matrix['alice@example.com'].read).toBe(true);
253
+ expect(result.matrix['alice@example.com'].write).toBe(true);
254
+ expect(result.matrix['bob@example.com'].read).toBe(true);
255
+ expect(result.matrix['bob@example.com'].write).toBe(true);
256
+ });
257
+
258
+ it('admin has all permissions', () => {
259
+ const users = [
260
+ { id: '1', email: 'admin@example.com', name: 'Admin', role: 'admin' },
261
+ ];
262
+
263
+ const result = getAccessMatrix(users);
264
+
265
+ // Admin should have all permissions due to wildcard
266
+ expect(result.matrix['admin@example.com'].read).toBe(true);
267
+ expect(result.matrix['admin@example.com'].write).toBe(true);
268
+ expect(result.matrix['admin@example.com'].deploy).toBe(true);
269
+ expect(result.matrix['admin@example.com'].verify).toBe(true);
270
+ });
271
+
272
+ it('qa has limited permissions', () => {
273
+ const users = [
274
+ { id: '1', email: 'qa@example.com', name: 'QA', role: 'qa' },
275
+ ];
276
+
277
+ const result = getAccessMatrix(users);
278
+
279
+ expect(result.matrix['qa@example.com'].read).toBe(true);
280
+ expect(result.matrix['qa@example.com'].verify).toBe(true);
281
+ expect(result.matrix['qa@example.com'].write).toBe(false);
282
+ expect(result.matrix['qa@example.com'].deploy).toBe(false);
283
+ });
284
+
285
+ it('returns empty matrix for no users', () => {
286
+ const result = getAccessMatrix([]);
287
+
288
+ expect(result.users).toEqual([]);
289
+ expect(result.permissions).toBeDefined();
290
+ expect(result.matrix).toEqual({});
291
+ });
292
+ });
293
+
294
+ describe('trackPermissionChange', () => {
295
+ it('logs permission changes', () => {
296
+ const store = createPermissionStore();
297
+
298
+ const change = trackPermissionChange(store, {
299
+ userId: '1',
300
+ oldRole: 'engineer',
301
+ newRole: 'admin',
302
+ changedBy: 'system',
303
+ reason: 'Promotion',
304
+ });
305
+
306
+ expect(change).toHaveProperty('id');
307
+ expect(change).toHaveProperty('timestamp');
308
+ expect(change.userId).toBe('1');
309
+ expect(change.oldRole).toBe('engineer');
310
+ expect(change.newRole).toBe('admin');
311
+ });
312
+
313
+ it('records timestamp automatically', () => {
314
+ const store = createPermissionStore();
315
+ const before = new Date().toISOString();
316
+
317
+ const change = trackPermissionChange(store, {
318
+ userId: '1',
319
+ oldRole: 'engineer',
320
+ newRole: 'qa',
321
+ changedBy: 'admin@example.com',
322
+ });
323
+
324
+ const after = new Date().toISOString();
325
+
326
+ expect(change.timestamp >= before).toBe(true);
327
+ expect(change.timestamp <= after).toBe(true);
328
+ });
329
+
330
+ it('stores change in history', () => {
331
+ const store = createPermissionStore();
332
+
333
+ trackPermissionChange(store, {
334
+ userId: '1',
335
+ oldRole: 'engineer',
336
+ newRole: 'admin',
337
+ changedBy: 'system',
338
+ });
339
+
340
+ expect(store.getHistory()).toHaveLength(1);
341
+ });
342
+ });
343
+
344
+ describe('getPermissionHistory', () => {
345
+ it('returns change history', () => {
346
+ const store = createPermissionStore();
347
+
348
+ trackPermissionChange(store, {
349
+ userId: '1',
350
+ oldRole: 'engineer',
351
+ newRole: 'admin',
352
+ changedBy: 'system',
353
+ });
354
+
355
+ trackPermissionChange(store, {
356
+ userId: '2',
357
+ oldRole: 'qa',
358
+ newRole: 'engineer',
359
+ changedBy: 'admin@example.com',
360
+ });
361
+
362
+ const history = getPermissionHistory(store);
363
+
364
+ expect(history).toHaveLength(2);
365
+ });
366
+
367
+ it('filters by user', () => {
368
+ const store = createPermissionStore();
369
+
370
+ trackPermissionChange(store, {
371
+ userId: '1',
372
+ oldRole: 'engineer',
373
+ newRole: 'admin',
374
+ changedBy: 'system',
375
+ });
376
+
377
+ trackPermissionChange(store, {
378
+ userId: '2',
379
+ oldRole: 'qa',
380
+ newRole: 'engineer',
381
+ changedBy: 'admin@example.com',
382
+ });
383
+
384
+ const history = getPermissionHistory(store, { userId: '1' });
385
+
386
+ expect(history).toHaveLength(1);
387
+ expect(history[0].userId).toBe('1');
388
+ });
389
+
390
+ it('filters by date range', () => {
391
+ const store = createPermissionStore();
392
+ const now = new Date();
393
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
394
+ const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
395
+
396
+ trackPermissionChange(store, {
397
+ userId: '1',
398
+ oldRole: 'engineer',
399
+ newRole: 'admin',
400
+ changedBy: 'system',
401
+ });
402
+
403
+ const history = getPermissionHistory(store, {
404
+ from: yesterday.toISOString(),
405
+ to: tomorrow.toISOString(),
406
+ });
407
+
408
+ expect(history).toHaveLength(1);
409
+ });
410
+
411
+ it('returns empty array for empty store', () => {
412
+ const store = createPermissionStore();
413
+
414
+ const history = getPermissionHistory(store);
415
+
416
+ expect(history).toEqual([]);
417
+ });
418
+
419
+ it('returns history in reverse chronological order', () => {
420
+ const store = createPermissionStore();
421
+
422
+ trackPermissionChange(store, {
423
+ userId: '1',
424
+ oldRole: 'engineer',
425
+ newRole: 'qa',
426
+ changedBy: 'system',
427
+ });
428
+
429
+ // Small delay to ensure different timestamps
430
+ trackPermissionChange(store, {
431
+ userId: '2',
432
+ oldRole: 'qa',
433
+ newRole: 'admin',
434
+ changedBy: 'system',
435
+ });
436
+
437
+ const history = getPermissionHistory(store);
438
+
439
+ expect(history[0].userId).toBe('2'); // Most recent first
440
+ });
441
+ });
442
+
443
+ describe('exportAsEvidence', () => {
444
+ it('generates compliance format', () => {
445
+ const users = [
446
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
447
+ { id: '2', email: 'bob@example.com', name: 'Bob', role: 'engineer' },
448
+ ];
449
+ const config = {
450
+ sso: {
451
+ roleMappings: [{ pattern: '^admin$', role: 'admin', priority: 1 }],
452
+ },
453
+ };
454
+
455
+ const evidence = exportAsEvidence(users, config, {});
456
+
457
+ expect(evidence).toHaveProperty('exportDate');
458
+ expect(evidence).toHaveProperty('users');
459
+ expect(evidence).toHaveProperty('roles');
460
+ expect(evidence).toHaveProperty('ssoMappings');
461
+ expect(evidence).toHaveProperty('accessMatrix');
462
+ });
463
+
464
+ it('includes metadata', () => {
465
+ const evidence = exportAsEvidence([], {}, {});
466
+
467
+ expect(evidence).toHaveProperty('version');
468
+ expect(evidence).toHaveProperty('exportDate');
469
+ expect(evidence).toHaveProperty('exportedBy');
470
+ });
471
+
472
+ it('supports JSON format', () => {
473
+ const evidence = exportAsEvidence([], {}, { format: 'json' });
474
+
475
+ expect(typeof evidence).toBe('object');
476
+ });
477
+
478
+ it('supports CSV format', () => {
479
+ const users = [
480
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
481
+ ];
482
+
483
+ const evidence = exportAsEvidence(users, {}, { format: 'csv' });
484
+
485
+ expect(typeof evidence).toBe('string');
486
+ expect(evidence).toContain('email');
487
+ expect(evidence).toContain('alice@example.com');
488
+ });
489
+
490
+ it('includes permission history when available', () => {
491
+ const store = createPermissionStore();
492
+
493
+ trackPermissionChange(store, {
494
+ userId: '1',
495
+ oldRole: 'engineer',
496
+ newRole: 'admin',
497
+ changedBy: 'system',
498
+ });
499
+
500
+ const evidence = exportAsEvidence([], {}, { permissionStore: store });
501
+
502
+ expect(evidence).toHaveProperty('permissionHistory');
503
+ expect(evidence.permissionHistory).toHaveLength(1);
504
+ });
505
+ });
506
+
507
+ describe('formatAccessReport', () => {
508
+ it('generates readable report', () => {
509
+ const users = [
510
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
511
+ { id: '2', email: 'bob@example.com', name: 'Bob', role: 'engineer' },
512
+ ];
513
+
514
+ const report = formatAccessReport(users);
515
+
516
+ expect(report).toContain('Access Control Report');
517
+ expect(report).toContain('alice@example.com');
518
+ expect(report).toContain('admin');
519
+ });
520
+
521
+ it('includes role summary', () => {
522
+ const users = [
523
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
524
+ { id: '2', email: 'bob@example.com', name: 'Bob', role: 'engineer' },
525
+ { id: '3', email: 'carol@example.com', name: 'Carol', role: 'engineer' },
526
+ ];
527
+
528
+ const report = formatAccessReport(users);
529
+
530
+ expect(report).toContain('Role Summary');
531
+ expect(report).toContain('admin: 1');
532
+ expect(report).toContain('engineer: 2');
533
+ });
534
+
535
+ it('includes permission matrix section', () => {
536
+ const users = [
537
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'admin' },
538
+ ];
539
+
540
+ const report = formatAccessReport(users, { includeMatrix: true });
541
+
542
+ expect(report).toContain('Permission Matrix');
543
+ });
544
+
545
+ it('includes SSO mappings when config provided', () => {
546
+ const users = [];
547
+ const config = {
548
+ sso: {
549
+ roleMappings: [{ pattern: '^admin$', role: 'admin', priority: 1 }],
550
+ },
551
+ };
552
+
553
+ const report = formatAccessReport(users, { config });
554
+
555
+ expect(report).toContain('SSO Role Mappings');
556
+ });
557
+
558
+ it('handles empty users', () => {
559
+ const report = formatAccessReport([]);
560
+
561
+ expect(report).toContain('Access Control Report');
562
+ expect(report).toContain('No users');
563
+ });
564
+ });
565
+
566
+ describe('detectOrphanedPermissions', () => {
567
+ it('finds unused permissions', () => {
568
+ const users = [
569
+ { id: '1', email: 'alice@example.com', name: 'Alice', role: 'qa' },
570
+ ];
571
+
572
+ const orphaned = detectOrphanedPermissions(users);
573
+
574
+ // QA doesn't have write, deploy, claim, release, plan, approve
575
+ expect(orphaned).toContain('write');
576
+ expect(orphaned).toContain('deploy');
577
+ });
578
+
579
+ it('returns empty when all permissions used', () => {
580
+ const users = [
581
+ { id: '1', email: 'admin@example.com', name: 'Admin', role: 'admin' },
582
+ ];
583
+
584
+ const orphaned = detectOrphanedPermissions(users);
585
+
586
+ // Admin has wildcard, so all permissions are used
587
+ expect(orphaned).toEqual([]);
588
+ });
589
+
590
+ it('handles multiple roles', () => {
591
+ const users = [
592
+ { id: '1', email: 'eng@example.com', name: 'Engineer', role: 'engineer' },
593
+ { id: '2', email: 'qa@example.com', name: 'QA', role: 'qa' },
594
+ { id: '3', email: 'po@example.com', name: 'PO', role: 'po' },
595
+ ];
596
+
597
+ const orphaned = detectOrphanedPermissions(users);
598
+
599
+ // All roles together should cover most permissions
600
+ // Only 'approve' might be unique to PO
601
+ expect(orphaned).not.toContain('read');
602
+ expect(orphaned).not.toContain('write');
603
+ expect(orphaned).not.toContain('verify');
604
+ });
605
+
606
+ it('returns all permissions when no users', () => {
607
+ const orphaned = detectOrphanedPermissions([]);
608
+
609
+ expect(orphaned.length).toBeGreaterThan(0);
610
+ expect(orphaned).toContain('read');
611
+ expect(orphaned).toContain('write');
612
+ });
613
+ });
614
+
615
+ describe('createAccessControlDoc', () => {
616
+ it('creates documenter instance', () => {
617
+ const doc = createAccessControlDoc();
618
+
619
+ expect(doc).toHaveProperty('listUsers');
620
+ expect(doc).toHaveProperty('listRoles');
621
+ expect(doc).toHaveProperty('getRolePermissions');
622
+ expect(doc).toHaveProperty('getSSOMapping');
623
+ expect(doc).toHaveProperty('getAccessMatrix');
624
+ expect(doc).toHaveProperty('trackPermissionChange');
625
+ expect(doc).toHaveProperty('getPermissionHistory');
626
+ expect(doc).toHaveProperty('exportAsEvidence');
627
+ expect(doc).toHaveProperty('formatAccessReport');
628
+ expect(doc).toHaveProperty('detectOrphanedPermissions');
629
+ });
630
+
631
+ it('accepts config on creation', () => {
632
+ const config = {
633
+ sso: {
634
+ roleMappings: [{ pattern: '^admin$', role: 'admin', priority: 1 }],
635
+ },
636
+ };
637
+
638
+ const doc = createAccessControlDoc({ config });
639
+
640
+ const mapping = doc.getSSOMapping();
641
+ expect(mapping.mappings).toHaveLength(1);
642
+ });
643
+
644
+ it('maintains internal permission store', () => {
645
+ const doc = createAccessControlDoc();
646
+
647
+ doc.trackPermissionChange({
648
+ userId: '1',
649
+ oldRole: 'engineer',
650
+ newRole: 'admin',
651
+ changedBy: 'system',
652
+ });
653
+
654
+ const history = doc.getPermissionHistory();
655
+ expect(history).toHaveLength(1);
656
+ });
657
+ });
658
+ });
659
+
660
+ // Helper for tests
661
+ function createPermissionStore() {
662
+ const changes = [];
663
+
664
+ return {
665
+ add(change) {
666
+ changes.push(change);
667
+ },
668
+ getHistory() {
669
+ return [...changes];
670
+ },
671
+ };
672
+ }