diff --git a/frontend/src/__tests__/recommendations.test.ts b/frontend/src/__tests__/recommendations.test.ts index 9f6e90c9..ebf066a7 100644 --- a/frontend/src/__tests__/recommendations.test.ts +++ b/frontend/src/__tests__/recommendations.test.ts @@ -5156,6 +5156,22 @@ describe('Issues #225 + #226: cell grouping with savings range and collapse/expa expect(groups.size).toBe(2); }); + test('EC2 recs differing only by platform or tenancy are separate cells; same pair shares one (issue #806)', () => { + const d = { platform: 'Linux/UNIX', tenancy: 'default' }; + const base = mkRec({ details: d }); + expect(groupRecsByCell([base, mkRec({ details: { ...d, platform: 'Windows' } })]).size).toBe(2); + expect(groupRecsByCell([base, mkRec({ details: { ...d, tenancy: 'dedicated' } })]).size).toBe(2); + expect(groupRecsByCell([base, mkRec({ details: { ...d }, term: 3 })]).size).toBe(1); + const zonal = mkRec({ availability_zone: 'us-east-1a', details: d }); + expect(Array.from(groupRecsByCell([zonal]).keys())[0]).toBe('aws||ec2|us-east-1|m5.large||us-east-1a|Linux/UNIX|default'); + }); + + test('non-EC2 recs and EC2 recs without platform/tenancy keep their cell key (issue #806)', () => { + const rds = mkRec({ service: 'rds', details: { platform: 'x', tenancy: 'y' } }); + expect(Array.from(groupRecsByCell([rds]).keys())[0]).toBe('aws||rds|us-east-1|m5.large|'); + expect(Array.from(groupRecsByCell([mkRec({ details: { instance_type: 'm5.large' } }), mkRec({})]).keys())).toEqual(['aws||ec2|us-east-1|m5.large|']); + }); + test('zonal EC2 recs differing only by AZ are separate cells; regional recs are unchanged (issue #792)', () => { const a = mkRec({ availability_zone: 'us-east-1a' }); const b = mkRec({ availability_zone: 'us-east-1b' }); diff --git a/frontend/src/recommendations.ts b/frontend/src/recommendations.ts index d0e81d38..6f44b1be 100644 --- a/frontend/src/recommendations.ts +++ b/frontend/src/recommendations.ts @@ -991,7 +991,24 @@ function cellKey(rec: LocalRecommendation): string { const key = `${rec.provider}|${rec.cloud_account_id ?? ''}|${rec.service}|${rec.region}|${rec.resource_type}|${rec.engine ?? ''}`; // Zonal EC2 recs in different AZs are distinct purchasable cells (issue #792); // the suffix is added only for them so every other cell key is unchanged. - return rec.availability_zone ? `${key}|${rec.availability_zone}` : key; + const zonal = rec.availability_zone ? `${key}|${rec.availability_zone}` : key; + // EC2 recs differing only by platform or tenancy are distinct purchasable cells + // (issue #806). Verbatim values from details, same source as + // samePurchaseVariantIdentity; the pair is added only when either is set. + const { platform, tenancy } = computeIdentity(rec); + return platform || tenancy ? `${zonal}|${platform}|${tenancy}` : zonal; +} + +function computeIdentity(rec: LocalRecommendation): { platform: string; tenancy: string } { + const details = rec.details; + if ((rec.service !== 'ec2' && rec.service !== 'compute') || typeof details !== 'object' || details === null || Array.isArray(details)) { + return { platform: '', tenancy: '' }; + } + const { platform, tenancy } = details as Record; + return { + platform: typeof platform === 'string' ? platform : '', + tenancy: typeof tenancy === 'string' ? tenancy : '', + }; } // issues #225 + #226: cell-grouping helpers.