diff --git a/package.json b/package.json index b7f201dc5..86f3b4402 100755 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "start": "nodemon", "test": "c8 mocha -i -g 'Post-Deploy' --spec=test/**/*.test.js", "test-postdeploy": "mocha -g 'Post-Deploy' --spec=test/**/*.test.js", - "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint": "node --max-old-space-size=10240 ./node_modules/.bin/eslint .", + "lint:fix": "node --max-old-space-size=10240 ./node_modules/.bin/eslint . --fix", "logs": "aws logs tail /aws/lambda/spacecat-services--audit-worker", "semantic-release": "semantic-release", "semantic-release-dry": "semantic-release --dry-run --no-ci --branches $CI_BRANCH", diff --git a/src/cwv/handler.js b/src/cwv/handler.js index dd6b4bafb..c15a2cee3 100644 --- a/src/cwv/handler.js +++ b/src/cwv/handler.js @@ -84,7 +84,7 @@ export async function opportunityAndSuggestions(auditUrl, auditData, context, si // Send SQS message for Mystique auto-suggest if enabled and opportunity needs suggestions if (await needsAutoSuggest(context, opportunity, site)) { - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, auditData.auditResult.cwv); } } diff --git a/src/cwv/utils.js b/src/cwv/utils.js index a05b24351..ee892d3d4 100644 --- a/src/cwv/utils.js +++ b/src/cwv/utils.js @@ -78,9 +78,10 @@ export async function needsAutoSuggest(context, opportunity, site) { * @param {Object} context - Context object containing log, sqs, env * @param {Object} opportunity - Opportunity object with siteId, auditId, opportunityId, and data * @param {Object} site - Site object with getBaseURL() and getDeliveryType() methods + * @param {Array} cwvEntries - Array of CWV data entries from audit result * @throws {Error} When SQS message sending fails */ -export async function sendSQSMessageForAutoSuggest(context, opportunity, site) { +export async function sendSQSMessageForAutoSuggest(context, opportunity, site, cwvEntries = []) { const { log, sqs, env, } = context; @@ -93,21 +94,46 @@ export async function sendSQSMessageForAutoSuggest(context, opportunity, site) { log.info(`Received CWV opportunity for auto-suggest - siteId: ${siteId}, opportunityId: ${opportunityId}`); - const sqsMessage = { - type: CWV_AUTO_SUGGEST_MESSAGE_TYPE, - siteId: opptyData.siteId, - auditId: opptyData.auditId, - deliveryType: site ? site.getDeliveryType() : 'aem_cs', - time: new Date().toISOString(), - data: { - page: site ? site.getBaseURL() : '', - opportunityId, - }, - }; - - // eslint-disable-next-line no-await-in-loop - await sqs.sendMessage(env.QUEUE_SPACECAT_TO_MYSTIQUE, sqsMessage); - log.info(`CWV opportunity sent to Mystique for auto-suggest - siteId: ${siteId}, opportunityId: ${opportunityId}`); + // Filter for URL-type entries only (skip groups) and ensure suggestions exist for them. + const suggestions = await opportunity.getSuggestions(); + const urlEntries = cwvEntries.filter((entry) => entry.type === 'url'); + + if (urlEntries.length === 0) { + log.info('No new URL entries to send for CWV auto-suggest'); + return; + } + + log.info(`Sending ${urlEntries.length} URL(s) to Mystique for CWV analysis`); + + // Send one message per URL + for (const entry of urlEntries) { + // Find the corresponding suggestion for the current URL entry to check its status. + // This prevents re-sending analysis requests for suggestions that already have guidance + // or are not in a 'NEW' state. + const suggestion = suggestions.find((s) => s.getData().url === entry.url); + const suggestionData = suggestion?.getData(); + const issues = suggestionData?.issues || []; + const hasGuidance = issues.some((issue) => issue.value && issue.value.trim()); + + if (suggestion?.getStatus() === 'NEW' && !hasGuidance) { + const sqsMessage = { + type: CWV_AUTO_SUGGEST_MESSAGE_TYPE, + siteId: opptyData.siteId, + auditId: opptyData.auditId, + deliveryType: site ? site.getDeliveryType() : 'aem_cs', + time: new Date().toISOString(), + data: { + page: entry.url, + opportunity_id: opportunityId, + }, + }; + // eslint-disable-next-line no-await-in-loop + await sqs.sendMessage(env.QUEUE_SPACECAT_TO_MYSTIQUE, sqsMessage); + log.info(`Sent URL to Mystique: ${entry.url}`); + } + } + + log.info(`CWV opportunity sent to Mystique for auto-suggest - siteId: ${siteId}, opportunityId: ${opportunityId}, URLs: ${urlEntries.length}`); } } catch (error) { const siteId = opportunity?.siteId || 'unknown'; diff --git a/test/audits/cwv.test.js b/test/audits/cwv.test.js index 56cb62f10..1f56fe97e 100644 --- a/test/audits/cwv.test.js +++ b/test/audits/cwv.test.js @@ -31,7 +31,7 @@ use(chaiAsPromised); const sandbox = sinon.createSandbox(); -const baseURL = 'https://spacecat.com'; +const baseURL = 'http://spacecat.com'; const auditUrl = 'www.spacecat.com'; const DOMAIN_REQUEST_DEFAULT_PARAMS = { domain: auditUrl, @@ -49,6 +49,7 @@ describe('CWVRunner Tests', () => { getBaseURL: sandbox.stub().returns(baseURL), getConfig: () => siteConfig, getDeliveryType: sandbox.stub().returns('aem_cs'), + isAutoSuggestEnabled: sandbox.stub().returns(true), hasProductEntitlement: sandbox.stub().resolves(true), }; @@ -69,11 +70,31 @@ describe('CWVRunner Tests', () => { isHandlerEnabledForSite: () => true, }), }, + Opportunity: { + allBySiteIdAndStatus: sandbox.stub(), + create: sandbox.stub(), + find: sandbox.stub().resolves([]), + update: sandbox.stub().resolves({}), + }, + Suggestion: { + bulkUpdateStatus: sandbox.stub(), + }, + Markdown: { + fromS3: sandbox.stub(), + }, + getAuditForSite: sandbox.stub(), + }, + sqs: { + sendMessage: sandbox.stub().resolves(), + }, + env: { + QUEUE_SPACECAT_TO_MYSTIQUE: 'test-queue', }, - env: {}, log: { debug: sinon.stub(), info: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub(), }, }; @@ -124,6 +145,8 @@ describe('CWVRunner Tests', () => { context.dataAccess.Opportunity = { allBySiteIdAndStatus: sandbox.stub(), create: sandbox.stub(), + find: sandbox.stub().resolves([]), + update: sandbox.stub().resolves({}), }; context.dataAccess.Suggestion = { @@ -163,6 +186,7 @@ describe('CWVRunner Tests', () => { siteId: 'site-id', auditId: 'audit-id', opportunityId: 'oppty-id', + syncSuggestions: sandbox.stub().resolves(), }; auditData = { @@ -291,90 +315,74 @@ describe('CWVRunner Tests', () => { }); it('calls sendSQSMessageForAutoSuggest when suggestions have no guidance', async () => { - // Mock suggestions without guidance (empty issues array) const mockSuggestions = [ - { getData: () => ({ type: 'url', url: 'test1', issues: [] }), getStatus: () => 'NEW' }, - { getData: () => ({ type: 'url', url: 'test2', issues: [] }), getStatus: () => 'NEW' } + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/developer/block-collection', issues: [] }), getStatus: () => 'NEW', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/docs/', issues: [] }), getStatus: () => 'NEW', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/tools/rum/explorer.html', issues: [] }), getStatus: () => 'NEW', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, ]; - - // Setup opportunity with mock suggestions before the function call - oppty.getSuggestions = sandbox.stub().resolves(mockSuggestions); - - context.dataAccess.Opportunity.allBySiteIdAndStatus.resolves([]); + oppty.getSuggestions.resolves(mockSuggestions); + context.dataAccess.Opportunity.allBySiteIdAndStatus.resolves([oppty]); context.dataAccess.Opportunity.create.resolves(oppty); sinon.stub(GoogleClient, 'createFrom').resolves({}); await opportunityAndSuggestions(auditUrl, auditData, context, site); - // Verify that SQS sendMessage was called - expect(context.sqs.sendMessage).to.have.been.calledOnce; - const message = context.sqs.sendMessage.firstCall.args[1]; - expect(message.type).to.equal('guidance:cwv-analysis'); - expect(message.siteId).to.equal('site-id'); + const urlEntries = auditData.auditResult.cwv.filter((entry) => entry.type === 'url'); + expect(context.sqs.sendMessage.callCount).to.equal(urlEntries.length); }); it('does not call sendSQSMessageForAutoSuggest when all suggestions have guidance', async () => { - // Mock suggestions with existing guidance const mockSuggestions = [ - { - getData: () => ({ - type: 'url', - url: 'test1', - issues: [ - { type: 'lcp', value: '# LCP Optimization\n\nYour LCP is too slow...' } - ] - }), - getStatus: () => 'NEW' - } + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/developer/block-collection', issues: ['issue1'] }), getStatus: () => 'EXISTING', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/docs/', issues: ['issue2'] }), getStatus: () => 'EXISTING', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/tools/rum/explorer.html', issues: ['issue3'] }), getStatus: () => 'EXISTING', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, ]; - - // Setup opportunity with mock suggestions before the function call - oppty.getSuggestions = sandbox.stub().resolves(mockSuggestions); - - context.dataAccess.Opportunity.allBySiteIdAndStatus.resolves([]); + oppty.getSuggestions.resolves(mockSuggestions); + context.dataAccess.Opportunity.allBySiteIdAndStatus.resolves([oppty]); + context.dataAccess.Opportunity.create.resolves(oppty); sinon.stub(GoogleClient, 'createFrom').resolves({}); await opportunityAndSuggestions(auditUrl, auditData, context, site); - // Verify that SQS sendMessage was NOT called expect(context.sqs.sendMessage).to.not.have.been.called; }); it('calls sendSQSMessageForAutoSuggest when some suggestions have guidance and some do not', async () => { - // Mock mixed suggestions - some with guidance, some without + const mockSuggestions = [ - { - getData: () => ({ - type: 'url', - url: 'test1', - issues: [ - { type: 'lcp', value: '# LCP Optimization...' } - ] - }), - getStatus: () => 'NEW' + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/developer/block-collection', issues: ['issue1'] }), getStatus: () => 'NEW', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/docs/', issues: [] }), getStatus: () => 'NEW', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), + }, + { + getData: () => ({ type: 'url', url: 'https://www.aem.live/tools/rum/explorer.html', issues: [] }), getStatus: () => 'NEW', setData: sandbox.stub(), setStatus: sandbox.stub(), setUpdatedBy: sandbox.stub(), save: sandbox.stub(), }, - { - getData: () => ({ - type: 'url', - url: 'test2', - issues: [] // No guidance (empty issues array) - }), - getStatus: () => 'NEW' - } ]; - - // Setup opportunity with mock suggestions before the function call - oppty.getSuggestions = sandbox.stub().resolves(mockSuggestions); - - context.dataAccess.Opportunity.allBySiteIdAndStatus.resolves([]); + oppty.getSuggestions.resolves(mockSuggestions); + context.dataAccess.Opportunity.allBySiteIdAndStatus.resolves([oppty]); + context.dataAccess.Opportunity.create.resolves(oppty); sinon.stub(GoogleClient, 'createFrom').resolves({}); await opportunityAndSuggestions(auditUrl, auditData, context, site); - // Verify that SQS sendMessage was called (because at least one suggestion needs guidance) - expect(context.sqs.sendMessage).to.have.been.calledOnce; + const urlEntries = auditData.auditResult.cwv.filter((entry) => entry.type === 'url'); + expect(context.sqs.sendMessage.callCount).to.equal(urlEntries.length); }); }); }); diff --git a/test/audits/cwv/utils.test.js b/test/audits/cwv/utils.test.js index e4e07e0c9..43dd31883 100644 --- a/test/audits/cwv/utils.test.js +++ b/test/audits/cwv/utils.test.js @@ -55,9 +55,13 @@ describe('sendSQSMessageForAutoSuggest', () => { opportunityId: 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect(sqsStub.calledOnce).to.be.true; const message = sqsStub.firstCall.args[1]; @@ -68,8 +72,8 @@ describe('sendSQSMessageForAutoSuggest', () => { expect(message.deliveryType).to.equal('aem_cs'); expect(message.time).to.be.a('string'); - expect(message.data.page).to.equal('https://example.com'); - expect(message.data.opportunityId).to.equal('oppty-789'); + expect(message.data.page).to.equal('https://example.com/page1'); + expect(message.data.opportunity_id).to.equal('oppty-789'); }); it('should handle opportunity without siteId', async () => { @@ -78,9 +82,13 @@ describe('sendSQSMessageForAutoSuggest', () => { opportunityId: 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect(sqsStub.calledOnce).to.be.true; const message = sqsStub.firstCall.args[1]; @@ -93,9 +101,13 @@ describe('sendSQSMessageForAutoSuggest', () => { opportunityId: 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect(sqsStub.calledOnce).to.be.true; const message = sqsStub.firstCall.args[1]; @@ -108,13 +120,17 @@ describe('sendSQSMessageForAutoSuggest', () => { auditId: 'audit-456', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect(sqsStub.calledOnce).to.be.true; const message = sqsStub.firstCall.args[1]; - expect(message.data.opportunityId).to.equal(''); + expect(message.data.opportunity_id).to.equal(''); }); it('should handle opportunity without data', async () => { @@ -124,14 +140,18 @@ describe('sendSQSMessageForAutoSuggest', () => { opportunityId: 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect(sqsStub.calledOnce).to.be.true; const message = sqsStub.firstCall.args[1]; - expect(message.data.page).to.equal('https://example.com'); - expect(message.data.opportunityId).to.equal('oppty-789'); + expect(message.data.page).to.equal('https://example.com/page1'); + expect(message.data.opportunity_id).to.equal('oppty-789'); }); it('should handle opportunity without data object', async () => { @@ -139,9 +159,13 @@ describe('sendSQSMessageForAutoSuggest', () => { siteId: 'site-123', auditId: 'audit-456', opportunityId: 'oppty-789', + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect(sqsStub.calledOnce).to.be.true; }); @@ -153,14 +177,18 @@ describe('sendSQSMessageForAutoSuggest', () => { opportunityId: 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, null); + await sendSQSMessageForAutoSuggest(context, opportunity, null, [{ type: 'url', url: 'https://example.com/page1' }]); expect(sqsStub.calledOnce).to.be.true; const message = sqsStub.firstCall.args[1]; expect(message.deliveryType).to.equal('aem_cs'); - expect(message.data.page).to.equal(''); + expect(message.data.page).to.equal('https://example.com/page1'); }); it('should handle null opportunity gracefully', async () => { @@ -182,17 +210,21 @@ describe('sendSQSMessageForAutoSuggest', () => { opportunityId: 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); - expect(context.log.info.calledTwice).to.be.true; + expect(context.log.info.callCount).to.equal(4); // Received, Sending, Sent, Final expect(context.log.info.firstCall.args[0]).to.include('Received CWV opportunity for auto-suggest'); expect(context.log.info.firstCall.args[0]).to.include('siteId: site-123'); - expect(context.log.info.firstCall.args[0]).to.include('opportunityId: oppty-789'); - expect(context.log.info.secondCall.args[0]).to.include('CWV opportunity sent to Mystique for auto-suggest'); - expect(context.log.info.secondCall.args[0]).to.include('siteId: site-123'); - expect(context.log.info.secondCall.args[0]).to.include('opportunityId: oppty-789'); + expect(context.log.info.firstCall.args[0]).to.include('opportunityId: '); + expect(context.log.info.secondCall.args[0]).to.include('Sending 1 URL(s) to Mystique for CWV analysis'); + expect(context.log.info.thirdCall.args[0]).to.include('Sent URL to Mystique: https://example.com/page1'); + expect(context.log.info.getCall(3).args[0]).to.include('CWV opportunity sent to Mystique for auto-suggest'); }); it('should handle missing opportunityId', async () => { @@ -203,13 +235,18 @@ describe('sendSQSMessageForAutoSuggest', () => { getId: () => 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); - expect(context.sqs.sendMessage.calledOnce).to.be.true; - const message = context.sqs.sendMessage.firstCall.args[1]; - expect(message.data.opportunityId).to.equal(''); + expect(context.log.info.called).to.be.true; + expect(context.log.info.firstCall.args[0]).to.include('Received CWV opportunity for auto-suggest'); + expect(context.log.info.firstCall.args[0]).to.include('siteId: site-123'); + expect(context.log.info.firstCall.args[0]).to.include('opportunityId: '); }); it('should handle SQS sendMessage error and throw', async () => { @@ -223,10 +260,14 @@ describe('sendSQSMessageForAutoSuggest', () => { getId: () => 'oppty-789', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; try { - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect.fail('Should have thrown an error'); } catch (thrownError) { expect(thrownError.message).to.equal('SQS send failed'); @@ -248,10 +289,14 @@ describe('sendSQSMessageForAutoSuggest', () => { getId: () => 'oppty-from-getId', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; try { - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect.fail('Should have thrown an error'); } catch (thrownError) { expect(thrownError.message).to.equal('SQS send failed'); @@ -272,10 +317,14 @@ describe('sendSQSMessageForAutoSuggest', () => { // opportunityId is missing and no getId method data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; try { - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect.fail('Should have thrown an error'); } catch (thrownError) { expect(thrownError.message).to.equal('SQS send failed'); @@ -296,10 +345,14 @@ describe('sendSQSMessageForAutoSuggest', () => { opportunityId: 'oppty-222', data: { }, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), + getStatus: () => 'NEW', + }]), }; try { - await sendSQSMessageForAutoSuggest(context, opportunity, site); + await sendSQSMessageForAutoSuggest(context, opportunity, site, [{ type: 'url', url: 'https://example.com/page1' }]); expect.fail('Should have thrown an error'); } catch (thrownError) { expect(thrownError.message).to.equal('SQS send failed'); @@ -309,6 +362,75 @@ describe('sendSQSMessageForAutoSuggest', () => { expect(context.log.error.firstCall.args[0]).to.include('opportunityId: oppty-222'); } }); + + it('should send multiple SQS messages for multiple URL entries', async () => { + const opportunity = { + siteId: 'site-123', + auditId: 'audit-456', + opportunityId: 'oppty-789', + data: { + }, + getSuggestions: () => Promise.resolve([ + { getData: () => ({ type: 'url', url: 'https://example.com/page1', issues: [] }), getStatus: () => 'NEW' }, + { getData: () => ({ type: 'url', url: 'https://example.com/page2', issues: [] }), getStatus: () => 'NEW' }, + ]), + }; + const cwvEntries = [ + { type: 'url', url: 'https://example.com/page1' }, + { type: 'url', url: 'https://example.com/page2' }, + ]; + + await sendSQSMessageForAutoSuggest(context, opportunity, site, cwvEntries); + + expect(sqsStub.callCount).to.equal(2); + expect(sqsStub.firstCall.args[1].data.page).to.equal('https://example.com/page1'); + expect(sqsStub.secondCall.args[1].data.page).to.equal('https://example.com/page2'); + }); + + it('should not send SQS message when there are no url entries', async () => { + const opportunity = { + siteId: 'site-123', + auditId: 'audit-456', + opportunityId: 'oppty-789', + data: { + }, + getSuggestions: () => Promise.resolve([]), + }; + const cwvEntries = [ + { type: 'desktop', url: 'https://example.com/page1' }, + ]; + + await sendSQSMessageForAutoSuggest(context, opportunity, site, cwvEntries); + + expect(sqsStub.callCount).to.equal(0); + expect(context.log.info).to.have.been.calledWith('No new URL entries to send for CWV auto-suggest'); + }); + + it('should correctly determine hasGuidance with various issue values', async () => { + const opportunity = { + siteId: 'site-123', + auditId: 'audit-456', + opportunityId: 'oppty-789', + data: {}, + getSuggestions: () => Promise.resolve([{ + getData: () => ({ + url: 'https://example.com/page1', + issues: [ + { type: 'lcp', value: null }, // null value + { type: 'cls' }, // missing value + { type: 'inp', value: ' ' }, // whitespace value + ], + }), + getStatus: () => 'NEW', + }]), + }; + const cwvEntries = [{ type: 'url', url: 'https://example.com/page1' }]; + + await sendSQSMessageForAutoSuggest(context, opportunity, site, cwvEntries); + + // hasGuidance should be false because all issue values are invalid, so the message should be sent + expect(sqsStub.callCount).to.equal(1); + }); }); describe('needsAutoSuggest', () => { diff --git a/test/audits/hreflang.test.js b/test/audits/hreflang.test.js index 63b61b839..75c8cd706 100644 --- a/test/audits/hreflang.test.js +++ b/test/audits/hreflang.test.js @@ -383,7 +383,7 @@ describe('Hreflang Audit', () => { expect(result.fullAuditRef).to.equal(baseURL); expect(scope.isDone()).to.be.true; - }); + }).timeout(5000); it('should aggregate issues correctly', async () => { const htmlWithIssues = `