@adobe/spacecat-shared-tokowaka-client 1.1.1 → 1.2.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/.releaserc.cjs +17 -0
- package/CHANGELOG.md +69 -1
- package/CODE_OF_CONDUCT.md +75 -0
- package/CONTRIBUTING.md +74 -0
- package/README.md +155 -15
- package/package.json +4 -4
- package/src/index.d.ts +120 -25
- package/src/index.js +481 -177
- package/src/mappers/base-mapper.js +41 -9
- package/src/mappers/content-summarization-mapper.js +38 -35
- package/src/mappers/faq-mapper.js +247 -0
- package/src/mappers/headings-mapper.js +37 -23
- package/src/mappers/mapper-registry.js +2 -0
- package/src/utils/custom-html-utils.js +195 -0
- package/src/utils/markdown-utils.js +24 -0
- package/src/utils/patch-utils.js +103 -0
- package/src/utils/s3-utils.js +117 -0
- package/src/utils/site-utils.js +25 -0
- package/src/utils/suggestion-utils.js +69 -0
- package/test/index.test.js +1268 -462
- package/test/mappers/base-mapper.test.js +250 -7
- package/test/mappers/content-mapper.test.js +26 -24
- package/test/mappers/faq-mapper.test.js +1428 -0
- package/test/mappers/headings-mapper.test.js +23 -17
- package/test/utils/html-utils.test.js +432 -0
- package/test/utils/patch-utils.test.js +409 -0
- package/test/utils/s3-utils.test.js +140 -0
- package/test/utils/site-utils.test.js +80 -0
- package/test/utils/suggestion-utils.test.js +187 -0
|
@@ -233,7 +233,7 @@ describe('HeadingsMapper', () => {
|
|
|
233
233
|
});
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
describe('
|
|
236
|
+
describe('suggestionsToPatches', () => {
|
|
237
237
|
it('should create patch for heading-empty with transformRules', () => {
|
|
238
238
|
const suggestion = {
|
|
239
239
|
getId: () => 'sugg-123',
|
|
@@ -248,7 +248,9 @@ describe('HeadingsMapper', () => {
|
|
|
248
248
|
}),
|
|
249
249
|
};
|
|
250
250
|
|
|
251
|
-
const
|
|
251
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-123');
|
|
252
|
+
expect(patches.length).to.equal(1);
|
|
253
|
+
const patch = patches[0];
|
|
252
254
|
|
|
253
255
|
expect(patch).to.deep.include({
|
|
254
256
|
op: 'replace',
|
|
@@ -275,7 +277,9 @@ describe('HeadingsMapper', () => {
|
|
|
275
277
|
}),
|
|
276
278
|
};
|
|
277
279
|
|
|
278
|
-
const
|
|
280
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-123');
|
|
281
|
+
expect(patches.length).to.equal(1);
|
|
282
|
+
const patch = patches[0];
|
|
279
283
|
|
|
280
284
|
expect(patch).to.deep.include({
|
|
281
285
|
op: 'replace',
|
|
@@ -302,7 +306,9 @@ describe('HeadingsMapper', () => {
|
|
|
302
306
|
}),
|
|
303
307
|
};
|
|
304
308
|
|
|
305
|
-
const
|
|
309
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-456');
|
|
310
|
+
expect(patches.length).to.equal(1);
|
|
311
|
+
const patch = patches[0];
|
|
306
312
|
|
|
307
313
|
expect(patch).to.deep.include({
|
|
308
314
|
op: 'insertAfter',
|
|
@@ -330,7 +336,9 @@ describe('HeadingsMapper', () => {
|
|
|
330
336
|
}),
|
|
331
337
|
};
|
|
332
338
|
|
|
333
|
-
const
|
|
339
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-789');
|
|
340
|
+
expect(patches.length).to.equal(1);
|
|
341
|
+
const patch = patches[0];
|
|
334
342
|
|
|
335
343
|
expect(patch).to.deep.include({
|
|
336
344
|
op: 'replace',
|
|
@@ -344,7 +352,7 @@ describe('HeadingsMapper', () => {
|
|
|
344
352
|
expect(patch.tag).to.be.undefined;
|
|
345
353
|
});
|
|
346
354
|
|
|
347
|
-
it('should return
|
|
355
|
+
it('should return empty array for heading-missing-h1 without transformRules', () => {
|
|
348
356
|
const suggestion = {
|
|
349
357
|
getId: () => 'sugg-999',
|
|
350
358
|
getData: () => ({
|
|
@@ -353,12 +361,11 @@ describe('HeadingsMapper', () => {
|
|
|
353
361
|
}),
|
|
354
362
|
};
|
|
355
363
|
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
expect(patch).to.be.null;
|
|
364
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-999');
|
|
365
|
+
expect(patches.length).to.equal(0);
|
|
359
366
|
});
|
|
360
367
|
|
|
361
|
-
it('should return
|
|
368
|
+
it('should return empty array for heading-h1-length without selector in transformRules', () => {
|
|
362
369
|
const suggestion = {
|
|
363
370
|
getId: () => 'sugg-888',
|
|
364
371
|
getData: () => ({
|
|
@@ -370,9 +377,8 @@ describe('HeadingsMapper', () => {
|
|
|
370
377
|
}),
|
|
371
378
|
};
|
|
372
379
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
expect(patch).to.be.null;
|
|
380
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-888');
|
|
381
|
+
expect(patches.length).to.equal(0);
|
|
376
382
|
});
|
|
377
383
|
|
|
378
384
|
it('should log warning for heading-missing-h1 with missing transformRules - validation path', () => {
|
|
@@ -393,9 +399,9 @@ describe('HeadingsMapper', () => {
|
|
|
393
399
|
}),
|
|
394
400
|
};
|
|
395
401
|
|
|
396
|
-
const
|
|
402
|
+
const patches = warnMapper.suggestionsToPatches('/path', [suggestion], 'opp-warn');
|
|
397
403
|
|
|
398
|
-
expect(
|
|
404
|
+
expect(patches.length).to.equal(0);
|
|
399
405
|
expect(warnLogged).to.be.true;
|
|
400
406
|
});
|
|
401
407
|
|
|
@@ -419,9 +425,9 @@ describe('HeadingsMapper', () => {
|
|
|
419
425
|
}),
|
|
420
426
|
};
|
|
421
427
|
|
|
422
|
-
const
|
|
428
|
+
const patches = warnMapper.suggestionsToPatches('/path', [suggestion], 'opp-defensive');
|
|
423
429
|
|
|
424
|
-
expect(
|
|
430
|
+
expect(patches.length).to.equal(0);
|
|
425
431
|
expect(warnMessage).to.include('cannot be deployed');
|
|
426
432
|
});
|
|
427
433
|
});
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* eslint-env mocha */
|
|
14
|
+
|
|
15
|
+
import { expect } from 'chai';
|
|
16
|
+
import sinon from 'sinon';
|
|
17
|
+
import { fetchHtmlWithWarmup } from '../../src/utils/custom-html-utils.js';
|
|
18
|
+
|
|
19
|
+
describe('HTML Utils', () => {
|
|
20
|
+
describe('fetchHtmlWithWarmup', () => {
|
|
21
|
+
let fetchStub;
|
|
22
|
+
let log;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
fetchStub = sinon.stub(global, 'fetch');
|
|
26
|
+
log = {
|
|
27
|
+
debug: sinon.stub(),
|
|
28
|
+
warn: sinon.stub(),
|
|
29
|
+
error: sinon.stub(),
|
|
30
|
+
info: sinon.stub(),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
sinon.restore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should throw error when URL is missing', async () => {
|
|
39
|
+
try {
|
|
40
|
+
await fetchHtmlWithWarmup(
|
|
41
|
+
'',
|
|
42
|
+
'host',
|
|
43
|
+
'edge-url',
|
|
44
|
+
log,
|
|
45
|
+
false,
|
|
46
|
+
);
|
|
47
|
+
expect.fail('Should have thrown error');
|
|
48
|
+
} catch (error) {
|
|
49
|
+
expect(error.message).to.equal('URL is required for fetching HTML');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should throw error when forwardedHost is missing', async () => {
|
|
54
|
+
try {
|
|
55
|
+
await fetchHtmlWithWarmup(
|
|
56
|
+
'https://example.com/page',
|
|
57
|
+
'api-key',
|
|
58
|
+
'',
|
|
59
|
+
'edge-url',
|
|
60
|
+
log,
|
|
61
|
+
false,
|
|
62
|
+
);
|
|
63
|
+
expect.fail('Should have thrown error');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
expect(error.message).to.equal('Forwarded host is required for fetching HTML');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should throw error when tokowakaEdgeUrl is missing', async () => {
|
|
70
|
+
try {
|
|
71
|
+
await fetchHtmlWithWarmup(
|
|
72
|
+
'https://example.com/page',
|
|
73
|
+
'api-key',
|
|
74
|
+
'host',
|
|
75
|
+
'',
|
|
76
|
+
log,
|
|
77
|
+
false,
|
|
78
|
+
);
|
|
79
|
+
expect.fail('Should have thrown error');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
expect(error.message).to.equal('TOKOWAKA_EDGE_URL is not configured');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw error when apiKey is missing', async () => {
|
|
86
|
+
try {
|
|
87
|
+
await fetchHtmlWithWarmup(
|
|
88
|
+
'https://example.com/page',
|
|
89
|
+
'',
|
|
90
|
+
'host',
|
|
91
|
+
'edge-url',
|
|
92
|
+
log,
|
|
93
|
+
false,
|
|
94
|
+
);
|
|
95
|
+
expect.fail('Should have thrown error');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
expect(error.message).to.equal('Tokowaka API key is required for fetching HTML');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should successfully fetch HTML with all required parameters', async () => {
|
|
102
|
+
fetchStub.resolves({
|
|
103
|
+
ok: true,
|
|
104
|
+
status: 200,
|
|
105
|
+
statusText: 'OK',
|
|
106
|
+
headers: {
|
|
107
|
+
get: (name) => (name === 'x-tokowaka-cache' ? 'HIT' : null),
|
|
108
|
+
},
|
|
109
|
+
text: async () => '<html>Test HTML</html>',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const html = await fetchHtmlWithWarmup(
|
|
113
|
+
'https://example.com/page',
|
|
114
|
+
'api-key',
|
|
115
|
+
'host',
|
|
116
|
+
'https://edge.example.com',
|
|
117
|
+
log,
|
|
118
|
+
false,
|
|
119
|
+
{ warmupDelayMs: 0 },
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(html).to.equal('<html>Test HTML</html>');
|
|
123
|
+
expect(fetchStub.callCount).to.equal(2); // warmup + actual
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle URL with existing query parameters when fetching optimized HTML', async () => {
|
|
127
|
+
fetchStub.resolves({
|
|
128
|
+
ok: true,
|
|
129
|
+
status: 200,
|
|
130
|
+
statusText: 'OK',
|
|
131
|
+
headers: {
|
|
132
|
+
get: (name) => (name === 'x-tokowaka-cache' ? 'HIT' : null),
|
|
133
|
+
},
|
|
134
|
+
text: async () => '<html>Optimized HTML</html>',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const html = await fetchHtmlWithWarmup(
|
|
138
|
+
'https://example.com/page?param=value',
|
|
139
|
+
'api-key',
|
|
140
|
+
'host',
|
|
141
|
+
'https://edge.example.com',
|
|
142
|
+
log,
|
|
143
|
+
true, // isOptimized
|
|
144
|
+
{ warmupDelayMs: 0 },
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(html).to.equal('<html>Optimized HTML</html>');
|
|
148
|
+
expect(fetchStub.callCount).to.equal(2); // warmup + actual
|
|
149
|
+
|
|
150
|
+
// Verify the URL includes & for the preview param (not ?)
|
|
151
|
+
const actualUrl = fetchStub.secondCall.args[0];
|
|
152
|
+
expect(actualUrl).to.include('param=value');
|
|
153
|
+
expect(actualUrl).to.include('&tokowakaPreview=true');
|
|
154
|
+
expect(actualUrl).to.not.include('?tokowakaPreview=true');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should throw error when HTTP response is not ok', async () => {
|
|
158
|
+
// Warmup succeeds
|
|
159
|
+
fetchStub.onCall(0).resolves({
|
|
160
|
+
ok: true,
|
|
161
|
+
status: 200,
|
|
162
|
+
statusText: 'OK',
|
|
163
|
+
headers: {
|
|
164
|
+
get: () => null,
|
|
165
|
+
},
|
|
166
|
+
text: async () => 'warmup',
|
|
167
|
+
});
|
|
168
|
+
// Actual call returns 404
|
|
169
|
+
fetchStub.onCall(1).resolves({
|
|
170
|
+
ok: false,
|
|
171
|
+
status: 404,
|
|
172
|
+
statusText: 'Not Found',
|
|
173
|
+
headers: {
|
|
174
|
+
get: () => null,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await fetchHtmlWithWarmup(
|
|
180
|
+
'https://example.com/page',
|
|
181
|
+
'api-key',
|
|
182
|
+
'host',
|
|
183
|
+
'https://edge.example.com',
|
|
184
|
+
log,
|
|
185
|
+
false,
|
|
186
|
+
{ warmupDelayMs: 0, maxRetries: 0 },
|
|
187
|
+
);
|
|
188
|
+
expect.fail('Should have thrown error');
|
|
189
|
+
} catch (error) {
|
|
190
|
+
expect(error.message).to.include('Failed to fetch original HTML');
|
|
191
|
+
expect(error.message).to.include('0 retries');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should retry and eventually throw error after max retries', async () => {
|
|
196
|
+
// Warmup succeeds
|
|
197
|
+
fetchStub.onCall(0).resolves({
|
|
198
|
+
ok: true,
|
|
199
|
+
status: 200,
|
|
200
|
+
statusText: 'OK',
|
|
201
|
+
text: async () => 'warmup',
|
|
202
|
+
});
|
|
203
|
+
// All actual calls fail
|
|
204
|
+
fetchStub.onCall(1).rejects(new Error('Network error'));
|
|
205
|
+
fetchStub.onCall(2).rejects(new Error('Network error'));
|
|
206
|
+
fetchStub.onCall(3).rejects(new Error('Network error'));
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await fetchHtmlWithWarmup(
|
|
210
|
+
'https://example.com/page',
|
|
211
|
+
'api-key',
|
|
212
|
+
'host',
|
|
213
|
+
'https://edge.example.com',
|
|
214
|
+
log,
|
|
215
|
+
false,
|
|
216
|
+
{ warmupDelayMs: 0, maxRetries: 2, retryDelayMs: 0 },
|
|
217
|
+
);
|
|
218
|
+
expect.fail('Should have thrown error');
|
|
219
|
+
} catch (error) {
|
|
220
|
+
expect(error.message).to.include('Failed to fetch original HTML');
|
|
221
|
+
expect(error.message).to.include('Network error');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Should have tried 3 times (initial + 2 retries) plus warmup
|
|
225
|
+
expect(fetchStub.callCount).to.equal(4);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should handle zero maxRetries value', async () => {
|
|
229
|
+
// Warmup succeeds
|
|
230
|
+
fetchStub.onCall(0).resolves({
|
|
231
|
+
ok: true,
|
|
232
|
+
status: 200,
|
|
233
|
+
statusText: 'OK',
|
|
234
|
+
text: async () => 'warmup',
|
|
235
|
+
});
|
|
236
|
+
// Actual call fails
|
|
237
|
+
fetchStub.onCall(1).rejects(new Error('Network error'));
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await fetchHtmlWithWarmup(
|
|
241
|
+
'https://example.com/page',
|
|
242
|
+
'api-key',
|
|
243
|
+
'host',
|
|
244
|
+
'https://edge.example.com',
|
|
245
|
+
log,
|
|
246
|
+
false,
|
|
247
|
+
{ warmupDelayMs: 0, maxRetries: 0 },
|
|
248
|
+
);
|
|
249
|
+
expect.fail('Should have thrown error');
|
|
250
|
+
} catch (error) {
|
|
251
|
+
expect(error.message).to.include('Network error');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Should have tried only once (no retries) plus warmup
|
|
255
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should handle negative maxRetries as edge case', async () => {
|
|
259
|
+
// Warmup succeeds
|
|
260
|
+
fetchStub.onCall(0).resolves({
|
|
261
|
+
ok: true,
|
|
262
|
+
status: 200,
|
|
263
|
+
statusText: 'OK',
|
|
264
|
+
headers: {
|
|
265
|
+
get: () => null,
|
|
266
|
+
},
|
|
267
|
+
text: async () => 'warmup',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
// With maxRetries: -1, the retry loop won't execute
|
|
272
|
+
// This tests the defensive 'throw lastError' fallback
|
|
273
|
+
await fetchHtmlWithWarmup(
|
|
274
|
+
'https://example.com/page',
|
|
275
|
+
'host',
|
|
276
|
+
'https://edge.example.com',
|
|
277
|
+
log,
|
|
278
|
+
false,
|
|
279
|
+
{ warmupDelayMs: 0, maxRetries: -1 },
|
|
280
|
+
);
|
|
281
|
+
expect.fail('Should have thrown error');
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// Should throw the lastError from the loop
|
|
284
|
+
expect(error).to.exist;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should stop retrying when x-tokowaka-cache header is found', async () => {
|
|
289
|
+
// Warmup succeeds
|
|
290
|
+
fetchStub.onCall(0).resolves({
|
|
291
|
+
ok: true,
|
|
292
|
+
status: 200,
|
|
293
|
+
statusText: 'OK',
|
|
294
|
+
headers: {
|
|
295
|
+
get: () => null,
|
|
296
|
+
},
|
|
297
|
+
text: async () => 'warmup',
|
|
298
|
+
});
|
|
299
|
+
// First actual call - no cache header
|
|
300
|
+
fetchStub.onCall(1).resolves({
|
|
301
|
+
ok: true,
|
|
302
|
+
status: 200,
|
|
303
|
+
statusText: 'OK',
|
|
304
|
+
headers: {
|
|
305
|
+
get: () => null,
|
|
306
|
+
},
|
|
307
|
+
text: async () => '<html>No cache</html>',
|
|
308
|
+
});
|
|
309
|
+
// Second actual call - cache header found
|
|
310
|
+
fetchStub.onCall(2).resolves({
|
|
311
|
+
ok: true,
|
|
312
|
+
status: 200,
|
|
313
|
+
statusText: 'OK',
|
|
314
|
+
headers: {
|
|
315
|
+
get: (name) => (name === 'x-tokowaka-cache' ? 'HIT' : null),
|
|
316
|
+
},
|
|
317
|
+
text: async () => '<html>Cached HTML</html>',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const html = await fetchHtmlWithWarmup(
|
|
321
|
+
'https://example.com/page',
|
|
322
|
+
'api-key',
|
|
323
|
+
'host',
|
|
324
|
+
'https://edge.example.com',
|
|
325
|
+
log,
|
|
326
|
+
false,
|
|
327
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(html).to.equal('<html>Cached HTML</html>');
|
|
331
|
+
// Should stop after finding cache header (warmup + 2 attempts)
|
|
332
|
+
expect(fetchStub.callCount).to.equal(3);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should throw error when cache header not found after max retries', async () => {
|
|
336
|
+
// Warmup succeeds
|
|
337
|
+
fetchStub.onCall(0).resolves({
|
|
338
|
+
ok: true,
|
|
339
|
+
status: 200,
|
|
340
|
+
statusText: 'OK',
|
|
341
|
+
headers: {
|
|
342
|
+
get: () => null,
|
|
343
|
+
},
|
|
344
|
+
text: async () => 'warmup',
|
|
345
|
+
});
|
|
346
|
+
// All actual calls succeed but no cache header
|
|
347
|
+
fetchStub.onCall(1).resolves({
|
|
348
|
+
ok: true,
|
|
349
|
+
status: 200,
|
|
350
|
+
statusText: 'OK',
|
|
351
|
+
headers: {
|
|
352
|
+
get: () => null,
|
|
353
|
+
},
|
|
354
|
+
text: async () => '<html>No cache 1</html>',
|
|
355
|
+
});
|
|
356
|
+
fetchStub.onCall(2).resolves({
|
|
357
|
+
ok: true,
|
|
358
|
+
status: 200,
|
|
359
|
+
statusText: 'OK',
|
|
360
|
+
headers: {
|
|
361
|
+
get: () => null,
|
|
362
|
+
},
|
|
363
|
+
text: async () => '<html>No cache 2</html>',
|
|
364
|
+
});
|
|
365
|
+
fetchStub.onCall(3).resolves({
|
|
366
|
+
ok: true,
|
|
367
|
+
status: 200,
|
|
368
|
+
statusText: 'OK',
|
|
369
|
+
headers: {
|
|
370
|
+
get: () => null,
|
|
371
|
+
},
|
|
372
|
+
text: async () => '<html>No cache 3</html>',
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
await fetchHtmlWithWarmup(
|
|
377
|
+
'https://example.com/page',
|
|
378
|
+
'api-key',
|
|
379
|
+
'host',
|
|
380
|
+
'https://edge.example.com',
|
|
381
|
+
log,
|
|
382
|
+
false,
|
|
383
|
+
{ warmupDelayMs: 0, maxRetries: 2, retryDelayMs: 0 },
|
|
384
|
+
);
|
|
385
|
+
expect.fail('Should have thrown error');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
expect(error.message).to.include('Failed to fetch original HTML');
|
|
388
|
+
expect(error.message).to.include('Cache header (x-tokowaka-cache) not found after 2 retries');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Should have tried 3 times (initial + 2 retries) plus warmup
|
|
392
|
+
expect(fetchStub.callCount).to.equal(4);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should return immediately on first attempt if cache header is present', async () => {
|
|
396
|
+
// Warmup succeeds
|
|
397
|
+
fetchStub.onCall(0).resolves({
|
|
398
|
+
ok: true,
|
|
399
|
+
status: 200,
|
|
400
|
+
statusText: 'OK',
|
|
401
|
+
headers: {
|
|
402
|
+
get: () => null,
|
|
403
|
+
},
|
|
404
|
+
text: async () => 'warmup',
|
|
405
|
+
});
|
|
406
|
+
// First actual call has cache header
|
|
407
|
+
fetchStub.onCall(1).resolves({
|
|
408
|
+
ok: true,
|
|
409
|
+
status: 200,
|
|
410
|
+
statusText: 'OK',
|
|
411
|
+
headers: {
|
|
412
|
+
get: (name) => (name === 'x-tokowaka-cache' ? 'HIT' : null),
|
|
413
|
+
},
|
|
414
|
+
text: async () => '<html>Cached HTML</html>',
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const html = await fetchHtmlWithWarmup(
|
|
418
|
+
'https://example.com/page',
|
|
419
|
+
'api-key',
|
|
420
|
+
'host',
|
|
421
|
+
'https://edge.example.com',
|
|
422
|
+
log,
|
|
423
|
+
false,
|
|
424
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
expect(html).to.equal('<html>Cached HTML</html>');
|
|
428
|
+
// Should not retry if cache header found on first attempt
|
|
429
|
+
expect(fetchStub.callCount).to.equal(2); // warmup + 1 actual
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|