diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/drift/drift-formatter.ts b/packages/@aws-cdk/toolkit-lib/lib/api/drift/drift-formatter.ts index c6d222e93..56a874f97 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/drift/drift-formatter.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/drift/drift-formatter.ts @@ -92,6 +92,20 @@ export class DriftFormatter { }); } + /** + * Resources that were not checked for drift: either CloudFormation returned + * no drift record for them at all, or it returned one with an UNKNOWN + * status (e.g. due to missing permissions or lack of drift-detection + * support for that resource type). + */ + private getUncheckedResources(): string[] { + const drifts = this.resourceDriftResults; + return Array.from(this.allStackResources.keys()).filter((logicalId) => { + const drift = drifts.find((d) => d.LogicalResourceId === logicalId); + return !drift || drift.StackResourceDriftStatus === StackResourceDriftStatus.UNKNOWN; + }); + } + /** * Format the stack drift detection results */ @@ -110,7 +124,7 @@ export class DriftFormatter { const finalResult = chalk.green('No drift detected\n'); return { numResourcesWithDrift: 0, - numResourcesUnchecked: this.allStackResources.size - this.resourceDriftResults.length, + numResourcesUnchecked: this.getUncheckedResources().length, stackHeader, unchecked: formatterOutput.unchecked, summary: finalResult, @@ -120,7 +134,7 @@ export class DriftFormatter { const finalResult = chalk.yellow(`\n${actualDrifts.length} resource${actualDrifts.length === 1 ? '' : 's'} ${actualDrifts.length === 1 ? 'has' : 'have'} drifted from their expected configuration\n`); return { numResourcesWithDrift: actualDrifts.length, - numResourcesUnchecked: this.allStackResources.size - this.resourceDriftResults.length, + numResourcesUnchecked: this.getUncheckedResources().length, stackHeader, unchanged: formatterOutput.unchanged, unchecked: formatterOutput.unchecked, @@ -161,10 +175,7 @@ export class DriftFormatter { } // Process all unchecked and unknown resources - const uncheckedResources = Array.from(this.allStackResources.keys()).filter((logicalId) => { - const drift = drifts.find((d) => d.LogicalResourceId === logicalId); - return !drift || drift.StackResourceDriftStatus === StackResourceDriftStatus.UNKNOWN; - }); + const uncheckedResources = this.getUncheckedResources(); if (uncheckedResources.length > 0) { unchecked = this.printSectionHeader('Unchecked Resources'); for (const logicalId of uncheckedResources) { diff --git a/packages/@aws-cdk/toolkit-lib/test/api/drift/drift.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/drift/drift.test.ts index a93a1e3ce..3c81e0b6c 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/drift/drift.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/drift/drift.test.ts @@ -806,6 +806,14 @@ describe('formatStackDrift', () => { // UNKNOWN resources should be treated as unchecked, not as drift expect(result.unchecked).toContain('AWS::IAM::Role'); expect(result.unchecked).toContain('Resource2'); + + // Regression test: numResourcesUnchecked used to be computed as + // `allStackResources.size - resourceDriftResults.length`, which only + // counts resources CloudFormation returned no drift record for at all. + // Resource2 has a drift record (status UNKNOWN), so that formula + // reported 0 unchecked here even though `unchecked` (above) lists it - + // a self-contradictory summary vs. detail output. + expect(result.numResourcesUnchecked).toBe(1); }); test('formatting with only UNKNOWN drift status', () => { @@ -868,6 +876,10 @@ describe('formatStackDrift', () => { expect(result.unchecked).toContain('Resource1'); expect(result.unchecked).toContain('AWS::IAM::Role'); expect(result.unchecked).toContain('Resource2'); + + // Both resources have UNKNOWN-status drift records (not missing entirely), + // so the naive size-minus-length formula previously reported 0 here. + expect(result.numResourcesUnchecked).toBe(2); }); test('filters out AWS::CDK::Metadata resources from drift count', () => {