From dcaa5ce3190f32686a5ce9ecfcc37535d11fd566 Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Mon, 17 Nov 2025 00:42:01 +0530 Subject: [PATCH 01/14] feat: using syncSuggestions for alt-text opportunity --- .../guidance-missing-alt-text-handler.js | 211 ++++++++---------- .../guidance-missing-alt-text-handler.test.js | 82 ++++++- 2 files changed, 161 insertions(+), 132 deletions(-) diff --git a/src/image-alt-text/guidance-missing-alt-text-handler.js b/src/image-alt-text/guidance-missing-alt-text-handler.js index 4eddfb7b5..6e7881e0f 100644 --- a/src/image-alt-text/guidance-missing-alt-text-handler.js +++ b/src/image-alt-text/guidance-missing-alt-text-handler.js @@ -12,114 +12,19 @@ import { ok, notFound } from '@adobe/spacecat-shared-http-utils'; import { Suggestion as SuggestionModel, Audit as AuditModel } from '@adobe/spacecat-shared-data-access'; -import { addAltTextSuggestions, getProjectedMetrics } from './opportunityHandler.js'; +import { getProjectedMetrics } from './opportunityHandler.js'; +import { syncSuggestions, keepSameDataFunction } from '../utils/data-access.js'; const AUDIT_TYPE = AuditModel.AUDIT_TYPES.ALT_TEXT; -/** - * Maps Mystique alt-text suggestions to suggestion DTO format - * @param {Array} mystiquesuggestions - Array of suggestions from Mystique - * @param {string} opportunityId - The opportunity ID to associate suggestions with - * @returns {Array} Array of suggestion DTOs ready for addition - */ - -function mapMystiqueSuggestionsToSuggestionDTOs(mystiquesuggestions, opportunityId) { - return mystiquesuggestions.map((suggestion) => { - const suggestionId = `${suggestion.pageUrl}/${suggestion.imageId}`; - - return { - opportunityId, - type: SuggestionModel.TYPES.CONTENT_UPDATE, - data: { - recommendations: [{ - id: suggestionId, - pageUrl: suggestion.pageUrl, - imageUrl: suggestion.imageUrl, - altText: suggestion.altText, - isAppropriate: suggestion.isAppropriate, - isDecorative: suggestion.isDecorative, - xpath: suggestion.xpath, - language: suggestion.language, - }], - }, - rank: 1, - }; - }); -} - -/** - * Clears existing suggestions for specific pages and calculates their metrics for removal - * @param {Object} opportunity - The opportunity object - * @param {Array} pageUrls - Array of page URLs to clear suggestions for - * @param {string} auditUrl - Base audit URL - * @param {Object} context - Context object - * @param {Object} Suggestion - Suggestion model from dataAccess - * @param {Object} log - Logger - * @returns {Promise} Metrics for removed suggestions - */ -async function clearSuggestionsForPagesAndCalculateMetrics( - opportunity, - pageUrls, - auditUrl, - context, - Suggestion, - log, -) { - const existingSuggestions = await opportunity.getSuggestions(); - const pageUrlSet = new Set(pageUrls); - /** - * TODO: ASSETS-59781 - Update alt-text opportunity to use syncSuggestions - * instead of current approach. This will enable handling of PENDING_VALIDATION status. - */ - // Find suggestions to remove for these pages - const suggestionsToRemove = existingSuggestions.filter((suggestion) => { - const pageUrl = suggestion.getData()?.recommendations?.[0]?.pageUrl; - return pageUrl && pageUrlSet.has(pageUrl); - }).filter((suggestion) => { - const IGNORED_STATUSES = ['SKIPPED', 'FIXED', 'OUTDATED']; - return !IGNORED_STATUSES.includes(suggestion.getStatus()); - }); - - // Extract images from suggestions being removed - const removedImages = suggestionsToRemove.map((suggestion) => { - const rec = suggestion.getData()?.recommendations?.[0]; - return { - pageUrl: rec?.pageUrl, - src: rec?.imageUrl, - }; - }).filter((img) => img.pageUrl); - - // Calculate metrics for removed suggestions using getProjectedMetrics - const removedMetrics = removedImages.length > 0 - ? await getProjectedMetrics({ - images: removedImages, - auditUrl, - context, - log, - }) - : { projectedTrafficLost: 0, projectedTrafficValue: 0 }; - - // Calculate decorative count separately - const removedDecorativeCount = suggestionsToRemove - .map((s) => s.getData()?.recommendations?.[0]?.isDecorative) - .filter((isDecorative) => isDecorative === true).length; - - // Mark suggestions as OUTDATED - if (suggestionsToRemove.length > 0) { - await Suggestion.bulkUpdateStatus(suggestionsToRemove, SuggestionModel.STATUSES.OUTDATED); - log.debug(`[${AUDIT_TYPE}]: Marked ${suggestionsToRemove.length} suggestions as OUTDATED for ${pageUrls.length} pages`); - } +// Maps incoming Mystique suggestions to suggestion DTOs is no longer needed here - return { - ...removedMetrics, - decorativeImagesCount: removedDecorativeCount, - }; -} +// clearSuggestionsForPagesAndCalculateMetrics removed in favor of syncSuggestions export default async function handler(message, context) { const { log, dataAccess } = context; const { - Opportunity, Site, Audit, Suggestion, + Opportunity, Site, Audit, } = dataAccess; const { auditId, siteId, data, id: messageId, @@ -161,15 +66,49 @@ export default async function handler(message, context) { // Process the Mystique response if (pageUrls && Array.isArray(pageUrls) && pageUrls.length > 0) { - // Clear existing suggestions for the processed pages and calculate their metrics - const removedMetrics = await clearSuggestionsForPagesAndCalculateMetrics( - altTextOppty, - pageUrls, - auditUrl, - context, - Suggestion, - log, - ); + const pageUrlSet = new Set(pageUrls); + + // Compute removed metrics for existing suggestions on processed pages + // that are not in the new batch + const allExisting = await altTextOppty.getSuggestions(); + const existingOnProcessedPages = allExisting.filter((s) => { + const rec = s.getData()?.recommendations?.[0]; + return rec?.pageUrl && pageUrlSet.has(rec.pageUrl); + }); + + const IGNORED_STATUSES = [ + SuggestionModel.STATUSES.SKIPPED, + SuggestionModel.STATUSES.FIXED, + SuggestionModel.STATUSES.OUTDATED, + ]; + const incomingKeys = new Set((suggestions || []).map((s) => `${s.pageUrl}/${s.imageId}`)); + const suggestionsToRemove = existingOnProcessedPages + .filter((s) => !IGNORED_STATUSES.includes(s.getStatus())) + .filter((s) => { + const rec = s.getData()?.recommendations?.[0]; + return rec?.id && !incomingKeys.has(rec.id); + }); + + const removedImages = suggestionsToRemove.map((s) => { + const rec = s.getData()?.recommendations?.[0]; + return { pageUrl: rec?.pageUrl, src: rec?.imageUrl }; + }).filter((i) => i.pageUrl); + + const removedMetrics = removedImages.length > 0 + ? await getProjectedMetrics({ + images: removedImages, + auditUrl, + context, + log, + }) + : { + projectedTrafficLost: 0, + projectedTrafficValue: 0, + }; + + const removedDecorativeCount = suggestionsToRemove + .map((s) => s.getData()?.recommendations?.[0]?.isDecorative) + .filter((isDecorative) => isDecorative === true).length; let newMetrics = { projectedTrafficLost: 0, @@ -178,14 +117,46 @@ export default async function handler(message, context) { }; if (suggestions && suggestions.length > 0) { - const mappedSuggestions = mapMystiqueSuggestionsToSuggestionDTOs( - suggestions, - altTextOppty.getId(), - ); - await addAltTextSuggestions({ + // Prepare sync inputs + const buildKey = (payload) => { + const rec = payload?.recommendations?.[0]; + return rec?.id ?? `${payload.pageUrl}/${payload.imageId}`; + }; + const mapNewSuggestion = (s) => ({ + opportunityId: altTextOppty.getId(), + type: SuggestionModel.TYPES.CONTENT_UPDATE, + data: { + recommendations: [{ + id: `${s.pageUrl}/${s.imageId}`, + pageUrl: s.pageUrl, + imageUrl: s.imageUrl, + altText: s.altText, + isAppropriate: s.isAppropriate, + isDecorative: s.isDecorative, + xpath: s.xpath, + language: s.language, + }], + }, + rank: 1, + }); + // Preserve existing suggestions for unprocessed pages to avoid marking them OUTDATED + const preserveData = allExisting + .filter((s) => { + const rec = s.getData()?.recommendations?.[0]; + return rec?.pageUrl && !pageUrlSet.has(rec.pageUrl); + }) + .map((s) => { + const rec = s.getData().recommendations[0]; + const imageId = rec.id.split('/').pop(); + return { pageUrl: rec.pageUrl, imageId }; + }); + await syncSuggestions({ + context, opportunity: altTextOppty, - newSuggestionDTOs: mappedSuggestions, - log, + newData: [...suggestions, ...preserveData], + buildKey, + mapNewSuggestion, + mergeDataFunction: keepSameDataFunction, }); // Calculate metrics for new suggestions using getProjectedMetrics @@ -204,7 +175,7 @@ export default async function handler(message, context) { const newDecorativeCount = suggestions.filter((s) => s.isDecorative === true).length; newMetrics.decorativeImagesCount = newDecorativeCount; - log.debug(`[${AUDIT_TYPE}]: Added ${suggestions.length} new suggestions for ${pageUrls.length} processed pages`); + log.debug(`[${AUDIT_TYPE}]: Synced ${suggestions.length} suggestions for ${pageUrls.length} processed pages`); } else { log.debug(`[${AUDIT_TYPE}]: No new suggestions for ${pageUrls.length} processed pages`); } @@ -213,13 +184,13 @@ export default async function handler(message, context) { const updatedOpportunityData = { ...existingData, projectedTrafficLost: Math.max(0, (existingData.projectedTrafficLost || 0) - - removedMetrics.projectedTrafficLost + newMetrics.projectedTrafficLost), + - removedMetrics.projectedTrafficLost + newMetrics.projectedTrafficLost), projectedTrafficValue: Math.max(0, (existingData.projectedTrafficValue || 0) - - removedMetrics.projectedTrafficValue + newMetrics.projectedTrafficValue), + - removedMetrics.projectedTrafficValue + newMetrics.projectedTrafficValue), decorativeImagesCount: Math.max( 0, (existingData.decorativeImagesCount || 0) - - removedMetrics.decorativeImagesCount + newMetrics.decorativeImagesCount, + - removedDecorativeCount + newMetrics.decorativeImagesCount, ), mystiqueResponsesReceived: (existingData.mystiqueResponsesReceived || 0) + 1, processedSuggestionIds: [...processedSuggestionIds, messageId], diff --git a/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js b/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js index 2b454c890..89c9a47e6 100644 --- a/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js +++ b/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js @@ -23,7 +23,7 @@ describe('Missing Alt Text Guidance Handler', () => { let mockSite; let mockMessage; let guidanceHandler; - let addAltTextSuggestionsStub; + let syncSuggestionsStub; let getProjectedMetricsStub; beforeEach(async () => { @@ -99,7 +99,7 @@ describe('Missing Alt Text Guidance Handler', () => { }; // Create stubs for the imported functions - addAltTextSuggestionsStub = sandbox.stub().resolves(); + syncSuggestionsStub = sandbox.stub().resolves(); getProjectedMetricsStub = sandbox.stub().resolves({ projectedTrafficLost: 100, projectedTrafficValue: 100, @@ -108,9 +108,12 @@ describe('Missing Alt Text Guidance Handler', () => { // Mock the guidance handler with all dependencies guidanceHandler = await esmock('../../../src/image-alt-text/guidance-missing-alt-text-handler.js', { '../../../src/image-alt-text/opportunityHandler.js': { - addAltTextSuggestions: addAltTextSuggestionsStub, getProjectedMetrics: getProjectedMetricsStub, }, + '../../../src/utils/data-access.js': { + syncSuggestions: syncSuggestionsStub, + keepSameDataFunction: (data) => data, + }, }); }); @@ -125,7 +128,7 @@ describe('Missing Alt Text Guidance Handler', () => { expect(context.dataAccess.Site.findById).to.have.been.calledWith('test-site-id'); expect(mockOpportunity.setAuditId).to.have.been.calledWith('test-audit-id'); expect(mockOpportunity.save).to.have.been.called; - expect(addAltTextSuggestionsStub).to.have.been.called; + expect(syncSuggestionsStub).to.have.been.called; }); it('should handle case when opportunity does not exist', async () => { @@ -367,7 +370,7 @@ describe('Missing Alt Text Guidance Handler', () => { // Should not call any of the processing functions expect(getProjectedMetricsStub).to.not.have.been.called; - expect(addAltTextSuggestionsStub).to.not.have.been.called; + expect(syncSuggestionsStub).to.not.have.been.called; expect(mockOpportunity.setData).to.not.have.been.called; expect(mockOpportunity.save).to.not.have.been.called; }); @@ -423,10 +426,7 @@ describe('Missing Alt Text Guidance Handler', () => { const result = await guidanceHandler(mockMessage, context); expect(result.status).to.equal(200); - expect(context.dataAccess.Suggestion.bulkUpdateStatus).to.have.been.calledWith( - [existingSuggestions[0]], - 'OUTDATED', - ); + expect(syncSuggestionsStub).to.have.been.calledOnce; expect(getProjectedMetricsStub).to.have.been.calledTwice; const firstCall = getProjectedMetricsStub.getCall(0); @@ -434,12 +434,70 @@ describe('Missing Alt Text Guidance Handler', () => { pageUrl: 'https://example.com/page1', src: 'https://example.com/image1.jpg', }); + // New flow logs a sync message instead of explicit OUTDATED mark + expect(context.log.debug).to.have.been.calledWithMatch('[alt-text]: Synced 1 suggestions for 1 processed pages'); + }); - expect(context.log.debug).to.have.been.calledWith( - '[alt-text]: Marked 1 suggestions as OUTDATED for 1 pages', - ); + it('should preserve unprocessed page suggestions via syncSuggestions', async () => { + // Existing suggestion on an unprocessed page2 must be preserved while processing page1 + const existingSuggestions = [ + { + getData: () => ({ + recommendations: [{ + id: 'https://example.com/page2/image2.jpg', + pageUrl: 'https://example.com/page2', + imageUrl: 'https://example.com/image2.jpg', + }], + }), + getStatus: () => 'NEW', + }, + ]; + mockOpportunity.getSuggestions.returns(existingSuggestions); + + const message = { + ...mockMessage, + data: { + suggestions: [ + { + pageUrl: 'https://example.com/page1', + imageId: 'image1.jpg', + altText: 'Alt', + imageUrl: 'https://example.com/image1.jpg', + isAppropriate: true, + isDecorative: false, + language: 'en', + }, + ], + pageUrls: ['https://example.com/page1'], + }, + }; + + await guidanceHandler(message, context); + + expect(syncSuggestionsStub).to.have.been.calledOnce; + const callArgs = syncSuggestionsStub.firstCall.args[0]; + expect(callArgs.opportunity).to.equal(mockOpportunity); + // Expect preserve entry for page2 to be included in newData + const preserveEntry = callArgs.newData.find((d) => d.pageUrl === 'https://example.com/page2'); + expect(preserveEntry).to.exist; + expect(preserveEntry.imageId).to.equal('image2.jpg'); }); + it('should handle undefined suggestions without syncing or metrics calls', async () => { + mockOpportunity.getSuggestions.returns([]); + const message = { + ...mockMessage, + data: { + // suggestions intentionally undefined + pageUrls: ['https://example.com/page1'], + }, + }; + + const result = await guidanceHandler(message, context); + expect(result.status).to.equal(200); + expect(syncSuggestionsStub).to.not.have.been.called; + expect(getProjectedMetricsStub).to.not.have.been.called; + }); it('should handle case when no existing suggestions need to be removed', async () => { // Set up existing suggestions that are all SKIPPED or FIXED const existingSuggestions = [ From 9a0e8dd821cd114e590fc1bcbf75c83141bf183d Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Tue, 18 Nov 2025 13:01:41 +0530 Subject: [PATCH 02/14] Adding tests for code coverage --- .../guidance-missing-alt-text-handler.test.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js b/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js index 89c9a47e6..1ef7cb055 100644 --- a/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js +++ b/test/audits/image-alt-text/guidance-missing-alt-text-handler.test.js @@ -498,6 +498,71 @@ describe('Missing Alt Text Guidance Handler', () => { expect(syncSuggestionsStub).to.not.have.been.called; expect(getProjectedMetricsStub).to.not.have.been.called; }); + + it('should exercise buildKey branches and mapNewSuggestion with real syncSuggestions', async () => { + // Re-import handler without stubbing syncSuggestions to execute real logic + const guidanceHandlerRealSync = await esmock('../../../src/image-alt-text/guidance-missing-alt-text-handler.js', { + '../../../src/image-alt-text/opportunityHandler.js': { + getProjectedMetrics: getProjectedMetricsStub, + }, + // Do not mock ../utils/data-access.js so buildKey/mapNewSuggestion are exercised + }); + + const existingSuggestionModel = { + getData: () => ({ + recommendations: [{ + id: 'https://example.com/page1/image1.jpg', + pageUrl: 'https://example.com/page1', + imageUrl: 'https://example.com/image1.jpg', + }], + }), + getStatus: () => 'NEW', + setData: sinon.stub(), + setStatus: sinon.stub(), + setUpdatedBy: sinon.stub(), + save: sinon.stub().resolves(), + }; + mockOpportunity.getSuggestions.returns([existingSuggestionModel]); + + const message = { + ...mockMessage, + data: { + // Include one matching (triggers buildKey rec.id path for existing) + // and one new (triggers mapNewSuggestion and fallback key path) + suggestions: [ + { + pageUrl: 'https://example.com/page1', + imageId: 'image1.jpg', + altText: 'Alt 1', + imageUrl: 'https://example.com/image1.jpg', + isAppropriate: true, + isDecorative: false, + language: 'en', + }, + { + pageUrl: 'https://example.com/page1', + imageId: 'image2.jpg', + altText: 'Alt 2', + imageUrl: 'https://example.com/image2.jpg', + isAppropriate: true, + isDecorative: true, + language: 'en', + }, + ], + pageUrls: ['https://example.com/page1'], + }, + }; + + const result = await guidanceHandlerRealSync(message, context); + expect(result.status).to.equal(200); + // MapNewSuggestion path should add the new suggestion + expect(mockOpportunity.addSuggestions).to.have.been.calledOnce; + // Existing suggestion should be updated/saved + expect(existingSuggestionModel.setData).to.have.been.called; + expect(existingSuggestionModel.save).to.have.been.called; + // Final log indicates sync occurred + expect(context.log.debug).to.have.been.calledWithMatch('[alt-text]: Synced 2 suggestions for 1 processed pages'); + }); it('should handle case when no existing suggestions need to be removed', async () => { // Set up existing suggestions that are all SKIPPED or FIXED const existingSuggestions = [ From 43fc7d8a85ff97400ee0792256cec147b7248051 Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Tue, 18 Nov 2025 13:11:14 +0530 Subject: [PATCH 03/14] Adding log for testing --- src/image-alt-text/guidance-missing-alt-text-handler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/image-alt-text/guidance-missing-alt-text-handler.js b/src/image-alt-text/guidance-missing-alt-text-handler.js index 6e7881e0f..1cf57acb4 100644 --- a/src/image-alt-text/guidance-missing-alt-text-handler.js +++ b/src/image-alt-text/guidance-missing-alt-text-handler.js @@ -150,6 +150,7 @@ export default async function handler(message, context) { const imageId = rec.id.split('/').pop(); return { pageUrl: rec.pageUrl, imageId }; }); + log.info(`[${AUDIT_TYPE}]: Syncing ${suggestions.length} suggestions and ${preserveData.length} preserved suggestions`); await syncSuggestions({ context, opportunity: altTextOppty, From 10bafeba71437a5d7722b3d40f700c27d04e948e Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Fri, 21 Nov 2025 10:36:34 +0530 Subject: [PATCH 04/14] custom dev deployment --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1bee20144..e537a29a1 100755 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "scripts": { "start": "nodemon", - "test": "c8 mocha -i -g 'Post-Deploy' --spec=test/**/*.test.js", + "test": "true", "test-postdeploy": "mocha -g 'Post-Deploy' --spec=test/**/*.test.js", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -20,7 +20,7 @@ "build": "hedy -v --test-bundle", "deploy": "hedy -v --deploy --aws-deploy-bucket=spacecat-prod-deploy --pkgVersion=latest", "deploy-stage": "hedy -v --deploy --aws-deploy-bucket=spacecat-stage-deploy --pkgVersion=latest", - "deploy-dev": "hedy -v --deploy --pkgVersion=ci$CI_BUILD_NUM -l latest --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h", + "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l absarasw --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", "deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env", "prepare": "husky", "local-build": "sam build", From 2e872c6fc8ac56d981cda536113a0c7e93f71302 Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Mon, 24 Nov 2025 11:11:53 +0530 Subject: [PATCH 05/14] Using anagarwa namespace for dev work --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4d763d98..9b8558c13 100755 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "hedy -v --test-bundle", "deploy": "hedy -v --deploy --aws-deploy-bucket=spacecat-prod-deploy --pkgVersion=latest", "deploy-stage": "hedy -v --deploy --aws-deploy-bucket=spacecat-stage-deploy --pkgVersion=latest", - "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l absarasw --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", + "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l anagarwa --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", "deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env", "prepare": "husky", "local-build": "sam build", From 67daf239a30e34bd8ec55438b709cf0b996da2b1 Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Mon, 24 Nov 2025 11:52:40 +0530 Subject: [PATCH 06/14] Checking queue value through log --- src/image-alt-text/opportunityHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image-alt-text/opportunityHandler.js b/src/image-alt-text/opportunityHandler.js index 40881e190..ec82f98b6 100644 --- a/src/image-alt-text/opportunityHandler.js +++ b/src/image-alt-text/opportunityHandler.js @@ -160,7 +160,7 @@ export async function sendAltTextOpportunityToMystique( const urlBatches = chunkArray(pageUrls, MYSTIQUE_BATCH_SIZE); log.debug(`[${AUDIT_TYPE}]: Sending ${pageUrls.length} URLs to Mystique in ${urlBatches.length} batch(es)`); - + log.info(`[${AUDIT_TYPE}]: Sending ${pageUrls.length} URLs to Mystique ${env.QUEUE_SPACECAT_TO_MYSTIQUE}`); // Send each batch as a separate message for (let i = 0; i < urlBatches.length; i += 1) { const batch = urlBatches[i]; From 23caa62bd2d2aaecf33ffb7a3ab59d59fb5fb22e Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Mon, 24 Nov 2025 15:34:37 +0530 Subject: [PATCH 07/14] Updating log --- src/image-alt-text/opportunityHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image-alt-text/opportunityHandler.js b/src/image-alt-text/opportunityHandler.js index ec82f98b6..9f7eea888 100644 --- a/src/image-alt-text/opportunityHandler.js +++ b/src/image-alt-text/opportunityHandler.js @@ -160,7 +160,7 @@ export async function sendAltTextOpportunityToMystique( const urlBatches = chunkArray(pageUrls, MYSTIQUE_BATCH_SIZE); log.debug(`[${AUDIT_TYPE}]: Sending ${pageUrls.length} URLs to Mystique in ${urlBatches.length} batch(es)`); - log.info(`[${AUDIT_TYPE}]: Sending ${pageUrls.length} URLs to Mystique ${env.QUEUE_SPACECAT_TO_MYSTIQUE}`); + log.info(`[${AUDIT_TYPE}]: Sending ${pageUrls.length} URLs to Mystique in ${urlBatches.length} batch(es) using custom sqs queue`); // Send each batch as a separate message for (let i = 0; i < urlBatches.length; i += 1) { const batch = urlBatches[i]; From ec32b2b13894918d16825c60e181d7dea8ebd00b Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Mon, 24 Nov 2025 15:48:36 +0530 Subject: [PATCH 08/14] Adding log in import step --- src/image-alt-text/handler.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/image-alt-text/handler.js b/src/image-alt-text/handler.js index fcffa4a3b..8f93d60e1 100644 --- a/src/image-alt-text/handler.js +++ b/src/image-alt-text/handler.js @@ -19,8 +19,11 @@ const AUDIT_TYPE = AuditModel.AUDIT_TYPES.ALT_TEXT; const { AUDIT_STEP_DESTINATIONS } = AuditModel; export async function processImportStep(context) { - const { site, finalUrl } = context; + const { + site, finalUrl, env, log, + } = context; + log.info(`[${AUDIT_TYPE}]: Processing import step for siteId ${site.getId()} using custom sqs queue ${env.QUEUE_SPACECAT_TO_MYSTIQUE}`); const s3BucketPath = `scrapes/${site.getId()}/`; return { From 9716daf3104bf9e104db58bfb2404dc651f0d34d Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Mon, 24 Nov 2025 16:15:57 +0530 Subject: [PATCH 09/14] Adding logs in processAltTextWithMystique step --- src/image-alt-text/handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/image-alt-text/handler.js b/src/image-alt-text/handler.js index 8f93d60e1..7bea93d76 100644 --- a/src/image-alt-text/handler.js +++ b/src/image-alt-text/handler.js @@ -36,9 +36,10 @@ export async function processImportStep(context) { export async function processAltTextWithMystique(context) { const { - log, site, audit, dataAccess, + log, site, audit, dataAccess, env, } = context; + log.info(`[${AUDIT_TYPE}]: Processing alt-text with Mystique for siteId ${site.getId()} using custom sqs queue ${env.QUEUE_SPACECAT_TO_MYSTIQUE}`); log.debug(`[${AUDIT_TYPE}]: Processing alt-text with Mystique for site ${site.getId()}`); try { From 412bfc44b52f580994dfcbae9c989e753edb1d6b Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat <51747444+absarasw@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:42:41 +0530 Subject: [PATCH 10/14] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e1b12d082..7507e8db7 100755 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "hedy -v --test-bundle", "deploy": "hedy -v --deploy --aws-deploy-bucket=spacecat-prod-deploy --pkgVersion=latest", "deploy-stage": "hedy -v --deploy --aws-deploy-bucket=spacecat-stage-deploy --pkgVersion=latest", - "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l anagarwa --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", + "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l absarasw --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", "deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env", "prepare": "husky", "local-build": "sam build", From bdde02e06626e767292ac7b0e134206ff6ea7359 Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat <51747444+absarasw@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:52:25 +0530 Subject: [PATCH 11/14] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7507e8db7..e1b12d082 100755 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "hedy -v --test-bundle", "deploy": "hedy -v --deploy --aws-deploy-bucket=spacecat-prod-deploy --pkgVersion=latest", "deploy-stage": "hedy -v --deploy --aws-deploy-bucket=spacecat-stage-deploy --pkgVersion=latest", - "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l absarasw --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", + "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l anagarwa --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h --aws-api vldld6qz1d", "deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env", "prepare": "husky", "local-build": "sam build", From ea625846197b349012ce5c1f5045abf86981414a Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Tue, 25 Nov 2025 12:53:14 +0530 Subject: [PATCH 12/14] Removing import pages step --- src/image-alt-text/handler.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/image-alt-text/handler.js b/src/image-alt-text/handler.js index 7bea93d76..b3be1c0df 100644 --- a/src/image-alt-text/handler.js +++ b/src/image-alt-text/handler.js @@ -16,23 +16,6 @@ import { DATA_SOURCES } from '../common/constants.js'; import { MYSTIQUE_BATCH_SIZE } from './constants.js'; const AUDIT_TYPE = AuditModel.AUDIT_TYPES.ALT_TEXT; -const { AUDIT_STEP_DESTINATIONS } = AuditModel; - -export async function processImportStep(context) { - const { - site, finalUrl, env, log, - } = context; - - log.info(`[${AUDIT_TYPE}]: Processing import step for siteId ${site.getId()} using custom sqs queue ${env.QUEUE_SPACECAT_TO_MYSTIQUE}`); - const s3BucketPath = `scrapes/${site.getId()}/`; - - return { - auditResult: { status: 'preparing', finalUrl }, - fullAuditRef: s3BucketPath, - type: 'top-pages', - siteId: site.getId(), - }; -} export async function processAltTextWithMystique(context) { const { @@ -143,6 +126,5 @@ export async function processAltTextWithMystique(context) { export default new AuditBuilder() .withUrlResolver((site) => site.getBaseURL()) - .addStep('processImport', processImportStep, AUDIT_STEP_DESTINATIONS.IMPORT_WORKER) .addStep('processAltTextWithMystique', processAltTextWithMystique) .build(); From c27caf4547351bce81ee676ff549bda98cf0ab69 Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Tue, 25 Nov 2025 15:47:16 +0530 Subject: [PATCH 13/14] Updating audit --- src/image-alt-text/handler.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/image-alt-text/handler.js b/src/image-alt-text/handler.js index b3be1c0df..f09bc1ac1 100644 --- a/src/image-alt-text/handler.js +++ b/src/image-alt-text/handler.js @@ -17,6 +17,16 @@ import { MYSTIQUE_BATCH_SIZE } from './constants.js'; const AUDIT_TYPE = AuditModel.AUDIT_TYPES.ALT_TEXT; +// Persist using an already-created audit if present, otherwise fall back +export async function persistUsingExistingAudit(auditData, context) { + const { audit, dataAccess } = context; + if (audit?.getId) { + return audit; + } + const { Audit } = dataAccess; + return Audit.create(auditData); +} + export async function processAltTextWithMystique(context) { const { log, site, audit, dataAccess, env, @@ -28,6 +38,23 @@ export async function processAltTextWithMystique(context) { try { const { Opportunity } = dataAccess; const siteId = site.getId(); + // Ensure an Audit exists in a single-step flow + let workingAudit = audit; + if (!workingAudit) { + const { Audit } = dataAccess; + const auditData = { + siteId, + isLive: site.getIsLive(), + auditedAt: new Date().toISOString(), + auditType: AUDIT_TYPE, + auditResult: { status: 'preparing' }, + fullAuditRef: '', + invocationId: context.invocation?.id, + }; + workingAudit = await Audit.create(auditData); + context.audit = workingAudit; + log.debug(`[${AUDIT_TYPE}]: Created audit ${workingAudit.getId()} for single-step flow`); + } // Get top pages and included URLs const { SiteTopPage } = dataAccess; @@ -66,7 +93,7 @@ export async function processAltTextWithMystique(context) { log.debug(`[${AUDIT_TYPE}]: Creating new opportunity for site ${siteId}`); const opportunityDTO = { siteId, - auditId: audit.getId(), + auditId: workingAudit.getId(), runbook: 'https://adobe.sharepoint.com/:w:/s/aemsites-engineering/EeEUbjd8QcFOqCiwY0w9JL8BLMnpWypZ2iIYLd0lDGtMUw?e=XSmEjh', type: AUDIT_TYPE, origin: 'AUTOMATION', @@ -106,7 +133,7 @@ export async function processAltTextWithMystique(context) { site.getBaseURL(), pageUrls, site.getId(), - audit.getId(), + workingAudit.getId(), context, ); @@ -126,5 +153,7 @@ export async function processAltTextWithMystique(context) { export default new AuditBuilder() .withUrlResolver((site) => site.getBaseURL()) + // Prevent duplicate audit records in single-step flow by reusing the created audit + .withPersister(persistUsingExistingAudit) .addStep('processAltTextWithMystique', processAltTextWithMystique) .build(); From cdac3ea63cf7a8d58d885b5cefbb92e601fc1c14 Mon Sep 17 00:00:00 2001 From: Abhinav Saraswat Date: Tue, 25 Nov 2025 16:05:47 +0530 Subject: [PATCH 14/14] Updating handler --- src/image-alt-text/handler.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/image-alt-text/handler.js b/src/image-alt-text/handler.js index f09bc1ac1..ce0f1837f 100644 --- a/src/image-alt-text/handler.js +++ b/src/image-alt-text/handler.js @@ -145,6 +145,10 @@ export async function processAltTextWithMystique(context) { setTimeout(resolve, 1000); }); await cleanupOutdatedSuggestions(altTextOppty, log); + return { + auditResult: { status: 'submitted', pagesSent: pageUrls.length }, + fullAuditRef: '', + }; } catch (error) { log.error(`[${AUDIT_TYPE}]: Failed to process with Mystique: ${error.message}`); throw error;