@adobe/spacecat-shared-tokowaka-client 1.18.1 → 1.19.0

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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## [@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)
2
+
3
+ ### Features
4
+
5
+ * **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))
6
+
7
+ ## [@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)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **tokowaka-client:** use scraperConfig headers in edge optimize probe (LLMO-5280) ([#1647](https://github.com/adobe/spacecat-shared/issues/1647)) ([6be14a1](https://github.com/adobe/spacecat-shared/commit/6be14a153ea8d9de010880afaa1c3c9290a32f13))
12
+
1
13
  ## [@adobe/spacecat-shared-tokowaka-client-v1.18.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.18.0...@adobe/spacecat-shared-tokowaka-client-v1.18.1) (2026-06-01)
2
14
 
3
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.js CHANGED
@@ -1287,10 +1287,18 @@ class TokowakaClient {
1287
1287
  return { edgeOptimizeEnabled: true };
1288
1288
  }
1289
1289
 
1290
+ const defaultProbeHeaders = {
1291
+ // eslint-disable-next-line max-len
1292
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Tokowaka-AI Tokowaka/1.0 AdobeEdgeOptimize-AI AdobeEdgeOptimize/1.0',
1293
+ 'fastly-debug': '1',
1294
+ };
1295
+ const customHeaders = currentConfig?.getScraperConfig?.()?.headers ?? {};
1296
+ const probeHeaders = { ...customHeaders, ...defaultProbeHeaders };
1297
+
1290
1298
  const baseURL = getEffectiveBaseURL(site);
1291
1299
  const targetUrl = new URL(path, baseURL).toString();
1292
1300
 
1293
- this.log.info(`Checking edge optimize status for ${targetUrl}`);
1301
+ this.log.info(`[edge-optimize-status] Checking edge optimize status for ${targetUrl}`);
1294
1302
 
1295
1303
  const maxRetries = 3;
1296
1304
  let attempt = 0;
