@friggframework/devtools 2.0.0--canary.461.849e166.0 → 2.0.0--canary.474.aa465e4.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 (32) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/domains/database/aurora-builder.js +234 -57
  3. package/infrastructure/domains/database/aurora-builder.test.js +7 -2
  4. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  5. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  6. package/infrastructure/domains/database/migration-builder.js +256 -215
  7. package/infrastructure/domains/database/migration-builder.test.js +5 -111
  8. package/infrastructure/domains/database/migration-resolver.js +163 -0
  9. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  10. package/infrastructure/domains/integration/integration-builder.js +258 -84
  11. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  12. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  13. package/infrastructure/domains/networking/vpc-builder.js +856 -135
  14. package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
  15. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  16. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  17. package/infrastructure/domains/security/kms-builder.js +179 -22
  18. package/infrastructure/domains/security/kms-resolver.js +96 -0
  19. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  20. package/infrastructure/domains/shared/base-resolver.js +186 -0
  21. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  22. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  23. package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
  24. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  25. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  26. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  27. package/infrastructure/domains/shared/types/index.js +46 -0
  28. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  29. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  30. package/package.json +6 -6
  31. package/infrastructure/REFACTOR.md +0 -532
  32. package/infrastructure/TRANSFORMATION-VISUAL.md +0 -239
