diff --git a/src/support/grant-suggestions-handler.js b/src/support/grant-suggestions-handler.js index 7d19064a57..ec2f28e140 100644 --- a/src/support/grant-suggestions-handler.js +++ b/src/support/grant-suggestions-handler.js @@ -143,6 +143,19 @@ const OPPORTUNITY_STRATEGIES = { return idA.localeCompare(idB); }, }, + // Decorative-image suggestions (data.recommendations[].isDecorative) are excluded + // from grant selection entirely, since they need no alt text fix and shouldn't + // consume one of the customer's limited PLG token slots. Remaining suggestions + // fall back to the default sort (rank ascending, then id ascending). + 'alt-text': { + groupFn: (suggestions) => suggestions + .filter((s) => { + const data = typeof s?.getData === 'function' ? s.getData() : s?.data; + const recommendations = data?.recommendations ?? []; + return !recommendations.some((r) => r?.isDecorative === true); + }) + .map((s) => createGroup([s])), + }, }; /** diff --git a/test/support/grant-suggestions-handler.test.js b/test/support/grant-suggestions-handler.test.js index 30e7150570..7e6f79b4d5 100644 --- a/test/support/grant-suggestions-handler.test.js +++ b/test/support/grant-suggestions-handler.test.js @@ -322,6 +322,67 @@ describe('grant-suggestions-handler', () => { const ids = groups.flatMap((g) => g.items.map((s) => s.getId())); expect(ids).to.include.members(['id-1', 'id-2']); }); + + it('alt-text: excludes suggestions with a decorative recommendation', () => { + const mk = (id, rank, isDecorative) => ({ + getId: () => id, + getRank: () => rank, + getData: () => ({ recommendations: [{ isDecorative }] }), + }); + const s1 = mk('id-1', 1, false); + const s2 = mk('id-2', 2, true); + const groups = getTopSuggestions([s1, s2], 'alt-text'); + expect(groups).to.have.lengthOf(1); + expect(groups[0].items[0]).to.equal(s1); + }); + + it('alt-text: excludes a suggestion when any of its recommendations is decorative', () => { + const s1 = { + getId: () => 'id-1', + getRank: () => 1, + getData: () => ({ recommendations: [{ isDecorative: false }, { isDecorative: true }] }), + }; + const groups = getTopSuggestions([s1], 'alt-text'); + expect(groups).to.have.lengthOf(0); + }); + + it('alt-text: keeps suggestions with no decorative recommendations', () => { + const s1 = { getId: () => 'id-1', getRank: () => 1, getData: () => ({ recommendations: [{ isDecorative: false }] }) }; + const s2 = { getId: () => 'id-2', getRank: () => 2, getData: () => ({ recommendations: [{ isDecorative: false }] }) }; + const groups = getTopSuggestions([s1, s2], 'alt-text'); + expect(groups).to.have.lengthOf(2); + }); + + it('alt-text: treats missing or empty recommendations as not decorative', () => { + const s1 = { getId: () => 'id-1', getRank: () => 1, getData: () => ({ recommendations: [] }) }; + const s2 = { getId: () => 'id-2', getRank: () => 2, getData: () => ({}) }; + const groups = getTopSuggestions([s1, s2], 'alt-text'); + expect(groups).to.have.lengthOf(2); + }); + + it('alt-text: works with plain object data (no getData())', () => { + const s1 = { id: 'id-1', rank: 1, data: { recommendations: [{ isDecorative: true }] } }; + const s2 = { id: 'id-2', rank: 2, data: { recommendations: [{ isDecorative: false }] } }; + const groups = getTopSuggestions([s1, s2], 'alt-text'); + expect(groups).to.have.lengthOf(1); + expect(groups[0].items[0]).to.equal(s2); + }); + + it('alt-text: sorts kept suggestions by rank ascending then id (default sort)', () => { + const mk = (id, rank) => ({ + getId: () => id, + getRank: () => rank, + getData: () => ({ recommendations: [{ isDecorative: false }] }), + }); + const s1 = mk('id-b', 10); + const s2 = mk('id-a', 5); + const s3 = mk('id-c', 10); + const groups = getTopSuggestions([s1, s2, s3], 'alt-text'); + expect(groups).to.have.lengthOf(3); + expect(groups[0].items[0]).to.equal(s2); // rank 5 first + expect(groups[1].items[0]).to.equal(s1); // rank 10, id-b before id-c + expect(groups[2].items[0]).to.equal(s3); // rank 10, id-c + }); }); describe('grantSuggestionsForOpportunity', () => {