@@ -1300,25 +1308,21 @@ class TokowakaClient {
1300
1308
  while (attempt <= maxRetries) {
1301
1309
  try {
1302
1310
  // eslint-disable-next-line max-len
1303
- this.log.debug(`Attempt ${attempt + 1}/${maxRetries + 1}: Checking edge optimize status for ${targetUrl}`);
1311
+ this.log.debug(`[edge-optimize-status] Attempt ${attempt + 1}/${maxRetries + 1}: Checking edge optimize status for ${targetUrl}`);
1304
1312
 
1305
1313
  // eslint-disable-next-line no-await-in-loop
1306
1314
  const response = await tracingFetch(targetUrl, {
1307
1315
  method: 'GET',
1308
- headers: {
1309
- // eslint-disable-next-line max-len
1310
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Tokowaka-AI Tokowaka/1.0 AdobeEdgeOptimize-AI AdobeEdgeOptimize/1.0',
1311
- 'fastly-debug': '1',
1312
- },
1316
+ headers: probeHeaders,
1313
1317
  timeout: REQUEST_TIMEOUT_MS,
1314
1318
  });
1315
1319
 
1316
- this.log.debug(`Response status: ${response.status}`);
1320
+ this.log.info(`[edge-optimize-status] Response status: ${response.status} for ${targetUrl}`);
1317
1321
 
1318
1322
  const edgeOptimizeEnabled = response.headers.get('x-tokowaka-request-id') !== null
1319
1323
  || response.headers.get('x-edgeoptimize-request-id') !== null;
1320
1324
 
1321
- this.log.debug(`Edge optimize headers found: ${edgeOptimizeEnabled}`);
1325
+ this.log.info(`[edge-optimize-status] Edge optimize headers found: ${edgeOptimizeEnabled} for ${targetUrl}`);
1322
1326
 
1323
1327
  return {
1324
1328
  edgeOptimizeEnabled,
@@ -1328,7 +1332,7 @@ class TokowakaClient {
1328
1332
 
1329
1333
  if (isTimeout) {
1330
1334
  // eslint-disable-next-line max-len
1331
- this.log.warn(`Request timed out after ${REQUEST_TIMEOUT_MS}ms for ${targetUrl}, returning edgeOptimizeEnabled: false`);
1335
+ this.log.warn(`[edge-optimize-status] Request timed out after ${REQUEST_TIMEOUT_MS}ms for ${targetUrl}, returning edgeOptimizeEnabled: false`);
1332
1336
  return { edgeOptimizeEnabled: false };
1333
1337
  }
1334
1338
 
@@ -1336,7 +1340,7 @@ class TokowakaClient {
1336
1340
 
1337
1341
  if (attempt > maxRetries) {
1338
1342
  // All retries exhausted
1339
- this.log.error(`Failed after ${maxRetries + 1} attempts: ${error.message}`);
1343
+ this.log.error(`[edge-optimize-status] Failed after ${maxRetries + 1} attempts: ${error.message} for ${targetUrl}`);
1340
1344
  throw this.#createError(
1341
1345
  `Failed to check edge optimize status: ${error.message}`,
1342
1346
  HTTP_INTERNAL_SERVER_ERROR,
@@ -1346,7 +1350,7 @@ class TokowakaClient {
1346
1350
  // Exponential backoff: 200ms, 400ms, 800ms
1347
1351
  const delay = 100 * (2 ** attempt);
1348
1352
  this.log.warn(
1349
- `Attempt ${attempt} to fetch failed: ${error.message}. Retrying in ${delay}ms...`,
1353
+ `[edge-optimize-status] Attempt ${attempt} to fetch failed: ${error.message}. Retrying in ${delay}ms... for ${targetUrl}`,
1350
1354
  );
1351
1355
  // eslint-disable-next-line no-await-in-loop
1352
1356
  await new Promise((res) => {
@@ -1528,12 +1532,14 @@ class TokowakaClient {
1528
1532
  if (!isEdgeDeployableSuggestionStatus(s.getStatus())) {
1529
1533
  return false;
1530
1534
  }
1531
- if (isPatternSuggestion(s)) {
1532
- return false;
1533
- }
1534
1535
  if (s.getData()?.edgeDeployed) {
1535
1536
  return false;
1536
1537
  }
1538
+ // Path-level pattern suggestions (not domain-wide) are fully covered by a
1539
+ // domain-wide deployment. Other pattern suggestions (including other DW ones) are not.
1540
+ if (isPatternSuggestion(s)) {
1541
+ return coverageField === 'coveredByDomainWide' && !s.getData()?.isDomainWide;
1542
+ }
1537
1543
  const url = s.getData()?.url;
1538
1544
  return url && matchers.some((match) => match(url));
1539
1545
  });
@@ -3117,6 +3117,55 @@ describe('TokowakaClient', () => {
3117
3117
  expect(coveredData).to.not.have.property('coveredByDomainWide');
3118
3118
  });
3119
3119
 
3120
+ it('cleans up coveredByDomainWide on path-level suggestions when rolling back domain-wide', async () => {
3121
+ const prerenderOpportunity = { getId: () => 'opp-dw', getType: () => 'prerender' };
3122
+ const dwSuggestion = {
3123
+ getId: () => 'dw-1',
3124
+ getData: () => ({
3125
+ isDomainWide: true,
3126
+ allowedRegexPatterns: ['/*'],
3127
+ edgeDeployed: Date.now(),
3128
+ }),
3129
+ setData: sinon.stub(),
3130
+ setUpdatedBy: sinon.stub(),
3131
+ save: sinon.stub().resolves(),
3132
+ };
3133
+ const pathSuggestion = {
3134
+ getId: () => 'path-1',
3135
+ getData: () => ({
3136
+ allowedRegexPatterns: ['/products/*'],
3137
+ coveredByDomainWide: 'dw-1',
3138
+ }),
3139
+ setData: sinon.stub(),
3140
+ setUpdatedBy: sinon.stub(),
3141
+ save: sinon.stub().resolves(),
3142
+ };
3143
+
3144
+ sinon.stub(client, 'fetchMetaconfig').resolves({
3145
+ siteId: 'site-123',
3146
+ prerender: { allowList: ['/*'] },
3147
+ });
3148
+ sinon.stub(client, 'uploadMetaconfig').resolves();
3149
+
3150
+ const result = await client.rollbackSuggestions(
3151
+ mockSite,
3152
+ prerenderOpportunity,
3153
+ [dwSuggestion],
3154
+ { allSuggestions: [dwSuggestion, pathSuggestion], updatedBy: 'test@example.com' },
3155
+ );
3156
+
3157
+ expect(result.succeededSuggestions).to.include(dwSuggestion);
3158
+ expect(pathSuggestion.setData.calledOnce).to.be.true;
3159
+ const pathData = pathSuggestion.setData.firstCall.args[0];
3160
+ expect(pathData).to.not.have.property('coveredByDomainWide');
3161
+ expect(pathData).to.have.property('allowedRegexPatterns');
3162
+ // Verify the cleaned-up path suggestion was actually persisted via saveMany
3163
+ expect(client.dataAccess.Suggestion.saveMany).to.have.been.calledWith(
3164
+ sinon.match((arr) => arr.includes(pathSuggestion)),
3165
+ sinon.match.any,
3166
+ );
3167
+ });
3168
+
3120
3169
  it('cleans up covered suggestions (coveredByPattern) when rolling back a path-level pattern', async () => {
3121
3170
  const prerenderOpportunity = { getId: () => 'opp-p', getType: () => 'prerender' };
3122
3171
  const pathSuggestion = {
@@ -5035,6 +5084,165 @@ describe('TokowakaClient', () => {
5035
5084
  expect(userAgent).to.include('Mozilla/5.0');
5036
5085
  });
5037
5086
 
5087
+ it('should ignore custom User-Agent from scraperConfig — default takes priority', async () => {
5088
+ const customSite = {
5089
+ getId: () => 'site-id',
5090
+ getBaseURL: () => 'https://example.com',
5091
+ getConfig: () => ({
5092
+ getEdgeOptimizeConfig: () => undefined,
5093
+ getScraperConfig: () => ({ headers: { 'User-Agent': 'AllowListedBot/1.0' } }),
5094
+ }),
5095
+ getDeliveryType: () => 'aem_edge',
5096
+ };
5097
+ const mockResponse = {
5098
+ status: 200,
5099
+ headers: new Map(),
5100
+ };
5101
+ mockResponse.headers.get = () => null;
5102
+
5103
+ tracingFetchStub.resolves(mockResponse);
5104
+
5105
+ await esmockClient.checkEdgeOptimizeStatus(customSite, '/');
5106
+
5107
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
5108
+ expect(fetchOptions.headers['User-Agent']).to.include('Tokowaka-AI');
5109
+ expect(fetchOptions.headers['User-Agent']).to.not.include('AllowListedBot/1.0');
5110
+ });
5111
+
5112
+ it('should add extra custom headers while defaults always win on collision', async () => {
5113
+ const customSite = {
5114
+ getId: () => 'site-id',
5115
+ getBaseURL: () => 'https://example.com',
5116
+ getConfig: () => ({
5117
+ getEdgeOptimizeConfig: () => undefined,
5118
+ getScraperConfig: () => ({ headers: { 'x-allowlist-token': 'secret123', 'fastly-debug': 'custom' } }),
5119
+ }),
5120
+ getDeliveryType: () => 'aem_edge',
5121
+ };
5122
+ const mockResponse = {
5123
+ status: 200,
5124
+ headers: new Map(),
5125
+ };
5126
+ mockResponse.headers.get = () => null;
5127
+
5128
+ tracingFetchStub.resolves(mockResponse);
5129
+
5130
+ await esmockClient.checkEdgeOptimizeStatus(customSite, '/');
5131
+
5132
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
5133
+ expect(fetchOptions.headers['x-allowlist-token']).to.equal('secret123');
5134
+ expect(fetchOptions.headers['fastly-debug']).to.equal('1');
5135
+ expect(fetchOptions.headers['User-Agent']).to.include('Mozilla/5.0');
5136
+ });
5137
+
5138
+ it('should use default probe headers when scraperConfig has no headers', async () => {
5139
+ const noHeadersSite = {
5140
+ getId: () => 'site-id',
5141
+ getBaseURL: () => 'https://example.com',
5142
+ getConfig: () => ({
5143
+ getEdgeOptimizeConfig: () => undefined,
5144
+ getScraperConfig: () => ({}),
5145
+ }),
5146
+ getDeliveryType: () => 'aem_edge',
5147
+ };
5148
+ const mockResponse = {
5149
+ status: 200,
5150
+ headers: new Map(),
5151
+ };
5152
+ mockResponse.headers.get = () => null;
5153
+
5154
+ tracingFetchStub.resolves(mockResponse);
5155
+
5156
+ await esmockClient.checkEdgeOptimizeStatus(noHeadersSite, '/');
5157
+
5158
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
5159
+ expect(fetchOptions.headers['User-Agent']).to.include('Tokowaka-AI');
5160
+ expect(fetchOptions.headers['fastly-debug']).to.equal('1');
5161
+ });
5162
+
5163
+ it('should use default probe headers when getScraperConfig is not present on config', async () => {
5164
+ const noScraperSite = {
5165
+ getId: () => 'site-id',
5166
+ getBaseURL: () => 'https://example.com',
5167
+ getConfig: () => ({ getEdgeOptimizeConfig: () => undefined }),
5168
+ getDeliveryType: () => 'aem_edge',
5169
+ };
5170
+ const mockResponse = {
5171
+ status: 200,
5172
+ headers: new Map(),
5173
+ };
5174
+ mockResponse.headers.get = () => null;
5175
+
5176
+ tracingFetchStub.resolves(mockResponse);
5177
+
5178
+ await esmockClient.checkEdgeOptimizeStatus(noScraperSite, '/');
5179
+
5180
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
5181
+ expect(fetchOptions.headers['User-Agent']).to.include('AdobeEdgeOptimize-AI');
5182
+ expect(fetchOptions.headers['fastly-debug']).to.equal('1');
5183
+ });
5184
+
5185
+ it('should use default probe headers when getScraperConfig returns null', async () => {
5186
+ const nullScraperSite = {
5187
+ getId: () => 'site-id',
5188
+ getBaseURL: () => 'https://example.com',
5189
+ getConfig: () => ({
5190
+ getEdgeOptimizeConfig: () => undefined,
5191
+ getScraperConfig: () => null,
5192
+ }),
5193
+ getDeliveryType: () => 'aem_edge',
5194
+ };
5195
+ const mockResponse = { status: 200, headers: new Map() };
5196
+ mockResponse.headers.get = () => null;
5197
+ tracingFetchStub.resolves(mockResponse);
5198
+
5199
+ await esmockClient.checkEdgeOptimizeStatus(nullScraperSite, '/');
5200
+
5201
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
5202
+ expect(fetchOptions.headers['User-Agent']).to.include('Tokowaka-AI');
5203
+ expect(fetchOptions.headers['fastly-debug']).to.equal('1');
5204
+ });
5205
+
5206
+ it('should use default probe headers when getConfig returns null', async () => {
5207
+ const nullConfigSite = {
5208
+ getId: () => 'site-id',
5209
+ getBaseURL: () => 'https://example.com',
5210
+ getConfig: () => null,
5211
+ getDeliveryType: () => 'aem_edge',
5212
+ };
5213
+ const mockResponse = { status: 200, headers: new Map() };
5214
+ mockResponse.headers.get = () => null;
5215
+ tracingFetchStub.resolves(mockResponse);
5216
+
5217
+ await esmockClient.checkEdgeOptimizeStatus(nullConfigSite, '/');
5218
+
5219
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
5220
+ expect(fetchOptions.headers['User-Agent']).to.include('Tokowaka-AI');
5221
+ expect(fetchOptions.headers['fastly-debug']).to.equal('1');
5222
+ });
5223
+
5224
+ it('should use default probe headers when scraperConfig has an empty headers object', async () => {
5225
+ const emptyHeadersSite = {
5226
+ getId: () => 'site-id',
5227
+ getBaseURL: () => 'https://example.com',
5228
+ getConfig: () => ({
5229
+ getEdgeOptimizeConfig: () => undefined,
5230
+ getScraperConfig: () => ({ headers: {} }),
5231
+ }),
5232
+ getDeliveryType: () => 'aem_edge',
5233
+ };
5234
+ const mockResponse = { status: 200, headers: new Map() };
5235
+ mockResponse.headers.get = () => null;
5236
+ tracingFetchStub.resolves(mockResponse);
5237
+
5238
+ await esmockClient.checkEdgeOptimizeStatus(emptyHeadersSite, '/');
5239
+
5240
+ const fetchOptions = tracingFetchStub.firstCall.args[1];
5241
+ expect(fetchOptions.headers['User-Agent']).to.include('Tokowaka-AI');
5242
+ expect(fetchOptions.headers['fastly-debug']).to.equal('1');
5243
+ expect(Object.keys(fetchOptions.headers)).to.have.lengthOf(2);
5244
+ });
5245
+
5038
5246
  it('should pass timeout option to tracingFetch', async () => {
5039
5247
  const mockResponse = {
5040
5248
  status: 200,
@@ -5168,7 +5376,7 @@ describe('TokowakaClient', () => {
5168
5376
  await promise;
5169
5377
 
5170
5378
  expect(log.warn).to.have.been.calledWith(
5171
- sinon.match(/Attempt 1 to fetch failed.*Connection refused.*Retrying in 200ms/),
5379
+ sinon.match(/\[edge-optimize-status\] Attempt 1 to fetch failed.*Connection refused.*Retrying in 200ms/),
5172
5380
  );
5173
5381
  });
5174
5382
 
@@ -5182,9 +5390,38 @@ describe('TokowakaClient', () => {
5182
5390
  expect(result).to.deep.equal({ edgeOptimizeEnabled: false });
5183
5391
  expect(tracingFetchStub).to.have.been.calledOnce;
5184
5392
  expect(log.warn).to.have.been.calledWith(
5185
- sinon.match(/Request timed out after 5000ms for https:\/\/example.com\/, returning edgeOptimizeEnabled: false/),
5393
+ sinon.match(/\[edge-optimize-status\] Request timed out after 5000ms for https:\/\/example.com\/, returning edgeOptimizeEnabled: false/),
5186
5394
  );
5187
5395
  });
5396
+
5397
+ it('should send the same custom headers on every retry attempt', async () => {
5398
+ const customSite = {
5399
+ getId: () => 'site-id',
5400
+ getBaseURL: () => 'https://example.com',
5401
+ getConfig: () => ({
5402
+ getEdgeOptimizeConfig: () => undefined,
5403
+ getScraperConfig: () => ({ headers: { 'x-allowlist-token': 'secret123' } }),
5404
+ }),
5405
+ getDeliveryType: () => 'aem_edge',
5406
+ };
5407
+ const mockResponse = { status: 200, headers: new Map() };
5408
+ mockResponse.headers.get = () => null;
5409
+
5410
+ tracingFetchStub.onFirstCall().rejects(new Error('Temporary failure'));
5411
+ tracingFetchStub.onSecondCall().resolves(mockResponse);
5412
+
5413
+ const promise = esmockClient.checkEdgeOptimizeStatus(customSite, '/');
5414
+ await clock.tickAsync(200);
5415
+ await promise;
5416
+
5417
+ expect(tracingFetchStub).to.have.been.calledTwice;
5418
+ const firstCallHeaders = tracingFetchStub.firstCall.args[1].headers;
5419
+ const secondCallHeaders = tracingFetchStub.secondCall.args[1].headers;
5420
+ expect(firstCallHeaders['x-allowlist-token']).to.equal('secret123');
5421
+ expect(secondCallHeaders['x-allowlist-token']).to.equal('secret123');
5422
+ expect(firstCallHeaders['User-Agent']).to.include('Tokowaka-AI');
5423
+ expect(secondCallHeaders['User-Agent']).to.include('Tokowaka-AI');
5424
+ });
5188
5425
  });
5189
5426
 
5190
5427
  describe('URL Construction', () => {
@@ -5721,6 +5958,51 @@ describe('TokowakaClient', () => {
5721
5958
  expect(result.coveredSuggestions).to.not.include(dw2);
5722
5959
  });
5723
5960
 
5961
+ it('should mark path-level pattern suggestions as coveredByDomainWide when domain-wide is deployed', async () => {
5962
+ const dw = makeSuggestion('dw1', {
5963
+ isDomainWide: true,
5964
+ allowedRegexPatterns: ['^https://example\\.com/.*'],
5965
+ });
5966
+ const pathLevel = makeSuggestion('path1', {
5967
+ allowedRegexPatterns: ['/products/*'],
5968
+ });
5969
+
5970
+ fetchMetaconfigStub.resolves({ siteId: 'site-123' });
5971
+
5972
+ const result = await client.deployToEdge({
5973
+ site: mockSite,
5974
+ opportunity: mockOpportunity,
5975
+ targetSuggestions: [dw],
5976
+ allSuggestions: [dw, pathLevel],
5977
+ });
5978
+
5979
+ expect(result.coveredSuggestions).to.include(pathLevel);
5980
+ expect(pathLevel.getData()).to.have.property('coveredByDomainWide', 'dw1');
5981
+ });
5982
+
5983
+ it('should not mark already-deployed path-level suggestions as coveredByDomainWide', async () => {
5984
+ const dw = makeSuggestion('dw1', {
5985
+ isDomainWide: true,
5986
+ allowedRegexPatterns: ['^https://example\\.com/.*'],
5987
+ });
5988
+ const pathLevel = makeSuggestion('path1', {
5989
+ allowedRegexPatterns: ['/products/*'],
5990
+ edgeDeployed: Date.now(),
5991
+ });
5992
+
5993
+ fetchMetaconfigStub.resolves({ siteId: 'site-123' });
5994
+
5995
+ const result = await client.deployToEdge({
5996
+ site: mockSite,
5997
+ opportunity: mockOpportunity,
5998
+ targetSuggestions: [dw],
5999
+ allSuggestions: [dw, pathLevel],
6000
+ });
6001
+
6002
+ expect(result.coveredSuggestions).to.not.include(pathLevel);
6003
+ expect(pathLevel.getData()).to.not.have.property('coveredByDomainWide');
6004
+ });
6005
+
5724
6006
  it('should not mark non-NEW suggestions as covered', async () => {
5725
6007
  const dw = makeSuggestion('dw1', {
5726
6008
  isDomainWide: true,