@@ -0,0 +1,501 @@
1
+ const VpcResourceResolver = require('./vpc-resolver');
2
+ const { ResourceOwnership } = require('../shared/types');
3
+
4
+ describe('VpcResourceResolver', () => {
5
+ let resolver;
6
+
7
+ beforeEach(() => {
8
+ resolver = new VpcResourceResolver();
9
+ });
10
+
11
+ describe('resolveVpc', () => {
12
+ it('should resolve to EXTERNAL when user specifies external', () => {
13
+ const appDefinition = {
14
+ vpc: {
15
+ ownership: { vpc: 'external' },
16
+ external: { vpcId: 'vpc-external-123' }
17
+ }
18
+ };
19
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
20
+
21
+ const decision = resolver.resolveVpc(appDefinition, discovery);
22
+
23
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
24
+ expect(decision.physicalId).toBe('vpc-external-123');
25
+ expect(decision.reason).toContain('User specified ownership=external');
26
+ });
27
+
28
+ it('should throw when external specified but vpcId missing', () => {
29
+ const appDefinition = {
30
+ vpc: {
31
+ ownership: { vpc: 'external' },
32
+ external: {}
33
+ }
34
+ };
35
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
36
+
37
+ expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
38
+ "ownership='external' for vpcId requires external.vpcId"
39
+ );
40
+ });
41
+
42
+ it('should resolve to STACK when user specifies stack', () => {
43
+ const appDefinition = {
44
+ vpc: {
45
+ ownership: { vpc: 'stack' }
46
+ }
47
+ };
48
+ const discovery = {
49
+ stackManaged: [
50
+ { logicalId: 'FriggVPC', physicalId: 'vpc-stack-123', resourceType: 'AWS::EC2::VPC' }
51
+ ],
52
+ external: [],
53
+ fromCloudFormation: true
54
+ };
55
+
56
+ const decision = resolver.resolveVpc(appDefinition, discovery);
57
+
58
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
59
+ expect(decision.physicalId).toBe('vpc-stack-123');
60
+ expect(decision.reason).toContain('User specified ownership=stack');
61
+ });
62
+
63
+ it('should auto-resolve to STACK when VPC in stack (CRITICAL)', () => {
64
+ const appDefinition = {
65
+ vpc: { ownership: { vpc: 'auto' } }
66
+ };
67
+ const discovery = {
68
+ stackManaged: [
69
+ { logicalId: 'FriggVPC', physicalId: 'vpc-in-stack', resourceType: 'AWS::EC2::VPC' }
70
+ ],
71
+ external: [],
72
+ fromCloudFormation: true
73
+ };
74
+
75
+ const decision = resolver.resolveVpc(appDefinition, discovery);
76
+
77
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
78
+ expect(decision.physicalId).toBe('vpc-in-stack');
79
+ expect(decision.reason).toContain('Found in CloudFormation stack');
80
+ });
81
+
82
+ it('should auto-resolve to EXTERNAL when found externally', () => {
83
+ const appDefinition = {
84
+ vpc: { ownership: { vpc: 'auto' } }
85
+ };
86
+ const discovery = {
87
+ stackManaged: [],
88
+ external: [
89
+ { physicalId: 'vpc-external', resourceType: 'AWS::EC2::VPC', source: 'tag-search' }
90
+ ],
91
+ fromCloudFormation: false
92
+ };
93
+
94
+ const decision = resolver.resolveVpc(appDefinition, discovery);
95
+
96
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
97
+ expect(decision.physicalId).toBe('vpc-external');
98
+ });
99
+
100
+ it('should auto-resolve to STACK when not found (create new)', () => {
101
+ const appDefinition = {
102
+ vpc: { ownership: { vpc: 'auto' } }
103
+ };
104
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
105
+
106
+ const decision = resolver.resolveVpc(appDefinition, discovery);
107
+
108
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
109
+ expect(decision.physicalId).toBeUndefined();
110
+ expect(decision.reason).toContain('No existing resource found');
111
+ });
112
+ });
113
+
114
+ describe('resolveSecurityGroup', () => {
115
+ it('should resolve to EXTERNAL with user-provided IDs', () => {
116
+ const appDefinition = {
117
+ vpc: {
118
+ ownership: { securityGroup: 'external' },
119
+ external: { securityGroupIds: ['sg-1', 'sg-2'] }
120
+ }
121
+ };
122
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
123
+
124
+ const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
125
+
126
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
127
+ expect(decision.physicalIds).toEqual(['sg-1', 'sg-2']);
128
+ });
129
+
130
+ it('should auto-resolve to STACK when FriggLambdaSecurityGroup in stack', () => {
131
+ const appDefinition = { vpc: { ownership: { securityGroup: 'auto' } } };
132
+ const discovery = {
133
+ stackManaged: [
134
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' }
135
+ ],
136
+ external: [],
137
+ fromCloudFormation: true
138
+ };
139
+
140
+ const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
141
+
142
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
143
+ expect(decision.physicalId).toBe('sg-069629001ade41c9a');
144
+ expect(decision.reason).toContain('Found FriggLambdaSecurityGroup in CloudFormation stack');
145
+ });
146
+ });
147
+
148
+ describe('resolveSubnets', () => {
149
+ it('should resolve to EXTERNAL with user-provided subnet IDs', () => {
150
+ const appDefinition = {
151
+ vpc: {
152
+ ownership: { subnets: 'external' },
153
+ external: { subnetIds: ['subnet-1', 'subnet-2', 'subnet-3'] }
154
+ }
155
+ };
156
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
157
+
158
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
159
+
160
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
161
+ expect(decision.physicalIds).toEqual(['subnet-1', 'subnet-2', 'subnet-3']);
162
+ });
163
+
164
+ it('should resolve to STACK when subnets found in stack', () => {
165
+ const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
166
+ const discovery = {
167
+ stackManaged: [
168
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-a', resourceType: 'AWS::EC2::Subnet' },
169
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-b', resourceType: 'AWS::EC2::Subnet' }
170
+ ],
171
+ external: [],
172
+ fromCloudFormation: true
173
+ };
174
+
175
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
176
+
177
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
178
+ expect(decision.physicalIds).toEqual(['subnet-a', 'subnet-b']);
179
+ expect(decision.metadata.subnet1).toBe('subnet-a');
180
+ expect(decision.metadata.subnet2).toBe('subnet-b');
181
+ });
182
+
183
+ it('should resolve to EXTERNAL when found externally', () => {
184
+ const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
185
+ const discovery = {
186
+ stackManaged: [],
187
+ external: [
188
+ { physicalId: 'subnet-ext-1', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
189
+ { physicalId: 'subnet-ext-2', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
190
+ { physicalId: 'subnet-ext-3', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' }
191
+ ],
192
+ fromCloudFormation: false
193
+ };
194
+
195
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
196
+
197
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
198
+ expect(decision.physicalIds).toHaveLength(2); // Takes first 2
199
+ expect(decision.physicalIds).toEqual(['subnet-ext-1', 'subnet-ext-2']);
200
+ });
201
+
202
+ it('should resolve to STACK when no subnets found (create new)', () => {
203
+ const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
204
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
205
+
206
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
207
+
208
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
209
+ expect(decision.physicalId).toBeNull();
210
+ expect(decision.reason).toContain('No existing subnets found');
211
+ });
212
+ });
213
+
214
+ describe('resolveNatGateway', () => {
215
+ it('should return null decision when NAT disabled', () => {
216
+ const appDefinition = {
217
+ vpc: {
218
+ ownership: { natGateway: 'auto' },
219
+ config: { natGateway: { enable: false } }
220
+ }
221
+ };
222
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
223
+
224
+ const decision = resolver.resolveNatGateway(appDefinition, discovery);
225
+
226
+ expect(decision.ownership).toBeNull();
227
+ expect(decision.reason).toContain('NAT Gateway disabled');
228
+ });
229
+
230
+ it('should resolve to EXTERNAL with user-provided ID', () => {
231
+ const appDefinition = {
232
+ vpc: {
233
+ ownership: { natGateway: 'external' },
234
+ external: { natGatewayId: 'nat-external-123' }
235
+ }
236
+ };
237
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
238
+
239
+ const decision = resolver.resolveNatGateway(appDefinition, discovery);
240
+
241
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
242
+ expect(decision.physicalId).toBe('nat-external-123');
243
+ });
244
+
245
+ it('should auto-resolve to STACK when found in stack', () => {
246
+ const appDefinition = { vpc: { ownership: { natGateway: 'auto' } } };
247
+ const discovery = {
248
+ stackManaged: [
249
+ { logicalId: 'FriggNatGateway', physicalId: 'nat-stack-123', resourceType: 'AWS::EC2::NatGateway' }
250
+ ],
251
+ external: [],
252
+ fromCloudFormation: true
253
+ };
254
+
255
+ const decision = resolver.resolveNatGateway(appDefinition, discovery);
256
+
257
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
258
+ expect(decision.physicalId).toBe('nat-stack-123');
259
+ });
260
+ });
261
+
262
+ describe('resolveVpcEndpoints', () => {
263
+ it('should return null decisions when endpoints disabled', () => {
264
+ const appDefinition = {
265
+ vpc: {
266
+ ownership: { vpcEndpoints: 'auto' },
267
+ config: { enableVpcEndpoints: false }
268
+ }
269
+ };
270
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
271
+
272
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
273
+
274
+ expect(decisions.s3.ownership).toBeNull();
275
+ expect(decisions.dynamodb.ownership).toBeNull();
276
+ expect(decisions.kms.ownership).toBeNull();
277
+ expect(decisions.secretsManager.ownership).toBeNull();
278
+ expect(decisions.sqs.ownership).toBeNull();
279
+ });
280
+
281
+ it('should resolve to EXTERNAL with user-provided endpoint IDs', () => {
282
+ const appDefinition = {
283
+ vpc: {
284
+ ownership: { vpcEndpoints: 'external' },
285
+ external: {
286
+ vpcEndpointIds: {
287
+ s3: 'vpce-s3-123',
288
+ dynamodb: 'vpce-ddb-456'
289
+ }
290
+ }
291
+ }
292
+ };
293
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
294
+
295
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
296
+
297
+ expect(decisions.s3.ownership).toBe(ResourceOwnership.EXTERNAL);
298
+ expect(decisions.s3.physicalId).toBe('vpce-s3-123');
299
+ expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.EXTERNAL);
300
+ expect(decisions.dynamodb.physicalId).toBe('vpce-ddb-456');
301
+ expect(decisions.kms.ownership).toBeNull(); // Not provided
302
+ });
303
+
304
+ it('should auto-resolve to STACK when endpoints found in stack', () => {
305
+ const appDefinition = { vpc: { ownership: { vpcEndpoints: 'auto' } } };
306
+ const discovery = {
307
+ stackManaged: [
308
+ { logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' },
309
+ { logicalId: 'FriggDynamoDBVPCEndpoint', physicalId: 'vpce-ddb-stack', resourceType: 'AWS::EC2::VPCEndpoint' }
310
+ ],
311
+ external: [],
312
+ fromCloudFormation: true
313
+ };
314
+
315
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
316
+
317
+ expect(decisions.s3.ownership).toBe(ResourceOwnership.STACK);
318
+ expect(decisions.s3.physicalId).toBe('vpce-s3-stack');
319
+ expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.STACK);
320
+ expect(decisions.dynamodb.physicalId).toBe('vpce-ddb-stack');
321
+ });
322
+
323
+ it('should auto-resolve mixed: some in stack, some new', () => {
324
+ const appDefinition = {
325
+ vpc: { ownership: { vpcEndpoints: 'auto' } },
326
+ encryption: { fieldLevelEncryptionMethod: 'kms' } // Enable KMS endpoint
327
+ };
328
+ const discovery = {
329
+ stackManaged: [
330
+ { logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' }
331
+ ],
332
+ external: [],
333
+ fromCloudFormation: true
334
+ };
335
+
336
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
337
+
338
+ expect(decisions.s3.ownership).toBe(ResourceOwnership.STACK);
339
+ expect(decisions.s3.physicalId).toBe('vpce-s3-stack');
340
+
341
+ // Others not in stack - should create new
342
+ expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.STACK);
343
+ expect(decisions.dynamodb.physicalId).toBeUndefined();
344
+ expect(decisions.kms.ownership).toBe(ResourceOwnership.STACK);
345
+ expect(decisions.secretsManager.ownership).toBe(ResourceOwnership.STACK);
346
+ expect(decisions.sqs.ownership).toBe(ResourceOwnership.STACK);
347
+ });
348
+ });
349
+
350
+ describe('resolveAll', () => {
351
+ it('should resolve all VPC resources at once', () => {
352
+ const appDefinition = {
353
+ vpc: {
354
+ ownership: {
355
+ vpc: 'auto',
356
+ securityGroup: 'auto',
357
+ subnets: 'auto',
358
+ natGateway: 'auto',
359
+ vpcEndpoints: 'auto'
360
+ }
361
+ }
362
+ };
363
+ const discovery = {
364
+ stackManaged: [
365
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
366
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' },
367
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
368
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' },
369
+ { logicalId: 'FriggNatGateway', physicalId: 'nat-789', resourceType: 'AWS::EC2::NatGateway' },
370
+ { logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3', resourceType: 'AWS::EC2::VPCEndpoint' }
371
+ ],
372
+ external: [],
373
+ fromCloudFormation: true
374
+ };
375
+
376
+ const decisions = resolver.resolveAll(appDefinition, discovery);
377
+
378
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
379
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
380
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
381
+ expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK);
382
+ expect(decisions.vpcEndpoints.s3.ownership).toBe(ResourceOwnership.STACK);
383
+ });
384
+
385
+ it('should handle mixed ownership scenarios', () => {
386
+ const appDefinition = {
387
+ vpc: {
388
+ ownership: {
389
+ vpc: 'external',
390
+ securityGroup: 'stack',
391
+ subnets: 'stack',
392
+ natGateway: 'auto',
393
+ vpcEndpoints: 'auto'
394
+ },
395
+ external: {
396
+ vpcId: 'vpc-shared-production'
397
+ }
398
+ }
399
+ };
400
+ const discovery = {
401
+ stackManaged: [
402
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stack', resourceType: 'AWS::EC2::SecurityGroup' },
403
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
404
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
405
+ ],
406
+ external: [],
407
+ fromCloudFormation: true
408
+ };
409
+
410
+ const decisions = resolver.resolveAll(appDefinition, discovery);
411
+
412
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.EXTERNAL);
413
+ expect(decisions.vpc.physicalId).toBe('vpc-shared-production');
414
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
415
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
416
+ expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK); // Not found, create new
417
+ });
418
+ });
419
+
420
+ describe('real-world scenarios', () => {
421
+ it('scenario: fresh deploy, no resources exist', () => {
422
+ const appDefinition = {
423
+ vpc: { enable: true, ownership: {} }
424
+ };
425
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
426
+
427
+ const decisions = resolver.resolveAll(appDefinition, discovery);
428
+
429
+ // All should be STACK (create new)
430
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
431
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
432
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
433
+ expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK);
434
+ });
435
+
436
+ it('scenario: redeploy existing stack (the original bug case)', () => {
437
+ const appDefinition = {
438
+ vpc: { enable: true, ownership: {} }
439
+ };
440
+ const discovery = {
441
+ stackManaged: [
442
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
443
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' },
444
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
445
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
446
+ ],
447
+ external: [],
448
+ fromCloudFormation: true,
449
+ stackName: 'create-frigg-app-production'
450
+ };
451
+
452
+ const decisions = resolver.resolveAll(appDefinition, discovery);
453
+
454
+ // CRITICAL: All resources in stack must get STACK ownership
455
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
456
+ expect(decisions.vpc.physicalId).toBe('vpc-123');
457
+
458
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
459
+ expect(decisions.securityGroup.physicalId).toBe('sg-069629001ade41c9a');
460
+ expect(decisions.securityGroup.reason).toContain('Found FriggLambdaSecurityGroup in CloudFormation stack');
461
+
462
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
463
+ expect(decisions.subnets.physicalIds).toEqual(['subnet-1', 'subnet-2']);
464
+ });
465
+
466
+ it('scenario: use shared VPC with stack-managed resources', () => {
467
+ const appDefinition = {
468
+ vpc: {
469
+ enable: true,
470
+ ownership: {
471
+ vpc: 'external',
472
+ securityGroup: 'auto',
473
+ subnets: 'auto'
474
+ },
475
+ external: {
476
+ vpcId: 'vpc-shared-across-stages'
477
+ }
478
+ }
479
+ };
480
+ const discovery = {
481
+ stackManaged: [
482
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stage-specific', resourceType: 'AWS::EC2::SecurityGroup' },
483
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
484
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
485
+ ],
486
+ external: [],
487
+ fromCloudFormation: true
488
+ };
489
+
490
+ const decisions = resolver.resolveAll(appDefinition, discovery);
491
+
492
+ // VPC is external
493
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.EXTERNAL);
494
+ expect(decisions.vpc.physicalId).toBe('vpc-shared-across-stages');
495
+
496
+ // But security group and subnets are stack-managed
497
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
498
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
499
+ });
500
+ });
501
+ });