@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.47

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 (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +695 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,419 @@
1
+ /**
2
+ * UpdateProgressMonitor Tests
3
+ *
4
+ * TDD tests for monitoring CloudFormation UPDATE operations
5
+ */
6
+
7
+ const { UpdateProgressMonitor } = require('../update-progress-monitor');
8
+ const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
9
+
10
+ // Mock timers for testing delays and timeouts
11
+ jest.useFakeTimers();
12
+
13
+ describe('UpdateProgressMonitor', () => {
14
+ let monitor;
15
+ let mockCFRepo;
16
+ let onProgressCallback;
17
+
18
+ beforeEach(() => {
19
+ // Reset mock CloudFormation repository
20
+ mockCFRepo = {
21
+ getStackEvents: jest.fn(),
22
+ getStackStatus: jest.fn(),
23
+ };
24
+
25
+ // Reset progress callback
26
+ onProgressCallback = jest.fn();
27
+
28
+ // Create monitor instance
29
+ monitor = new UpdateProgressMonitor({
30
+ cloudFormationRepository: mockCFRepo,
31
+ });
32
+
33
+ // Clear all timers
34
+ jest.clearAllTimers();
35
+ });
36
+
37
+ afterEach(() => {
38
+ jest.clearAllTimers();
39
+ });
40
+
41
+ describe('constructor', () => {
42
+ it('should require cloudFormationRepository', () => {
43
+ expect(() => new UpdateProgressMonitor({})).toThrow(
44
+ 'cloudFormationRepository is required'
45
+ );
46
+ });
47
+
48
+ it('should create instance with valid dependencies', () => {
49
+ const monitor = new UpdateProgressMonitor({
50
+ cloudFormationRepository: mockCFRepo,
51
+ });
52
+ expect(monitor).toBeInstanceOf(UpdateProgressMonitor);
53
+ });
54
+ });
55
+
56
+ describe('monitorUpdate - successful update', () => {
57
+ it('should monitor single resource update to completion', async () => {
58
+ const stackIdentifier = new StackIdentifier({
59
+ stackName: 'test-stack',
60
+ region: 'us-east-1',
61
+ });
62
+
63
+ const resourceLogicalIds = ['AttioLambdaFunction'];
64
+
65
+ // Mock stack events sequence
66
+ mockCFRepo.getStackEvents
67
+ // First poll: UPDATE_IN_PROGRESS
68
+ .mockResolvedValueOnce([
69
+ {
70
+ LogicalResourceId: 'AttioLambdaFunction',
71
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
72
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
73
+ },
74
+ ])
75
+ // Second poll: UPDATE_COMPLETE
76
+ .mockResolvedValueOnce([
77
+ {
78
+ LogicalResourceId: 'AttioLambdaFunction',
79
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
80
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
81
+ },
82
+ {
83
+ LogicalResourceId: 'AttioLambdaFunction',
84
+ ResourceStatus: 'UPDATE_COMPLETE',
85
+ Timestamp: new Date('2025-01-01T00:00:05Z'),
86
+ },
87
+ ]);
88
+
89
+ // Mock stack status
90
+ mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
91
+
92
+ // Start monitoring in background
93
+ const monitorPromise = monitor.monitorUpdate({
94
+ stackIdentifier,
95
+ resourceLogicalIds,
96
+ onProgress: onProgressCallback,
97
+ });
98
+
99
+ // Advance timers to trigger first poll (2 seconds)
100
+ await jest.advanceTimersByTimeAsync(2000);
101
+
102
+ // Advance timers to trigger second poll (2 more seconds)
103
+ await jest.advanceTimersByTimeAsync(2000);
104
+
105
+ // Wait for monitoring to complete
106
+ const result = await monitorPromise;
107
+
108
+ // Verify result
109
+ expect(result.success).toBe(true);
110
+ expect(result.updatedCount).toBe(1);
111
+ expect(result.failedCount).toBe(0);
112
+
113
+ // Verify progress callbacks
114
+ expect(onProgressCallback).toHaveBeenCalledWith({
115
+ logicalId: 'AttioLambdaFunction',
116
+ status: 'IN_PROGRESS',
117
+ });
118
+ expect(onProgressCallback).toHaveBeenCalledWith({
119
+ logicalId: 'AttioLambdaFunction',
120
+ status: 'COMPLETE',
121
+ progress: 1,
122
+ total: 1,
123
+ });
124
+ });
125
+
126
+ it('should monitor multiple resources updating simultaneously', async () => {
127
+ const stackIdentifier = new StackIdentifier({
128
+ stackName: 'test-stack',
129
+ region: 'us-east-1',
130
+ });
131
+
132
+ const resourceLogicalIds = [
133
+ 'AttioLambdaFunction',
134
+ 'PipedriveLambdaFunction',
135
+ 'ZohoCrmLambdaFunction',
136
+ ];
137
+
138
+ // Mock stack events - all resources update together
139
+ mockCFRepo.getStackEvents
140
+ // First poll: All IN_PROGRESS
141
+ .mockResolvedValueOnce([
142
+ {
143
+ LogicalResourceId: 'AttioLambdaFunction',
144
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
145
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
146
+ },
147
+ {
148
+ LogicalResourceId: 'PipedriveLambdaFunction',
149
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
150
+ Timestamp: new Date('2025-01-01T00:00:02Z'),
151
+ },
152
+ {
153
+ LogicalResourceId: 'ZohoCrmLambdaFunction',
154
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
155
+ Timestamp: new Date('2025-01-01T00:00:03Z'),
156
+ },
157
+ ])
158
+ // Second poll: First complete
159
+ .mockResolvedValueOnce([
160
+ {
161
+ LogicalResourceId: 'AttioLambdaFunction',
162
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
163
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
164
+ },
165
+ {
166
+ LogicalResourceId: 'AttioLambdaFunction',
167
+ ResourceStatus: 'UPDATE_COMPLETE',
168
+ Timestamp: new Date('2025-01-01T00:00:10Z'),
169
+ },
170
+ {
171
+ LogicalResourceId: 'PipedriveLambdaFunction',
172
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
173
+ Timestamp: new Date('2025-01-01T00:00:02Z'),
174
+ },
175
+ {
176
+ LogicalResourceId: 'ZohoCrmLambdaFunction',
177
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
178
+ Timestamp: new Date('2025-01-01T00:00:03Z'),
179
+ },
180
+ ])
181
+ // Third poll: All complete
182
+ .mockResolvedValueOnce([
183
+ {
184
+ LogicalResourceId: 'AttioLambdaFunction',
185
+ ResourceStatus: 'UPDATE_COMPLETE',
186
+ Timestamp: new Date('2025-01-01T00:00:10Z'),
187
+ },
188
+ {
189
+ LogicalResourceId: 'PipedriveLambdaFunction',
190
+ ResourceStatus: 'UPDATE_COMPLETE',
191
+ Timestamp: new Date('2025-01-01T00:00:11Z'),
192
+ },
193
+ {
194
+ LogicalResourceId: 'ZohoCrmLambdaFunction',
195
+ ResourceStatus: 'UPDATE_COMPLETE',
196
+ Timestamp: new Date('2025-01-01T00:00:12Z'),
197
+ },
198
+ ]);
199
+
200
+ mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
201
+
202
+ const monitorPromise = monitor.monitorUpdate({
203
+ stackIdentifier,
204
+ resourceLogicalIds,
205
+ onProgress: onProgressCallback,
206
+ });
207
+
208
+ // Advance through polling intervals
209
+ await jest.advanceTimersByTimeAsync(2000);
210
+ await jest.advanceTimersByTimeAsync(2000);
211
+ await jest.advanceTimersByTimeAsync(2000);
212
+
213
+ const result = await monitorPromise;
214
+
215
+ expect(result.success).toBe(true);
216
+ expect(result.updatedCount).toBe(3);
217
+ expect(result.failedCount).toBe(0);
218
+ });
219
+ });
220
+
221
+ describe('monitorUpdate - failed updates', () => {
222
+ it('should detect and report UPDATE_FAILED resources', async () => {
223
+ const stackIdentifier = new StackIdentifier({
224
+ stackName: 'test-stack',
225
+ region: 'us-east-1',
226
+ });
227
+
228
+ const resourceLogicalIds = ['AttioLambdaFunction'];
229
+
230
+ mockCFRepo.getStackEvents
231
+ .mockResolvedValueOnce([
232
+ {
233
+ LogicalResourceId: 'AttioLambdaFunction',
234
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
235
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
236
+ },
237
+ ])
238
+ .mockResolvedValueOnce([
239
+ {
240
+ LogicalResourceId: 'AttioLambdaFunction',
241
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
242
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
243
+ },
244
+ {
245
+ LogicalResourceId: 'AttioLambdaFunction',
246
+ ResourceStatus: 'UPDATE_FAILED',
247
+ ResourceStatusReason: 'Subnet does not exist: subnet-invalid',
248
+ Timestamp: new Date('2025-01-01T00:00:05Z'),
249
+ },
250
+ ]);
251
+
252
+ mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_ROLLBACK_COMPLETE');
253
+
254
+ const monitorPromise = monitor.monitorUpdate({
255
+ stackIdentifier,
256
+ resourceLogicalIds,
257
+ onProgress: onProgressCallback,
258
+ });
259
+
260
+ await jest.advanceTimersByTimeAsync(2000);
261
+ await jest.advanceTimersByTimeAsync(2000);
262
+
263
+ const result = await monitorPromise;
264
+
265
+ expect(result.success).toBe(false);
266
+ expect(result.updatedCount).toBe(0);
267
+ expect(result.failedCount).toBe(1);
268
+ expect(result.failedResources).toHaveLength(1);
269
+ expect(result.failedResources[0].logicalId).toBe('AttioLambdaFunction');
270
+ expect(result.failedResources[0].reason).toBe('Subnet does not exist: subnet-invalid');
271
+
272
+ // Verify FAILED callback was triggered
273
+ expect(onProgressCallback).toHaveBeenCalledWith({
274
+ logicalId: 'AttioLambdaFunction',
275
+ status: 'FAILED',
276
+ reason: 'Subnet does not exist: subnet-invalid',
277
+ });
278
+ });
279
+
280
+ it('should detect stack rollback during update', async () => {
281
+ const stackIdentifier = new StackIdentifier({
282
+ stackName: 'test-stack',
283
+ region: 'us-east-1',
284
+ });
285
+
286
+ const resourceLogicalIds = ['AttioLambdaFunction'];
287
+
288
+ mockCFRepo.getStackEvents.mockResolvedValue([
289
+ {
290
+ LogicalResourceId: 'AttioLambdaFunction',
291
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
292
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
293
+ },
294
+ ]);
295
+
296
+ // Stack status shows rollback in progress
297
+ mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_ROLLBACK_IN_PROGRESS');
298
+
299
+ const monitorPromise = monitor.monitorUpdate({
300
+ stackIdentifier,
301
+ resourceLogicalIds,
302
+ onProgress: onProgressCallback,
303
+ });
304
+
305
+ await jest.advanceTimersByTimeAsync(2000);
306
+
307
+ await expect(monitorPromise).rejects.toThrow('Update operation failed and rolled back');
308
+ });
309
+ });
310
+
311
+ describe('monitorUpdate - timeout handling', () => {
312
+ it('should timeout after 5 minutes', async () => {
313
+ const stackIdentifier = new StackIdentifier({
314
+ stackName: 'test-stack',
315
+ region: 'us-east-1',
316
+ });
317
+
318
+ const resourceLogicalIds = ['AttioLambdaFunction'];
319
+
320
+ // Mock events that never complete
321
+ mockCFRepo.getStackEvents.mockResolvedValue([
322
+ {
323
+ LogicalResourceId: 'AttioLambdaFunction',
324
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
325
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
326
+ },
327
+ ]);
328
+
329
+ mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_IN_PROGRESS');
330
+
331
+ const monitorPromise = monitor.monitorUpdate({
332
+ stackIdentifier,
333
+ resourceLogicalIds,
334
+ onProgress: onProgressCallback,
335
+ });
336
+
337
+ // Advance past 5 minute timeout (300000ms)
338
+ await jest.advanceTimersByTimeAsync(300000 + 2000);
339
+
340
+ await expect(monitorPromise).rejects.toThrow('Update operation timed out');
341
+ });
342
+ });
343
+
344
+ describe('monitorUpdate - event deduplication', () => {
345
+ it('should not process duplicate events', async () => {
346
+ const stackIdentifier = new StackIdentifier({
347
+ stackName: 'test-stack',
348
+ region: 'us-east-1',
349
+ });
350
+
351
+ const resourceLogicalIds = ['AttioLambdaFunction'];
352
+
353
+ // Mock duplicate events (same timestamp + logicalId + status)
354
+ mockCFRepo.getStackEvents
355
+ .mockResolvedValueOnce([
356
+ {
357
+ LogicalResourceId: 'AttioLambdaFunction',
358
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
359
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
360
+ },
361
+ ])
362
+ .mockResolvedValueOnce([
363
+ // Duplicate event
364
+ {
365
+ LogicalResourceId: 'AttioLambdaFunction',
366
+ ResourceStatus: 'UPDATE_IN_PROGRESS',
367
+ Timestamp: new Date('2025-01-01T00:00:01Z'),
368
+ },
369
+ // New event
370
+ {
371
+ LogicalResourceId: 'AttioLambdaFunction',
372
+ ResourceStatus: 'UPDATE_COMPLETE',
373
+ Timestamp: new Date('2025-01-01T00:00:05Z'),
374
+ },
375
+ ]);
376
+
377
+ mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
378
+
379
+ const monitorPromise = monitor.monitorUpdate({
380
+ stackIdentifier,
381
+ resourceLogicalIds,
382
+ onProgress: onProgressCallback,
383
+ });
384
+
385
+ await jest.advanceTimersByTimeAsync(2000);
386
+ await jest.advanceTimersByTimeAsync(2000);
387
+
388
+ await monitorPromise;
389
+
390
+ // IN_PROGRESS callback should only be called once (not twice for duplicate)
391
+ const inProgressCalls = onProgressCallback.mock.calls.filter(
392
+ (call) => call[0].status === 'IN_PROGRESS'
393
+ );
394
+ expect(inProgressCalls).toHaveLength(1);
395
+ });
396
+ });
397
+
398
+ describe('monitorUpdate - no resources to track', () => {
399
+ it('should return immediately if no resources to track', async () => {
400
+ const stackIdentifier = new StackIdentifier({
401
+ stackName: 'test-stack',
402
+ region: 'us-east-1',
403
+ });
404
+
405
+ const result = await monitor.monitorUpdate({
406
+ stackIdentifier,
407
+ resourceLogicalIds: [],
408
+ onProgress: onProgressCallback,
409
+ });
410
+
411
+ expect(result.success).toBe(true);
412
+ expect(result.updatedCount).toBe(0);
413
+ expect(result.failedCount).toBe(0);
414
+
415
+ // Should not have polled CloudFormation
416
+ expect(mockCFRepo.getStackEvents).not.toHaveBeenCalled();
417
+ });
418
+ });
419
+ });
@@ -0,0 +1,248 @@
1
+ /**
2
+ * HealthScoreCalculator Domain Service
3
+ *
4
+ * Calculates health scores (0-100) based on percentage-based penalties
5
+ * weighted by resource criticality.
6
+ *
7
+ * Resource Criticality:
8
+ * - Critical: Lambda, RDS, DynamoDB (affect application functionality)
9
+ * - Infrastructure: VPC, Subnet, SecurityGroup, KMS, etc.
10
+ *
11
+ * Percentage-Based Penalties (max 100 points):
12
+ * - Critical issues (orphaned, missing): up to 50 points (% of total resources)
13
+ * - Functional drift: up to 30 points (% of critical resources drifted)
14
+ * - Infrastructure drift: up to 20 points (% of infrastructure resources drifted)
15
+ *
16
+ * Example: 16/16 Lambdas drifted = 100% functional drift = 30 penalty → 70/100 score
17
+ */
18
+
19
+ const HealthScore = require('../value-objects/health-score');
20
+
21
+ class HealthScoreCalculator {
22
+ /**
23
+ * Critical resource types that affect application functionality
24
+ * @private
25
+ */
26
+ static CRITICAL_RESOURCE_TYPES = [
27
+ 'AWS::Lambda::Function',
28
+ 'AWS::RDS::DBCluster',
29
+ 'AWS::RDS::DBInstance',
30
+ 'AWS::DynamoDB::Table',
31
+ ];
32
+
33
+ /**
34
+ * Maximum penalties for each category (sum = 100)
35
+ * @private
36
+ */
37
+ static MAX_PENALTIES = {
38
+ criticalIssues: 50, // Orphaned resources, missing resources
39
+ functionalDrift: 30, // Drift on critical resources (Lambda, RDS, DynamoDB)
40
+ infrastructureDrift: 20, // Drift on infrastructure resources (VPC, networking, KMS)
41
+ };
42
+
43
+ /**
44
+ * Create a new HealthScoreCalculator
45
+ *
46
+ * @param {Object} [config={}]
47
+ * @param {Object} [config.maxPenalties] - Custom max penalty configuration
48
+ * @param {number} [config.maxPenalties.criticalIssues] - Max penalty for critical issues (default: 50)
49
+ * @param {number} [config.maxPenalties.functionalDrift] - Max penalty for functional drift (default: 30)
50
+ * @param {number} [config.maxPenalties.infrastructureDrift] - Max penalty for infra drift (default: 20)
51
+ */
52
+ constructor(config = {}) {
53
+ this.maxPenalties = {
54
+ ...HealthScoreCalculator.MAX_PENALTIES,
55
+ ...(config.maxPenalties || {}),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Check if resource type is critical (affects functionality)
61
+ * @private
62
+ */
63
+ _isCriticalResourceType(resourceType) {
64
+ return HealthScoreCalculator.CRITICAL_RESOURCE_TYPES.includes(resourceType);
65
+ }
66
+
67
+ /**
68
+ * Calculate health score based on percentage-based penalties
69
+ *
70
+ * @param {Object} params
71
+ * @param {Resource[]} params.resources - Resources in the stack
72
+ * @param {Issue[]} params.issues - Detected issues
73
+ * @returns {HealthScore}
74
+ */
75
+ calculate({ resources, issues }) {
76
+ const startingScore = 100;
77
+
78
+ // Handle empty stack edge case
79
+ if (resources.length === 0) {
80
+ return new HealthScore(startingScore);
81
+ }
82
+
83
+ // Categorize resources by criticality
84
+ const criticalResources = resources.filter((r) =>
85
+ this._isCriticalResourceType(r.resourceType)
86
+ );
87
+ const infraResources = resources.filter(
88
+ (r) => !this._isCriticalResourceType(r.resourceType)
89
+ );
90
+
91
+ // Count issues by category
92
+ const criticalIssues = issues.filter(
93
+ (issue) => issue.type === 'ORPHANED_RESOURCE' || issue.type === 'MISSING_RESOURCE'
94
+ );
95
+ const functionalDriftIssues = issues.filter(
96
+ (issue) =>
97
+ issue.type === 'PROPERTY_MISMATCH' &&
98
+ this._isCriticalResourceType(issue.resourceType)
99
+ );
100
+ const infraDriftIssues = issues.filter(
101
+ (issue) =>
102
+ issue.type === 'PROPERTY_MISMATCH' &&
103
+ !this._isCriticalResourceType(issue.resourceType)
104
+ );
105
+
106
+ // Calculate percentage-based penalties
107
+ let totalPenalty = 0;
108
+
109
+ // 1. Critical issues penalty (up to 50 points)
110
+ if (criticalIssues.length > 0) {
111
+ const criticalImpactPercent = criticalIssues.length / resources.length;
112
+ totalPenalty += criticalImpactPercent * this.maxPenalties.criticalIssues;
113
+ }
114
+
115
+ // 2. Functional drift penalty (up to 30 points)
116
+ if (functionalDriftIssues.length > 0 && criticalResources.length > 0) {
117
+ // Get unique drifted critical resources
118
+ const driftedCriticalResourceIds = new Set(
119
+ functionalDriftIssues.map((issue) => issue.resourceId)
120
+ );
121
+ const functionalDriftPercent =
122
+ driftedCriticalResourceIds.size / criticalResources.length;
123
+ totalPenalty += functionalDriftPercent * this.maxPenalties.functionalDrift;
124
+ }
125
+
126
+ // 3. Infrastructure drift penalty (up to 20 points)
127
+ if (infraDriftIssues.length > 0 && infraResources.length > 0) {
128
+ // Get unique drifted infrastructure resources
129
+ const driftedInfraResourceIds = new Set(
130
+ infraDriftIssues.map((issue) => issue.resourceId)
131
+ );
132
+ const infraDriftPercent = driftedInfraResourceIds.size / infraResources.length;
133
+ totalPenalty += infraDriftPercent * this.maxPenalties.infrastructureDrift;
134
+ }
135
+
136
+ // Calculate final score (capped at 0)
137
+ const finalScore = Math.max(0, Math.round(startingScore - totalPenalty));
138
+
139
+ return new HealthScore(finalScore);
140
+ }
141
+
142
+ /**
143
+ * Explain the score calculation with detailed percentage-based breakdown
144
+ *
145
+ * @param {Object} params
146
+ * @param {Resource[]} params.resources - Resources in the stack
147
+ * @param {Issue[]} params.issues - Detected issues
148
+ * @returns {Object} Explanation with breakdown
149
+ */
150
+ explainScore({ resources, issues }) {
151
+ const startingScore = 100;
152
+
153
+ if (resources.length === 0) {
154
+ return {
155
+ finalScore: startingScore,
156
+ startingScore,
157
+ totalPenalty: 0,
158
+ breakdown: {
159
+ criticalIssues: { count: 0, impactPercent: 0, penalty: 0 },
160
+ functionalDrift: { count: 0, impactPercent: 0, penalty: 0 },
161
+ infrastructureDrift: { count: 0, impactPercent: 0, penalty: 0 },
162
+ },
163
+ };
164
+ }
165
+
166
+ // Categorize resources
167
+ const criticalResources = resources.filter((r) =>
168
+ this._isCriticalResourceType(r.resourceType)
169
+ );
170
+ const infraResources = resources.filter(
171
+ (r) => !this._isCriticalResourceType(r.resourceType)
172
+ );
173
+
174
+ // Count issues by category
175
+ const criticalIssues = issues.filter(
176
+ (issue) => issue.type === 'ORPHANED_RESOURCE' || issue.type === 'MISSING_RESOURCE'
177
+ );
178
+ const functionalDriftIssues = issues.filter(
179
+ (issue) =>
180
+ issue.type === 'PROPERTY_MISMATCH' &&
181
+ this._isCriticalResourceType(issue.resourceType)
182
+ );
183
+ const infraDriftIssues = issues.filter(
184
+ (issue) =>
185
+ issue.type === 'PROPERTY_MISMATCH' &&
186
+ !this._isCriticalResourceType(issue.resourceType)
187
+ );
188
+
189
+ // Calculate penalties
190
+ const breakdown = {
191
+ criticalIssues: {
192
+ count: criticalIssues.length,
193
+ impactPercent: criticalIssues.length / resources.length,
194
+ penalty:
195
+ (criticalIssues.length / resources.length) * this.maxPenalties.criticalIssues,
196
+ },
197
+ functionalDrift: {
198
+ count: functionalDriftIssues.length,
199
+ impactPercent:
200
+ criticalResources.length > 0
201
+ ? new Set(functionalDriftIssues.map((i) => i.resourceId)).size /
202
+ criticalResources.length
203
+ : 0,
204
+ penalty:
205
+ criticalResources.length > 0
206
+ ? (new Set(functionalDriftIssues.map((i) => i.resourceId)).size /
207
+ criticalResources.length) *
208
+ this.maxPenalties.functionalDrift
209
+ : 0,
210
+ },
211
+ infrastructureDrift: {
212
+ count: infraDriftIssues.length,
213
+ impactPercent:
214
+ infraResources.length > 0
215
+ ? new Set(infraDriftIssues.map((i) => i.resourceId)).size /
216
+ infraResources.length
217
+ : 0,
218
+ penalty:
219
+ infraResources.length > 0
220
+ ? (new Set(infraDriftIssues.map((i) => i.resourceId)).size /
221
+ infraResources.length) *
222
+ this.maxPenalties.infrastructureDrift
223
+ : 0,
224
+ },
225
+ };
226
+
227
+ const totalPenalty =
228
+ breakdown.criticalIssues.penalty +
229
+ breakdown.functionalDrift.penalty +
230
+ breakdown.infrastructureDrift.penalty;
231
+
232
+ const finalScore = Math.max(0, Math.round(startingScore - totalPenalty));
233
+
234
+ return {
235
+ finalScore,
236
+ startingScore,
237
+ totalPenalty: Math.round(totalPenalty * 100) / 100, // Round to 2 decimal places
238
+ breakdown,
239
+ resourceCounts: {
240
+ total: resources.length,
241
+ critical: criticalResources.length,
242
+ infrastructure: infraResources.length,
243
+ },
244
+ };
245
+ }
246
+ }
247
+
248
+ module.exports = HealthScoreCalculator;