From 22893d3e3a34b1a1d3551167ad4deb62a36a618b Mon Sep 17 00:00:00 2001 From: Serhii Litviachenko Date: Mon, 30 Mar 2026 22:51:08 -0500 Subject: [PATCH 1/4] Manage a custom list of audit target URLs --- .../src/models/site/config.js | 63 ++++++ .../src/models/site/index.d.ts | 21 ++ .../test/unit/models/site/config.test.js | 192 ++++++++++++++++++ 3 files changed, 276 insertions(+) diff --git a/packages/spacecat-shared-data-access/src/models/site/config.js b/packages/spacecat-shared-data-access/src/models/site/config.js index 5dee87b3e..2e227a5c6 100644 --- a/packages/spacecat-shared-data-access/src/models/site/config.js +++ b/packages/spacecat-shared-data-access/src/models/site/config.js @@ -407,6 +407,14 @@ export const configSchema = Joi.object({ contentAiConfig: Joi.object({ index: Joi.string().optional(), }).optional(), + auditTargetURLs: Joi.object({ + manual: Joi.array().items(Joi.object({ + url: Joi.string().uri().required(), + })).optional().default([]), + 'money-pages': Joi.array().items(Joi.object({ + url: Joi.string().uri().required(), + })).optional().default([]), + }).optional(), handlers: Joi.object().pattern(Joi.string(), Joi.object({ mentions: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())), excludedURLs: Joi.array().items(Joi.string()), @@ -517,6 +525,60 @@ export const Config = (data = {}) => { self.getEdgeOptimizeConfig = () => state?.edgeOptimizeConfig; self.getOnboardConfig = () => state?.onboardConfig; self.getCommerceLlmoConfig = () => state?.commerceLlmoConfig; + const AUDIT_TARGET_SOURCES = ['manual', 'money-pages']; + const auditTargetEntrySchema = Joi.object({ + url: Joi.string().uri().required(), + }); + + const validateAuditTargetSource = (source) => { + if (!AUDIT_TARGET_SOURCES.includes(source)) { + throw new Error(`Invalid audit target source: "${source}". Must be one of: ${AUDIT_TARGET_SOURCES.join(', ')}`); + } + }; + + self.getAuditTargetURLsConfig = () => state?.auditTargetURLs; + + self.getAuditTargetURLs = () => { + const targets = state?.auditTargetURLs; + if (!targets) return []; + return AUDIT_TARGET_SOURCES.flatMap( + (source) => (targets[source] || []).map((entry) => ({ ...entry, source })), + ); + }; + + self.getAuditTargetURLsBySource = (source) => { + validateAuditTargetSource(source); + return state?.auditTargetURLs?.[source] || []; + }; + + self.updateAuditTargetURLs = (source, urls) => { + validateAuditTargetSource(source); + Joi.assert(urls, Joi.array().items(auditTargetEntrySchema), 'Invalid audit target URLs'); + state.auditTargetURLs = state.auditTargetURLs || {}; + state.auditTargetURLs[source] = urls; + }; + + self.addAuditTargetURL = (source, urlObj) => { + validateAuditTargetSource(source); + Joi.assert(urlObj, auditTargetEntrySchema, 'Invalid audit target URL'); + + state.auditTargetURLs = state.auditTargetURLs || {}; + state.auditTargetURLs[source] = state.auditTargetURLs[source] || []; + const allUrls = AUDIT_TARGET_SOURCES.flatMap( + (s) => (state.auditTargetURLs[s] || []).map((e) => e.url), + ); + if (!allUrls.includes(urlObj.url)) { + state.auditTargetURLs[source].push(urlObj); + } + }; + + self.removeAuditTargetURL = (source, url) => { + validateAuditTargetSource(source); + if (!state.auditTargetURLs?.[source]) return; + state.auditTargetURLs[source] = state.auditTargetURLs[source] + .filter((t) => t.url !== url); + }; + self.updateSlackConfig = (channel, workspace, invitedUserCount) => { state.slack = { channel, @@ -871,4 +933,5 @@ Config.toDynamoItem = (config) => ({ edgeOptimizeConfig: config.getEdgeOptimizeConfig(), onboardConfig: config.getOnboardConfig?.(), commerceLlmoConfig: config.getCommerceLlmoConfig?.(), + auditTargetURLs: config.getAuditTargetURLsConfig?.(), }); diff --git a/packages/spacecat-shared-data-access/src/models/site/index.d.ts b/packages/spacecat-shared-data-access/src/models/site/index.d.ts index cef428875..0101ed4ca 100644 --- a/packages/spacecat-shared-data-access/src/models/site/index.d.ts +++ b/packages/spacecat-shared-data-access/src/models/site/index.d.ts @@ -99,6 +99,21 @@ export interface LlmoCustomerIntent { value: string; } +export type AuditTargetSource = 'manual' | 'money-pages'; + +export interface AuditTargetEntry { + url: string; +} + +export interface AuditTargetEntryWithSource extends AuditTargetEntry { + source: AuditTargetSource; +} + +export interface AuditTargetURLs { + manual?: AuditTargetEntry[]; + 'money-pages'?: AuditTargetEntry[]; +} + export interface SiteConfig { state: { slack?: { @@ -107,6 +122,7 @@ export interface SiteConfig { invitedUserCount?: number; }; imports?: ImportConfig[]; + auditTargetURLs?: AuditTargetURLs; handlers?: Record; excludedURLs?: string[]; @@ -205,6 +221,11 @@ export interface SiteConfig { removeLlmoTag(tag: string): void; getOnboardConfig(): { lastProfile?: string; lastStartTime?: number; forcedOverride?: boolean; history?: Array<{ profile?: string; startTime?: number }> } | undefined; updateOnboardConfig(onboardConfig: { lastProfile?: string; lastStartTime?: number; forcedOverride?: boolean }, options?: { maxHistory?: number }): void; + getAuditTargetURLs(): AuditTargetEntryWithSource[]; + getAuditTargetURLsBySource(source: AuditTargetSource): AuditTargetEntry[]; + updateAuditTargetURLs(source: AuditTargetSource, urls: AuditTargetEntry[]): void; + addAuditTargetURL(source: AuditTargetSource, urlObj: AuditTargetEntry): void; + removeAuditTargetURL(source: AuditTargetSource, url: string): void; } export interface Site extends BaseModel { diff --git a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js index 402414e46..5b6abf1fd 100644 --- a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js @@ -3032,4 +3032,196 @@ describe('Config Tests', () => { expect(llmoConfig).to.be.undefined; }); }); + + describe('Audit Target URLs', () => { + it('returns empty array by default', () => { + const config = Config(); + expect(config.getAuditTargetURLs()).to.deep.equal([]); + }); + + it('creates config with grouped auditTargetURLs', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/page1' }], + 'money-pages': [{ url: 'https://example.com/page2' }], + }, + }); + const result = config.getAuditTargetURLs(); + expect(result).to.have.lengthOf(2); + expect(result[0].url).to.equal('https://example.com/page1'); + expect(result[0].source).to.equal('manual'); + expect(result[1].url).to.equal('https://example.com/page2'); + expect(result[1].source).to.equal('money-pages'); + }); + + describe('getAuditTargetURLsBySource', () => { + it('returns URLs for a specific source', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/m1' }], + 'money-pages': [{ url: 'https://example.com/mp1' }, { url: 'https://example.com/mp2' }], + }, + }); + const manual = config.getAuditTargetURLsBySource('manual'); + expect(manual).to.have.lengthOf(1); + expect(manual[0].url).to.equal('https://example.com/m1'); + + const moneyPages = config.getAuditTargetURLsBySource('money-pages'); + expect(moneyPages).to.have.lengthOf(2); + }); + + it('returns empty array for source with no entries', () => { + const config = Config(); + expect(config.getAuditTargetURLsBySource('manual')).to.deep.equal([]); + }); + + it('rejects invalid source', () => { + const config = Config(); + expect(() => config.getAuditTargetURLsBySource('invalid')).to.throw('Invalid audit target source'); + }); + }); + + describe('updateAuditTargetURLs', () => { + it('replaces URLs for a specific source', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://old.com' }], + 'money-pages': [{ url: 'https://keep.com' }], + }, + }); + config.updateAuditTargetURLs('manual', [ + { url: 'https://new1.com' }, + { url: 'https://new2.com' }, + ]); + expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(2); + expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(1); + }); + + it('rejects invalid URLs', () => { + const config = Config(); + expect(() => config.updateAuditTargetURLs('manual', [{ url: 'not-a-url' }])).to.throw(); + }); + + it('rejects invalid source', () => { + const config = Config(); + expect(() => config.updateAuditTargetURLs('invalid', [{ url: 'https://example.com' }])).to.throw('Invalid audit target source'); + }); + + it('accepts empty array', () => { + const config = Config({ + auditTargetURLs: { manual: [{ url: 'https://example.com' }] }, + }); + config.updateAuditTargetURLs('manual', []); + expect(config.getAuditTargetURLsBySource('manual')).to.deep.equal([]); + }); + }); + + describe('addAuditTargetURL', () => { + it('appends a new URL to the specified source', () => { + const config = Config(); + config.addAuditTargetURL('manual', { url: 'https://example.com/page1' }); + config.addAuditTargetURL('money-pages', { url: 'https://example.com/page2' }); + expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(1); + expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(1); + expect(config.getAuditTargetURLsBySource('money-pages')[0].url).to.equal('https://example.com/page2'); + }); + + it('deduplicates across all sources', () => { + const config = Config(); + config.addAuditTargetURL('manual', { url: 'https://example.com/page1' }); + config.addAuditTargetURL('money-pages', { url: 'https://example.com/page1' }); + expect(config.getAuditTargetURLs()).to.have.lengthOf(1); + expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(1); + expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(0); + }); + + it('rejects invalid URL', () => { + const config = Config(); + expect(() => config.addAuditTargetURL('manual', { url: 'bad-url' })).to.throw(); + }); + + it('rejects missing url field', () => { + const config = Config(); + expect(() => config.addAuditTargetURL('manual', {})).to.throw(); + }); + + it('rejects invalid source', () => { + const config = Config(); + expect(() => config.addAuditTargetURL('invalid', { url: 'https://example.com' })).to.throw('Invalid audit target source'); + }); + }); + + describe('removeAuditTargetURL', () => { + it('removes by url string from the specified source', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/page1' }], + 'money-pages': [{ url: 'https://example.com/page2' }], + }, + }); + config.removeAuditTargetURL('money-pages', 'https://example.com/page2'); + expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(1); + expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(0); + }); + + it('does nothing for non-existent url', () => { + const config = Config({ + auditTargetURLs: { manual: [{ url: 'https://example.com' }] }, + }); + config.removeAuditTargetURL('manual', 'https://does-not-exist.com'); + expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(1); + }); + + it('does nothing when auditTargetURLs is not set', () => { + const config = Config(); + config.removeAuditTargetURL('manual', 'https://example.com'); + expect(config.getAuditTargetURLs()).to.deep.equal([]); + }); + + it('rejects invalid source', () => { + const config = Config(); + expect(() => config.removeAuditTargetURL('invalid', 'https://example.com')).to.throw('Invalid audit target source'); + }); + }); + + describe('serialization', () => { + it('includes auditTargetURLs in toDynamoItem conversion', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/page1' }], + }, + }); + const item = Config.toDynamoItem(config); + expect(item.auditTargetURLs).to.deep.equal({ + manual: [{ url: 'https://example.com/page1' }], + 'money-pages': [], + }); + }); + + it('round-trips through toDynamoItem and fromDynamoItem', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/page1' }], + 'money-pages': [ + { url: 'https://example.com/page2' }, + { url: 'https://example.com/page3' }, + ], + }, + }); + const item = Config.toDynamoItem(config); + const restored = Config.fromDynamoItem(item); + expect(restored.getAuditTargetURLs()).to.deep.equal(config.getAuditTargetURLs()); + }); + }); + + describe('field validation', () => { + it('rejects extra fields', () => { + const config = Config(); + expect(() => config.addAuditTargetURL('manual', { + url: 'https://example.com', + label: 'not-allowed', + })).to.throw(); + }); + }); + }); }); From 49be2f984d51c3981e38c3388c9f22ea6990a974 Mon Sep 17 00:00:00 2001 From: Serhii Litviachenko Date: Tue, 31 Mar 2026 17:27:11 -0500 Subject: [PATCH 2/4] Delete 'money-pages' --- .../src/models/site/config.js | 5 +-- .../src/models/site/index.d.ts | 3 +- .../test/unit/models/site/config.test.js | 41 +++++++------------ 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/packages/spacecat-shared-data-access/src/models/site/config.js b/packages/spacecat-shared-data-access/src/models/site/config.js index 2e227a5c6..87a7adeea 100644 --- a/packages/spacecat-shared-data-access/src/models/site/config.js +++ b/packages/spacecat-shared-data-access/src/models/site/config.js @@ -411,9 +411,6 @@ export const configSchema = Joi.object({ manual: Joi.array().items(Joi.object({ url: Joi.string().uri().required(), })).optional().default([]), - 'money-pages': Joi.array().items(Joi.object({ - url: Joi.string().uri().required(), - })).optional().default([]), }).optional(), handlers: Joi.object().pattern(Joi.string(), Joi.object({ mentions: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())), @@ -525,7 +522,7 @@ export const Config = (data = {}) => { self.getEdgeOptimizeConfig = () => state?.edgeOptimizeConfig; self.getOnboardConfig = () => state?.onboardConfig; self.getCommerceLlmoConfig = () => state?.commerceLlmoConfig; - const AUDIT_TARGET_SOURCES = ['manual', 'money-pages']; + const AUDIT_TARGET_SOURCES = ['manual']; const auditTargetEntrySchema = Joi.object({ url: Joi.string().uri().required(), }); diff --git a/packages/spacecat-shared-data-access/src/models/site/index.d.ts b/packages/spacecat-shared-data-access/src/models/site/index.d.ts index 0101ed4ca..fe9f96e6e 100644 --- a/packages/spacecat-shared-data-access/src/models/site/index.d.ts +++ b/packages/spacecat-shared-data-access/src/models/site/index.d.ts @@ -99,7 +99,7 @@ export interface LlmoCustomerIntent { value: string; } -export type AuditTargetSource = 'manual' | 'money-pages'; +export type AuditTargetSource = 'manual'; export interface AuditTargetEntry { url: string; @@ -111,7 +111,6 @@ export interface AuditTargetEntryWithSource extends AuditTargetEntry { export interface AuditTargetURLs { manual?: AuditTargetEntry[]; - 'money-pages'?: AuditTargetEntry[]; } export interface SiteConfig { diff --git a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js index 5b6abf1fd..effcfeaa4 100644 --- a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js @@ -3042,8 +3042,7 @@ describe('Config Tests', () => { it('creates config with grouped auditTargetURLs', () => { const config = Config({ auditTargetURLs: { - manual: [{ url: 'https://example.com/page1' }], - 'money-pages': [{ url: 'https://example.com/page2' }], + manual: [{ url: 'https://example.com/page1' }, { url: 'https://example.com/page2' }], }, }); const result = config.getAuditTargetURLs(); @@ -3051,23 +3050,20 @@ describe('Config Tests', () => { expect(result[0].url).to.equal('https://example.com/page1'); expect(result[0].source).to.equal('manual'); expect(result[1].url).to.equal('https://example.com/page2'); - expect(result[1].source).to.equal('money-pages'); + expect(result[1].source).to.equal('manual'); }); describe('getAuditTargetURLsBySource', () => { it('returns URLs for a specific source', () => { const config = Config({ auditTargetURLs: { - manual: [{ url: 'https://example.com/m1' }], - 'money-pages': [{ url: 'https://example.com/mp1' }, { url: 'https://example.com/mp2' }], + manual: [{ url: 'https://example.com/m1' }, { url: 'https://example.com/m2' }], }, }); const manual = config.getAuditTargetURLsBySource('manual'); - expect(manual).to.have.lengthOf(1); + expect(manual).to.have.lengthOf(2); expect(manual[0].url).to.equal('https://example.com/m1'); - - const moneyPages = config.getAuditTargetURLsBySource('money-pages'); - expect(moneyPages).to.have.lengthOf(2); + expect(manual[1].url).to.equal('https://example.com/m2'); }); it('returns empty array for source with no entries', () => { @@ -3086,7 +3082,6 @@ describe('Config Tests', () => { const config = Config({ auditTargetURLs: { manual: [{ url: 'https://old.com' }], - 'money-pages': [{ url: 'https://keep.com' }], }, }); config.updateAuditTargetURLs('manual', [ @@ -3094,7 +3089,6 @@ describe('Config Tests', () => { { url: 'https://new2.com' }, ]); expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(2); - expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(1); }); it('rejects invalid URLs', () => { @@ -3120,19 +3114,17 @@ describe('Config Tests', () => { it('appends a new URL to the specified source', () => { const config = Config(); config.addAuditTargetURL('manual', { url: 'https://example.com/page1' }); - config.addAuditTargetURL('money-pages', { url: 'https://example.com/page2' }); - expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(1); - expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(1); - expect(config.getAuditTargetURLsBySource('money-pages')[0].url).to.equal('https://example.com/page2'); + config.addAuditTargetURL('manual', { url: 'https://example.com/page2' }); + expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(2); + expect(config.getAuditTargetURLsBySource('manual')[1].url).to.equal('https://example.com/page2'); }); - it('deduplicates across all sources', () => { + it('deduplicates within the source', () => { const config = Config(); config.addAuditTargetURL('manual', { url: 'https://example.com/page1' }); - config.addAuditTargetURL('money-pages', { url: 'https://example.com/page1' }); + config.addAuditTargetURL('manual', { url: 'https://example.com/page1' }); expect(config.getAuditTargetURLs()).to.have.lengthOf(1); expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(1); - expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(0); }); it('rejects invalid URL', () => { @@ -3155,13 +3147,12 @@ describe('Config Tests', () => { it('removes by url string from the specified source', () => { const config = Config({ auditTargetURLs: { - manual: [{ url: 'https://example.com/page1' }], - 'money-pages': [{ url: 'https://example.com/page2' }], + manual: [{ url: 'https://example.com/page1' }, { url: 'https://example.com/page2' }], }, }); - config.removeAuditTargetURL('money-pages', 'https://example.com/page2'); + config.removeAuditTargetURL('manual', 'https://example.com/page2'); expect(config.getAuditTargetURLsBySource('manual')).to.have.lengthOf(1); - expect(config.getAuditTargetURLsBySource('money-pages')).to.have.lengthOf(0); + expect(config.getAuditTargetURLsBySource('manual')[0].url).to.equal('https://example.com/page1'); }); it('does nothing for non-existent url', () => { @@ -3194,17 +3185,15 @@ describe('Config Tests', () => { const item = Config.toDynamoItem(config); expect(item.auditTargetURLs).to.deep.equal({ manual: [{ url: 'https://example.com/page1' }], - 'money-pages': [], }); }); it('round-trips through toDynamoItem and fromDynamoItem', () => { const config = Config({ auditTargetURLs: { - manual: [{ url: 'https://example.com/page1' }], - 'money-pages': [ + manual: [ + { url: 'https://example.com/page1' }, { url: 'https://example.com/page2' }, - { url: 'https://example.com/page3' }, ], }, }); From 82e91cde654d402820b6401b99797f779c06fbf3 Mon Sep 17 00:00:00 2001 From: Serhii Litviachenko Date: Tue, 31 Mar 2026 17:57:16 -0500 Subject: [PATCH 3/4] Some improvements --- .../src/models/site/config.js | 2 +- .../test/unit/models/site/config.test.js | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/spacecat-shared-data-access/src/models/site/config.js b/packages/spacecat-shared-data-access/src/models/site/config.js index 87a7adeea..fc027c0d6 100644 --- a/packages/spacecat-shared-data-access/src/models/site/config.js +++ b/packages/spacecat-shared-data-access/src/models/site/config.js @@ -411,7 +411,7 @@ export const configSchema = Joi.object({ manual: Joi.array().items(Joi.object({ url: Joi.string().uri().required(), })).optional().default([]), - }).optional(), + }).options({ stripUnknown: true }).optional(), handlers: Joi.object().pattern(Joi.string(), Joi.object({ mentions: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())), excludedURLs: Joi.array().items(Joi.string()), diff --git a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js index effcfeaa4..327bef0e3 100644 --- a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js @@ -3211,6 +3211,19 @@ describe('Config Tests', () => { label: 'not-allowed', })).to.throw(); }); + + it('strips unknown source keys from auditTargetURLs', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/page1' }], + unknown: [{ url: 'https://example.com/page2' }], + }, + }); + const result = config.getAuditTargetURLs(); + expect(result).to.have.lengthOf(1); + expect(result[0].url).to.equal('https://example.com/page1'); + expect(result[0].source).to.equal('manual'); + }); }); }); }); From ec5cf7c866fe4579c19c6cfd2479d35dd6bfd35d Mon Sep 17 00:00:00 2001 From: Nitin Jadhav <3931042+nitinja@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:23:38 -0500 Subject: [PATCH 4/4] feat(audit-url): add moneyPages source to auditTargetURLs config Add moneyPages as a new audit target URL source alongside manual, with identical structure (array of { url } entries) and full cross-source deduplication support. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/models/site/config.js | 5 +- .../src/models/site/index.d.ts | 3 +- .../test/unit/models/site/config.test.js | 103 +++++++++++++++++- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/spacecat-shared-data-access/src/models/site/config.js b/packages/spacecat-shared-data-access/src/models/site/config.js index fc027c0d6..266b9e953 100644 --- a/packages/spacecat-shared-data-access/src/models/site/config.js +++ b/packages/spacecat-shared-data-access/src/models/site/config.js @@ -411,6 +411,9 @@ export const configSchema = Joi.object({ manual: Joi.array().items(Joi.object({ url: Joi.string().uri().required(), })).optional().default([]), + moneyPages: Joi.array().items(Joi.object({ + url: Joi.string().uri().required(), + })).optional().default([]), }).options({ stripUnknown: true }).optional(), handlers: Joi.object().pattern(Joi.string(), Joi.object({ mentions: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())), @@ -522,7 +525,7 @@ export const Config = (data = {}) => { self.getEdgeOptimizeConfig = () => state?.edgeOptimizeConfig; self.getOnboardConfig = () => state?.onboardConfig; self.getCommerceLlmoConfig = () => state?.commerceLlmoConfig; - const AUDIT_TARGET_SOURCES = ['manual']; + const AUDIT_TARGET_SOURCES = ['manual', 'moneyPages']; const auditTargetEntrySchema = Joi.object({ url: Joi.string().uri().required(), }); diff --git a/packages/spacecat-shared-data-access/src/models/site/index.d.ts b/packages/spacecat-shared-data-access/src/models/site/index.d.ts index fe9f96e6e..a0dea07a9 100644 --- a/packages/spacecat-shared-data-access/src/models/site/index.d.ts +++ b/packages/spacecat-shared-data-access/src/models/site/index.d.ts @@ -99,7 +99,7 @@ export interface LlmoCustomerIntent { value: string; } -export type AuditTargetSource = 'manual'; +export type AuditTargetSource = 'manual' | 'moneyPages'; export interface AuditTargetEntry { url: string; @@ -111,6 +111,7 @@ export interface AuditTargetEntryWithSource extends AuditTargetEntry { export interface AuditTargetURLs { manual?: AuditTargetEntry[]; + moneyPages?: AuditTargetEntry[]; } export interface SiteConfig { diff --git a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js index 327bef0e3..7f92b1783 100644 --- a/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/site/config.test.js @@ -3053,6 +3053,20 @@ describe('Config Tests', () => { expect(result[1].source).to.equal('manual'); }); + it('returns entries from both manual and moneyPages with correct source tags', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/manual1' }], + moneyPages: [{ url: 'https://example.com/money1' }, { url: 'https://example.com/money2' }], + }, + }); + const result = config.getAuditTargetURLs(); + expect(result).to.have.lengthOf(3); + expect(result[0]).to.deep.equal({ url: 'https://example.com/manual1', source: 'manual' }); + expect(result[1]).to.deep.equal({ url: 'https://example.com/money1', source: 'moneyPages' }); + expect(result[2]).to.deep.equal({ url: 'https://example.com/money2', source: 'moneyPages' }); + }); + describe('getAuditTargetURLsBySource', () => { it('returns URLs for a specific source', () => { const config = Config({ @@ -3066,9 +3080,22 @@ describe('Config Tests', () => { expect(manual[1].url).to.equal('https://example.com/m2'); }); + it('returns moneyPages URLs for moneyPages source', () => { + const config = Config({ + auditTargetURLs: { + moneyPages: [{ url: 'https://example.com/mp1' }, { url: 'https://example.com/mp2' }], + }, + }); + const moneyPages = config.getAuditTargetURLsBySource('moneyPages'); + expect(moneyPages).to.have.lengthOf(2); + expect(moneyPages[0].url).to.equal('https://example.com/mp1'); + expect(moneyPages[1].url).to.equal('https://example.com/mp2'); + }); + it('returns empty array for source with no entries', () => { const config = Config(); expect(config.getAuditTargetURLsBySource('manual')).to.deep.equal([]); + expect(config.getAuditTargetURLsBySource('moneyPages')).to.deep.equal([]); }); it('rejects invalid source', () => { @@ -3108,6 +3135,19 @@ describe('Config Tests', () => { config.updateAuditTargetURLs('manual', []); expect(config.getAuditTargetURLsBySource('manual')).to.deep.equal([]); }); + + it('replaces URLs for moneyPages source', () => { + const config = Config({ + auditTargetURLs: { + moneyPages: [{ url: 'https://old.com' }], + }, + }); + config.updateAuditTargetURLs('moneyPages', [ + { url: 'https://new1.com' }, + { url: 'https://new2.com' }, + ]); + expect(config.getAuditTargetURLsBySource('moneyPages')).to.have.lengthOf(2); + }); }); describe('addAuditTargetURL', () => { @@ -3141,6 +3181,25 @@ describe('Config Tests', () => { const config = Config(); expect(() => config.addAuditTargetURL('invalid', { url: 'https://example.com' })).to.throw('Invalid audit target source'); }); + + it('appends a new URL to moneyPages source', () => { + const config = Config(); + config.addAuditTargetURL('moneyPages', { url: 'https://example.com/mp1' }); + config.addAuditTargetURL('moneyPages', { url: 'https://example.com/mp2' }); + expect(config.getAuditTargetURLsBySource('moneyPages')).to.have.lengthOf(2); + expect(config.getAuditTargetURLsBySource('moneyPages')[1].url).to.equal('https://example.com/mp2'); + }); + + it('deduplicates across manual and moneyPages sources', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/shared' }], + }, + }); + config.addAuditTargetURL('moneyPages', { url: 'https://example.com/shared' }); + expect(config.getAuditTargetURLsBySource('moneyPages')).to.have.lengthOf(0); + expect(config.getAuditTargetURLs()).to.have.lengthOf(1); + }); }); describe('removeAuditTargetURL', () => { @@ -3173,6 +3232,17 @@ describe('Config Tests', () => { const config = Config(); expect(() => config.removeAuditTargetURL('invalid', 'https://example.com')).to.throw('Invalid audit target source'); }); + + it('removes by url string from moneyPages source', () => { + const config = Config({ + auditTargetURLs: { + moneyPages: [{ url: 'https://example.com/mp1' }, { url: 'https://example.com/mp2' }], + }, + }); + config.removeAuditTargetURL('moneyPages', 'https://example.com/mp2'); + expect(config.getAuditTargetURLsBySource('moneyPages')).to.have.lengthOf(1); + expect(config.getAuditTargetURLsBySource('moneyPages')[0].url).to.equal('https://example.com/mp1'); + }); }); describe('serialization', () => { @@ -3185,6 +3255,7 @@ describe('Config Tests', () => { const item = Config.toDynamoItem(config); expect(item.auditTargetURLs).to.deep.equal({ manual: [{ url: 'https://example.com/page1' }], + moneyPages: [], }); }); @@ -3201,6 +3272,31 @@ describe('Config Tests', () => { const restored = Config.fromDynamoItem(item); expect(restored.getAuditTargetURLs()).to.deep.equal(config.getAuditTargetURLs()); }); + + it('includes moneyPages in toDynamoItem conversion', () => { + const config = Config({ + auditTargetURLs: { + moneyPages: [{ url: 'https://example.com/mp1' }], + }, + }); + const item = Config.toDynamoItem(config); + expect(item.auditTargetURLs).to.deep.equal({ + manual: [], + moneyPages: [{ url: 'https://example.com/mp1' }], + }); + }); + + it('round-trips both manual and moneyPages through serialization', () => { + const config = Config({ + auditTargetURLs: { + manual: [{ url: 'https://example.com/m1' }], + moneyPages: [{ url: 'https://example.com/mp1' }], + }, + }); + const item = Config.toDynamoItem(config); + const restored = Config.fromDynamoItem(item); + expect(restored.getAuditTargetURLs()).to.deep.equal(config.getAuditTargetURLs()); + }); }); describe('field validation', () => { @@ -3212,17 +3308,20 @@ describe('Config Tests', () => { })).to.throw(); }); - it('strips unknown source keys from auditTargetURLs', () => { + it('strips unknown source keys from auditTargetURLs but keeps moneyPages', () => { const config = Config({ auditTargetURLs: { manual: [{ url: 'https://example.com/page1' }], + moneyPages: [{ url: 'https://example.com/mp1' }], unknown: [{ url: 'https://example.com/page2' }], }, }); const result = config.getAuditTargetURLs(); - expect(result).to.have.lengthOf(1); + expect(result).to.have.lengthOf(2); expect(result[0].url).to.equal('https://example.com/page1'); expect(result[0].source).to.equal('manual'); + expect(result[1].url).to.equal('https://example.com/mp1'); + expect(result[1].source).to.equal('moneyPages'); }); }); });