@adobe/spacecat-shared-tokowaka-client 1.18.2 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/index.js +94 -9
- package/test/index.test.js +306 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-tokowaka-client-v1.19.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.19.0...@adobe/spacecat-shared-tokowaka-client-v1.19.1) (2026-06-21)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* apply stale config in IVE edge deployment flow ([#1691](https://github.com/adobe/spacecat-shared/issues/1691)) ([9f07a9c](https://github.com/adobe/spacecat-shared/commit/9f07a9ccc73140249aad16e9c9d0d7f5cededf59))
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-tokowaka-client-v1.19.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.18.2...@adobe/spacecat-shared-tokowaka-client-v1.19.0) (2026-06-03)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **tokowaka-client:** mark path-level suggestions as coveredByDomainWide on domain-wide deploy ([#1648](https://github.com/adobe/spacecat-shared/issues/1648)) ([9cc5992](https://github.com/adobe/spacecat-shared/commit/9cc5992bb98acf9da48a432fedfbec694d30fe23))
|
|
12
|
+
|
|
1
13
|
## [@adobe/spacecat-shared-tokowaka-client-v1.18.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.18.1...@adobe/spacecat-shared-tokowaka-client-v1.18.2) (2026-06-02)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -712,7 +712,8 @@ class TokowakaClient {
|
|
|
712
712
|
* @param {Array} suggestions - Array of suggestion entities to deploy
|
|
713
713
|
* @returns {Promise<Object>} - Deployment result with succeeded/failed suggestions
|
|
714
714
|
*/
|
|
715
|
-
async deploySuggestions(site, opportunity, suggestions) {
|
|
715
|
+
async deploySuggestions(site, opportunity, suggestions, metadata = {}) {
|
|
716
|
+
const { applyStale = false } = metadata;
|
|
716
717
|
const opportunityType = opportunity.getType();
|
|
717
718
|
const baseURL = getEffectiveBaseURL(site);
|
|
718
719
|
const mapper = this.mapperRegistry.getMapper(opportunityType);
|
|
@@ -788,6 +789,10 @@ class TokowakaClient {
|
|
|
788
789
|
continue;
|
|
789
790
|
}
|
|
790
791
|
|
|
792
|
+
if (applyStale && newConfig.patches?.length > 0) {
|
|
793
|
+
newConfig.patches = newConfig.patches.map((patch) => ({ ...patch, applyStale: true }));
|
|
794
|
+
}
|
|
795
|
+
|
|
791
796
|
// Merge with existing config for this URL
|
|
792
797
|
const config = this.mergeConfigs(existingConfig, newConfig);
|
|
793
798
|
|
|
@@ -828,8 +833,15 @@ class TokowakaClient {
|
|
|
828
833
|
* @returns {Promise<number>} Number of patches removed
|
|
829
834
|
* @private
|
|
830
835
|
*/
|
|
831
|
-
|
|
832
|
-
|
|
836
|
+
async #rollbackPerUrlConfig(
|
|
837
|
+
fullUrl,
|
|
838
|
+
urlSuggestions,
|
|
839
|
+
opportunity,
|
|
840
|
+
mapper,
|
|
841
|
+
opportunityType,
|
|
842
|
+
s3Paths,
|
|
843
|
+
rolledBackUrls,
|
|
844
|
+
) {
|
|
833
845
|
const existingConfig = await this.fetchConfig(fullUrl);
|
|
834
846
|
if (!existingConfig) {
|
|
835
847
|
this.log.warn(`No existing configuration found for URL: ${fullUrl}`);
|
|
@@ -1427,9 +1439,11 @@ class TokowakaClient {
|
|
|
1427
1439
|
* @returns {Promise<{ succeededSuggestions: Array, failedSuggestions: Array }>}
|
|
1428
1440
|
* @private
|
|
1429
1441
|
*/
|
|
1430
|
-
|
|
1442
|
+
// eslint-disable-next-line max-len
|
|
1443
|
+
async #deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy, metadata = {}) {
|
|
1431
1444
|
try {
|
|
1432
|
-
|
|
1445
|
+
// eslint-disable-next-line max-len
|
|
1446
|
+
const result = await this.deploySuggestions(site, opportunity, validSuggestions, metadata);
|
|
1433
1447
|
const deploymentTimestamp = Date.now();
|
|
1434
1448
|
|
|
1435
1449
|
const succeeded = result.succeededSuggestions.map((s) => {
|
|
@@ -1532,12 +1546,14 @@ class TokowakaClient {
|
|
|
1532
1546
|
if (!isEdgeDeployableSuggestionStatus(s.getStatus())) {
|
|
1533
1547
|
return false;
|
|
1534
1548
|
}
|
|
1535
|
-
if (isPatternSuggestion(s)) {
|
|
1536
|
-
return false;
|
|
1537
|
-
}
|
|
1538
1549
|
if (s.getData()?.edgeDeployed) {
|
|
1539
1550
|
return false;
|
|
1540
1551
|
}
|
|
1552
|
+
// Path-level pattern suggestions (not domain-wide) are fully covered by a
|
|
1553
|
+
// domain-wide deployment. Other pattern suggestions (including other DW ones) are not.
|
|
1554
|
+
if (isPatternSuggestion(s)) {
|
|
1555
|
+
return coverageField === 'coveredByDomainWide' && !s.getData()?.isDomainWide;
|
|
1556
|
+
}
|
|
1541
1557
|
const url = s.getData()?.url;
|
|
1542
1558
|
return url && matchers.some((match) => match(url));
|
|
1543
1559
|
});
|
|
@@ -1588,6 +1604,7 @@ class TokowakaClient {
|
|
|
1588
1604
|
targetSuggestions,
|
|
1589
1605
|
allSuggestions,
|
|
1590
1606
|
updatedBy = 'edge-deploy',
|
|
1607
|
+
metadata = {},
|
|
1591
1608
|
}) {
|
|
1592
1609
|
// Step 1: classify suggestions into pattern vs per-URL buckets.
|
|
1593
1610
|
// eslint-disable-next-line max-len
|
|
@@ -1607,7 +1624,7 @@ class TokowakaClient {
|
|
|
1607
1624
|
// Step 3: deploy per-URL suggestions via S3.
|
|
1608
1625
|
if (validSuggestions.length > 0) {
|
|
1609
1626
|
// eslint-disable-next-line max-len
|
|
1610
|
-
const result = await this.#deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy);
|
|
1627
|
+
const result = await this.#deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy, metadata);
|
|
1611
1628
|
const { succeededSuggestions: perUrlSucceeded, failedSuggestions: perUrlFailed } = result;
|
|
1612
1629
|
succeededSuggestions = perUrlSucceeded;
|
|
1613
1630
|
failedSuggestions.push(...perUrlFailed);
|
|
@@ -1696,6 +1713,74 @@ class TokowakaClient {
|
|
|
1696
1713
|
|
|
1697
1714
|
return { succeededSuggestions, failedSuggestions, coveredSuggestions };
|
|
1698
1715
|
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Strips the `applyStale` flag from patches in S3 for the given per-URL suggestions.
|
|
1719
|
+
* Called when an experiment completes so edge reverts to normal (non-stale) behaviour.
|
|
1720
|
+
* Pattern / domain-wide suggestions are skipped — they have no per-URL S3 config.
|
|
1721
|
+
* CDN cache is invalidated for every URL whose config was actually modified.
|
|
1722
|
+
*
|
|
1723
|
+
* @param {Object} site - Site entity
|
|
1724
|
+
* @param {Object} opportunity - Opportunity entity
|
|
1725
|
+
* @param {Array} suggestions - Suggestion entities to clear (may include pattern suggestions,
|
|
1726
|
+
* which are silently skipped)
|
|
1727
|
+
* @returns {Promise<void>}
|
|
1728
|
+
*/
|
|
1729
|
+
async clearApplyStaleFromPatches(site, opportunity, suggestions) {
|
|
1730
|
+
const baseURL = getEffectiveBaseURL(site);
|
|
1731
|
+
|
|
1732
|
+
const perUrlSuggestions = suggestions.filter((s) => {
|
|
1733
|
+
const data = s.getData();
|
|
1734
|
+
return data?.url && !Array.isArray(data?.allowedRegexPatterns);
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
if (perUrlSuggestions.length === 0) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const suggestionsByUrl = groupSuggestionsByUrlPath(perUrlSuggestions, baseURL, this.log);
|
|
1742
|
+
const clearedUrls = [];
|
|
1743
|
+
|
|
1744
|
+
for (const [urlPath, urlSuggestions] of Object.entries(suggestionsByUrl)) {
|
|
1745
|
+
const fullUrl = new URL(urlPath, baseURL).toString();
|
|
1746
|
+
try {
|
|
1747
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1748
|
+
const existingConfig = await this.fetchConfig(fullUrl);
|
|
1749
|
+
if (!existingConfig?.patches?.length) {
|
|
1750
|
+
// eslint-disable-next-line no-continue
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const suggestionIds = new Set(urlSuggestions.map((s) => s.getId()));
|
|
1755
|
+
let modified = false;
|
|
1756
|
+
const updatedPatches = existingConfig.patches.map((patch) => {
|
|
1757
|
+
if (suggestionIds.has(patch.suggestionId) && patch.applyStale) {
|
|
1758
|
+
modified = true;
|
|
1759
|
+
const { applyStale: _, ...rest } = patch;
|
|
1760
|
+
return rest;
|
|
1761
|
+
}
|
|
1762
|
+
return patch;
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
if (modified) {
|
|
1766
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1767
|
+
await this.uploadConfig(fullUrl, { ...existingConfig, patches: updatedPatches });
|
|
1768
|
+
clearedUrls.push(fullUrl);
|
|
1769
|
+
}
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
this.log.error(`[clear-apply-stale-failed] Failed to process URL ${fullUrl}: ${err.message}`, err);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (clearedUrls.length > 0) {
|
|
1776
|
+
try {
|
|
1777
|
+
await this.invalidateCdnCache({ urls: clearedUrls });
|
|
1778
|
+
this.log.info(`[clear-apply-stale] Cleared applyStale from ${clearedUrls.length} URL(s) and invalidated CDN`);
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
this.log.warn(`[clear-apply-stale-failed] CDN invalidation failed for ${clearedUrls.length} URL(s), S3 configs already updated: ${err.message}`, err);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1699
1784
|
}
|
|
1700
1785
|
|
|
1701
1786
|
// Export the client as default and base classes for custom implementations
|
package/test/index.test.js
CHANGED
|
@@ -2495,6 +2495,37 @@ describe('TokowakaClient', () => {
|
|
|
2495
2495
|
expect(updatedPatch.suggestionId).to.equal('sugg-1');
|
|
2496
2496
|
expect(updatedPatch.lastUpdated).to.be.greaterThan(1234567890);
|
|
2497
2497
|
});
|
|
2498
|
+
|
|
2499
|
+
it('should add applyStale: true to all patches when applyStale option is true', async () => {
|
|
2500
|
+
const result = await client.deploySuggestions(
|
|
2501
|
+
mockSite,
|
|
2502
|
+
mockOpportunity,
|
|
2503
|
+
mockSuggestions,
|
|
2504
|
+
{ applyStale: true },
|
|
2505
|
+
);
|
|
2506
|
+
|
|
2507
|
+
expect(result.s3Paths).to.have.length(1);
|
|
2508
|
+
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
2509
|
+
expect(uploadedConfig.patches).to.have.length(2);
|
|
2510
|
+
uploadedConfig.patches.forEach((patch) => {
|
|
2511
|
+
expect(patch.applyStale).to.equal(true);
|
|
2512
|
+
});
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
it('should not add applyStale to patches when applyStale option is false', async () => {
|
|
2516
|
+
const result = await client.deploySuggestions(
|
|
2517
|
+
mockSite,
|
|
2518
|
+
mockOpportunity,
|
|
2519
|
+
mockSuggestions,
|
|
2520
|
+
{ applyStale: false },
|
|
2521
|
+
);
|
|
2522
|
+
|
|
2523
|
+
expect(result.s3Paths).to.have.length(1);
|
|
2524
|
+
const uploadedConfig = JSON.parse(s3Client.send.firstCall.args[0].input.Body);
|
|
2525
|
+
uploadedConfig.patches.forEach((patch) => {
|
|
2526
|
+
expect(patch.applyStale).to.be.undefined;
|
|
2527
|
+
});
|
|
2528
|
+
});
|
|
2498
2529
|
});
|
|
2499
2530
|
|
|
2500
2531
|
describe('rollbackSuggestions', () => {
|
|
@@ -3117,6 +3148,55 @@ describe('TokowakaClient', () => {
|
|
|
3117
3148
|
expect(coveredData).to.not.have.property('coveredByDomainWide');
|
|
3118
3149
|
});
|
|
3119
3150
|
|
|
3151
|
+
it('cleans up coveredByDomainWide on path-level suggestions when rolling back domain-wide', async () => {
|
|
3152
|
+
const prerenderOpportunity = { getId: () => 'opp-dw', getType: () => 'prerender' };
|
|
3153
|
+
const dwSuggestion = {
|
|
3154
|
+
getId: () => 'dw-1',
|
|
3155
|
+
getData: () => ({
|
|
3156
|
+
isDomainWide: true,
|
|
3157
|
+
allowedRegexPatterns: ['/*'],
|
|
3158
|
+
edgeDeployed: Date.now(),
|
|
3159
|
+
}),
|
|
3160
|
+
setData: sinon.stub(),
|
|
3161
|
+
setUpdatedBy: sinon.stub(),
|
|
3162
|
+
save: sinon.stub().resolves(),
|
|
3163
|
+
};
|
|
3164
|
+
const pathSuggestion = {
|
|
3165
|
+
getId: () => 'path-1',
|
|
3166
|
+
getData: () => ({
|
|
3167
|
+
allowedRegexPatterns: ['/products/*'],
|
|
3168
|
+
coveredByDomainWide: 'dw-1',
|
|
3169
|
+
}),
|
|
3170
|
+
setData: sinon.stub(),
|
|
3171
|
+
setUpdatedBy: sinon.stub(),
|
|
3172
|
+
save: sinon.stub().resolves(),
|
|
3173
|
+
};
|
|
3174
|
+
|
|
3175
|
+
sinon.stub(client, 'fetchMetaconfig').resolves({
|
|
3176
|
+
siteId: 'site-123',
|
|
3177
|
+
prerender: { allowList: ['/*'] },
|
|
3178
|
+
});
|
|
3179
|
+
sinon.stub(client, 'uploadMetaconfig').resolves();
|
|
3180
|
+
|
|
3181
|
+
const result = await client.rollbackSuggestions(
|
|
3182
|
+
mockSite,
|
|
3183
|
+
prerenderOpportunity,
|
|
3184
|
+
[dwSuggestion],
|
|
3185
|
+
{ allSuggestions: [dwSuggestion, pathSuggestion], updatedBy: 'test@example.com' },
|
|
3186
|
+
);
|
|
3187
|
+
|
|
3188
|
+
expect(result.succeededSuggestions).to.include(dwSuggestion);
|
|
3189
|
+
expect(pathSuggestion.setData.calledOnce).to.be.true;
|
|
3190
|
+
const pathData = pathSuggestion.setData.firstCall.args[0];
|
|
3191
|
+
expect(pathData).to.not.have.property('coveredByDomainWide');
|
|
3192
|
+
expect(pathData).to.have.property('allowedRegexPatterns');
|
|
3193
|
+
// Verify the cleaned-up path suggestion was actually persisted via saveMany
|
|
3194
|
+
expect(client.dataAccess.Suggestion.saveMany).to.have.been.calledWith(
|
|
3195
|
+
sinon.match((arr) => arr.includes(pathSuggestion)),
|
|
3196
|
+
sinon.match.any,
|
|
3197
|
+
);
|
|
3198
|
+
});
|
|
3199
|
+
|
|
3120
3200
|
it('cleans up covered suggestions (coveredByPattern) when rolling back a path-level pattern', async () => {
|
|
3121
3201
|
const prerenderOpportunity = { getId: () => 'opp-p', getType: () => 'prerender' };
|
|
3122
3202
|
const pathSuggestion = {
|
|
@@ -5909,6 +5989,51 @@ describe('TokowakaClient', () => {
|
|
|
5909
5989
|
expect(result.coveredSuggestions).to.not.include(dw2);
|
|
5910
5990
|
});
|
|
5911
5991
|
|
|
5992
|
+
it('should mark path-level pattern suggestions as coveredByDomainWide when domain-wide is deployed', async () => {
|
|
5993
|
+
const dw = makeSuggestion('dw1', {
|
|
5994
|
+
isDomainWide: true,
|
|
5995
|
+
allowedRegexPatterns: ['^https://example\\.com/.*'],
|
|
5996
|
+
});
|
|
5997
|
+
const pathLevel = makeSuggestion('path1', {
|
|
5998
|
+
allowedRegexPatterns: ['/products/*'],
|
|
5999
|
+
});
|
|
6000
|
+
|
|
6001
|
+
fetchMetaconfigStub.resolves({ siteId: 'site-123' });
|
|
6002
|
+
|
|
6003
|
+
const result = await client.deployToEdge({
|
|
6004
|
+
site: mockSite,
|
|
6005
|
+
opportunity: mockOpportunity,
|
|
6006
|
+
targetSuggestions: [dw],
|
|
6007
|
+
allSuggestions: [dw, pathLevel],
|
|
6008
|
+
});
|
|
6009
|
+
|
|
6010
|
+
expect(result.coveredSuggestions).to.include(pathLevel);
|
|
6011
|
+
expect(pathLevel.getData()).to.have.property('coveredByDomainWide', 'dw1');
|
|
6012
|
+
});
|
|
6013
|
+
|
|
6014
|
+
it('should not mark already-deployed path-level suggestions as coveredByDomainWide', async () => {
|
|
6015
|
+
const dw = makeSuggestion('dw1', {
|
|
6016
|
+
isDomainWide: true,
|
|
6017
|
+
allowedRegexPatterns: ['^https://example\\.com/.*'],
|
|
6018
|
+
});
|
|
6019
|
+
const pathLevel = makeSuggestion('path1', {
|
|
6020
|
+
allowedRegexPatterns: ['/products/*'],
|
|
6021
|
+
edgeDeployed: Date.now(),
|
|
6022
|
+
});
|
|
6023
|
+
|
|
6024
|
+
fetchMetaconfigStub.resolves({ siteId: 'site-123' });
|
|
6025
|
+
|
|
6026
|
+
const result = await client.deployToEdge({
|
|
6027
|
+
site: mockSite,
|
|
6028
|
+
opportunity: mockOpportunity,
|
|
6029
|
+
targetSuggestions: [dw],
|
|
6030
|
+
allSuggestions: [dw, pathLevel],
|
|
6031
|
+
});
|
|
6032
|
+
|
|
6033
|
+
expect(result.coveredSuggestions).to.not.include(pathLevel);
|
|
6034
|
+
expect(pathLevel.getData()).to.not.have.property('coveredByDomainWide');
|
|
6035
|
+
});
|
|
6036
|
+
|
|
5912
6037
|
it('should not mark non-NEW suggestions as covered', async () => {
|
|
5913
6038
|
const dw = makeSuggestion('dw1', {
|
|
5914
6039
|
isDomainWide: true,
|
|
@@ -6524,5 +6649,186 @@ describe('TokowakaClient', () => {
|
|
|
6524
6649
|
expect(result.failedSuggestions[0].reason).to.equal('Internal server error');
|
|
6525
6650
|
expect(log.error).to.have.been.called;
|
|
6526
6651
|
});
|
|
6652
|
+
|
|
6653
|
+
it('should pass applyStale: true to deploySuggestions when metadata.applyStale is true', async () => {
|
|
6654
|
+
const s1 = makeSuggestion('s1', { url: 'https://example.com/page1', transformRules: {} });
|
|
6655
|
+
|
|
6656
|
+
deploySuggestionsStub.resolves({
|
|
6657
|
+
succeededSuggestions: [s1],
|
|
6658
|
+
failedSuggestions: [],
|
|
6659
|
+
});
|
|
6660
|
+
|
|
6661
|
+
await client.deployToEdge({
|
|
6662
|
+
site: mockSite,
|
|
6663
|
+
opportunity: mockOpportunity,
|
|
6664
|
+
targetSuggestions: [s1],
|
|
6665
|
+
allSuggestions: [s1],
|
|
6666
|
+
metadata: { applyStale: true },
|
|
6667
|
+
});
|
|
6668
|
+
|
|
6669
|
+
expect(deploySuggestionsStub).to.have.been.calledOnce;
|
|
6670
|
+
const [,, , metadata] = deploySuggestionsStub.firstCall.args;
|
|
6671
|
+
expect(metadata).to.deep.equal({ applyStale: true });
|
|
6672
|
+
});
|
|
6673
|
+
|
|
6674
|
+
it('should pass empty metadata to deploySuggestions when metadata is absent', async () => {
|
|
6675
|
+
const s1 = makeSuggestion('s1', { url: 'https://example.com/page1', transformRules: {} });
|
|
6676
|
+
|
|
6677
|
+
deploySuggestionsStub.resolves({
|
|
6678
|
+
succeededSuggestions: [s1],
|
|
6679
|
+
failedSuggestions: [],
|
|
6680
|
+
});
|
|
6681
|
+
|
|
6682
|
+
await client.deployToEdge({
|
|
6683
|
+
site: mockSite,
|
|
6684
|
+
opportunity: mockOpportunity,
|
|
6685
|
+
targetSuggestions: [s1],
|
|
6686
|
+
allSuggestions: [s1],
|
|
6687
|
+
});
|
|
6688
|
+
|
|
6689
|
+
expect(deploySuggestionsStub).to.have.been.calledOnce;
|
|
6690
|
+
const [,, , metadata] = deploySuggestionsStub.firstCall.args;
|
|
6691
|
+
expect(metadata).to.deep.equal({});
|
|
6692
|
+
});
|
|
6693
|
+
});
|
|
6694
|
+
|
|
6695
|
+
describe('clearApplyStaleFromPatches', () => {
|
|
6696
|
+
let fetchConfigStub;
|
|
6697
|
+
let uploadConfigStub;
|
|
6698
|
+
let invalidateCdnCacheStub;
|
|
6699
|
+
|
|
6700
|
+
function makeCompletedSuggestion(id, urlPath) {
|
|
6701
|
+
return {
|
|
6702
|
+
getId: () => id,
|
|
6703
|
+
getData: () => ({ url: `https://example.com${urlPath}`, edgeDeployed: Date.now() }),
|
|
6704
|
+
};
|
|
6705
|
+
}
|
|
6706
|
+
|
|
6707
|
+
beforeEach(() => {
|
|
6708
|
+
fetchConfigStub = sinon.stub(client, 'fetchConfig');
|
|
6709
|
+
uploadConfigStub = sinon.stub(client, 'uploadConfig').resolves('s3-path');
|
|
6710
|
+
invalidateCdnCacheStub = sinon.stub(client, 'invalidateCdnCache').resolves([]);
|
|
6711
|
+
});
|
|
6712
|
+
|
|
6713
|
+
it('should strip applyStale from matching patches and invalidate CDN', async () => {
|
|
6714
|
+
fetchConfigStub.resolves({
|
|
6715
|
+
url: 'https://example.com/page1',
|
|
6716
|
+
version: '1.0',
|
|
6717
|
+
patches: [
|
|
6718
|
+
{
|
|
6719
|
+
op: 'replace', selector: 'h1', value: 'New', suggestionId: 'sugg-1', applyStale: true,
|
|
6720
|
+
},
|
|
6721
|
+
{
|
|
6722
|
+
op: 'replace', selector: 'h2', value: 'Other', suggestionId: 'sugg-2', applyStale: true,
|
|
6723
|
+
},
|
|
6724
|
+
],
|
|
6725
|
+
});
|
|
6726
|
+
|
|
6727
|
+
const s1 = makeCompletedSuggestion('sugg-1', '/page1');
|
|
6728
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, [s1]);
|
|
6729
|
+
|
|
6730
|
+
expect(uploadConfigStub).to.have.been.calledOnce;
|
|
6731
|
+
const [, uploadedConfig] = uploadConfigStub.firstCall.args;
|
|
6732
|
+
const patch1 = uploadedConfig.patches.find((p) => p.suggestionId === 'sugg-1');
|
|
6733
|
+
expect(patch1).to.not.have.property('applyStale');
|
|
6734
|
+
// sugg-2 not in suggestions list — applyStale untouched
|
|
6735
|
+
const patch2 = uploadedConfig.patches.find((p) => p.suggestionId === 'sugg-2');
|
|
6736
|
+
expect(patch2.applyStale).to.equal(true);
|
|
6737
|
+
expect(invalidateCdnCacheStub).to.have.been.calledOnce;
|
|
6738
|
+
});
|
|
6739
|
+
|
|
6740
|
+
it('should skip upload when no patches have applyStale', async () => {
|
|
6741
|
+
fetchConfigStub.resolves({
|
|
6742
|
+
url: 'https://example.com/page1',
|
|
6743
|
+
version: '1.0',
|
|
6744
|
+
patches: [
|
|
6745
|
+
{
|
|
6746
|
+
op: 'replace', selector: 'h1', value: 'New', suggestionId: 'sugg-1',
|
|
6747
|
+
},
|
|
6748
|
+
],
|
|
6749
|
+
});
|
|
6750
|
+
|
|
6751
|
+
const s1 = makeCompletedSuggestion('sugg-1', '/page1');
|
|
6752
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, [s1]);
|
|
6753
|
+
|
|
6754
|
+
expect(uploadConfigStub).to.not.have.been.called;
|
|
6755
|
+
expect(invalidateCdnCacheStub).to.not.have.been.called;
|
|
6756
|
+
});
|
|
6757
|
+
|
|
6758
|
+
it('should skip URLs with no existing config', async () => {
|
|
6759
|
+
fetchConfigStub.resolves(null);
|
|
6760
|
+
|
|
6761
|
+
const s1 = makeCompletedSuggestion('sugg-1', '/page1');
|
|
6762
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, [s1]);
|
|
6763
|
+
|
|
6764
|
+
expect(uploadConfigStub).to.not.have.been.called;
|
|
6765
|
+
expect(invalidateCdnCacheStub).to.not.have.been.called;
|
|
6766
|
+
});
|
|
6767
|
+
|
|
6768
|
+
it('should silently skip pattern and domain-wide suggestions', async () => {
|
|
6769
|
+
const patternSugg = {
|
|
6770
|
+
getId: () => 'dw-1',
|
|
6771
|
+
getData: () => ({ isDomainWide: true, allowedRegexPatterns: ['/.*'] }),
|
|
6772
|
+
};
|
|
6773
|
+
|
|
6774
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, [patternSugg]);
|
|
6775
|
+
|
|
6776
|
+
expect(fetchConfigStub).to.not.have.been.called;
|
|
6777
|
+
expect(uploadConfigStub).to.not.have.been.called;
|
|
6778
|
+
});
|
|
6779
|
+
|
|
6780
|
+
it('should handle multiple URLs, only invalidate ones that changed', async () => {
|
|
6781
|
+
fetchConfigStub.onFirstCall().resolves({
|
|
6782
|
+
patches: [{ suggestionId: 'sugg-1', applyStale: true }],
|
|
6783
|
+
});
|
|
6784
|
+
fetchConfigStub.onSecondCall().resolves({
|
|
6785
|
+
patches: [{ suggestionId: 'sugg-2' }],
|
|
6786
|
+
});
|
|
6787
|
+
|
|
6788
|
+
const s1 = makeCompletedSuggestion('sugg-1', '/page1');
|
|
6789
|
+
const s2 = makeCompletedSuggestion('sugg-2', '/page2');
|
|
6790
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, [s1, s2]);
|
|
6791
|
+
|
|
6792
|
+
expect(uploadConfigStub).to.have.been.calledOnce;
|
|
6793
|
+
expect(invalidateCdnCacheStub).to.have.been.calledOnce;
|
|
6794
|
+
const { urls } = invalidateCdnCacheStub.firstCall.args[0];
|
|
6795
|
+
expect(urls).to.have.length(1);
|
|
6796
|
+
expect(urls[0]).to.include('/page1');
|
|
6797
|
+
});
|
|
6798
|
+
|
|
6799
|
+
it('should do nothing when given no per-URL suggestions', async () => {
|
|
6800
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, []);
|
|
6801
|
+
|
|
6802
|
+
expect(fetchConfigStub).to.not.have.been.called;
|
|
6803
|
+
});
|
|
6804
|
+
|
|
6805
|
+
it('should log warning and return normally when CDN invalidation throws', async () => {
|
|
6806
|
+
fetchConfigStub.resolves({
|
|
6807
|
+
patches: [{ suggestionId: 'sugg-1', applyStale: true }],
|
|
6808
|
+
});
|
|
6809
|
+
invalidateCdnCacheStub.rejects(new Error('CDN rate limit'));
|
|
6810
|
+
|
|
6811
|
+
const s1 = makeCompletedSuggestion('sugg-1', '/page1');
|
|
6812
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, [s1]);
|
|
6813
|
+
|
|
6814
|
+
expect(uploadConfigStub).to.have.been.calledOnce;
|
|
6815
|
+
});
|
|
6816
|
+
|
|
6817
|
+
it('should log error and continue processing remaining URLs when one URL throws', async () => {
|
|
6818
|
+
fetchConfigStub.onFirstCall().rejects(new Error('S3 transient error'));
|
|
6819
|
+
fetchConfigStub.onSecondCall().resolves({
|
|
6820
|
+
patches: [{ suggestionId: 'sugg-2', applyStale: true }],
|
|
6821
|
+
});
|
|
6822
|
+
|
|
6823
|
+
const s1 = makeCompletedSuggestion('sugg-1', '/page1');
|
|
6824
|
+
const s2 = makeCompletedSuggestion('sugg-2', '/page2');
|
|
6825
|
+
await client.clearApplyStaleFromPatches(mockSite, mockOpportunity, [s1, s2]);
|
|
6826
|
+
|
|
6827
|
+
expect(uploadConfigStub).to.have.been.calledOnce;
|
|
6828
|
+
expect(invalidateCdnCacheStub).to.have.been.calledOnce;
|
|
6829
|
+
const { urls } = invalidateCdnCacheStub.firstCall.args[0];
|
|
6830
|
+
expect(urls).to.have.length(1);
|
|
6831
|
+
expect(urls[0]).to.include('/page2');
|
|
6832
|
+
});
|
|
6527
6833
|
});
|
|
6528
6834
|
});
|