@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
|
@@ -41,9 +41,9 @@ describe('BaseOpportunityMapper', () => {
|
|
|
41
41
|
.to.throw('requiresPrerender() must be implemented by subclass');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it('
|
|
45
|
-
expect(() => mapper.
|
|
46
|
-
.to.throw('
|
|
44
|
+
it('suggestionsToPatches should throw error', () => {
|
|
45
|
+
expect(() => mapper.suggestionsToPatches('/path', [], 'opp-123', null))
|
|
46
|
+
.to.throw('suggestionsToPatches() must be implemented by subclass');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('canDeploy should throw error if not implemented', () => {
|
|
@@ -60,14 +60,15 @@ describe('BaseOpportunityMapper', () => {
|
|
|
60
60
|
|
|
61
61
|
requiresPrerender() { return true; }
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
suggestionsToPatches() { return []; }
|
|
64
64
|
|
|
65
65
|
canDeploy() { return { eligible: true }; }
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const testMapper = new TestMapper();
|
|
68
|
+
const testMapper = new TestMapper(log);
|
|
69
69
|
const suggestion = {
|
|
70
70
|
getId: () => 'test-123',
|
|
71
|
+
getData: () => ({}),
|
|
71
72
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
72
73
|
};
|
|
73
74
|
|
|
@@ -85,14 +86,15 @@ describe('BaseOpportunityMapper', () => {
|
|
|
85
86
|
|
|
86
87
|
requiresPrerender() { return true; }
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
suggestionsToPatches() { return []; }
|
|
89
90
|
|
|
90
91
|
canDeploy() { return { eligible: true }; }
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const testMapper = new TestMapper();
|
|
94
|
+
const testMapper = new TestMapper(log);
|
|
94
95
|
const suggestion = {
|
|
95
96
|
getId: () => 'test-no-date',
|
|
97
|
+
getData: () => ({}),
|
|
96
98
|
getUpdatedAt: () => null, // Returns null
|
|
97
99
|
};
|
|
98
100
|
|
|
@@ -106,5 +108,246 @@ describe('BaseOpportunityMapper', () => {
|
|
|
106
108
|
expect(patch.lastUpdated).to.be.at.most(afterTime);
|
|
107
109
|
expect(patch.prerenderRequired).to.be.true;
|
|
108
110
|
});
|
|
111
|
+
|
|
112
|
+
it('should prioritize scrapedAt from getData()', () => {
|
|
113
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
114
|
+
getOpportunityType() { return 'test'; }
|
|
115
|
+
|
|
116
|
+
requiresPrerender() { return true; }
|
|
117
|
+
|
|
118
|
+
suggestionsToPatches() { return []; }
|
|
119
|
+
|
|
120
|
+
canDeploy() { return { eligible: true }; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const testMapper = new TestMapper(log);
|
|
124
|
+
const scrapedTime = '2025-01-20T15:30:00.000Z';
|
|
125
|
+
const suggestion = {
|
|
126
|
+
getId: () => 'test-scraped',
|
|
127
|
+
getData: () => ({ scrapedAt: scrapedTime }),
|
|
128
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-scraped');
|
|
132
|
+
|
|
133
|
+
expect(patch.lastUpdated).to.equal(new Date(scrapedTime).getTime());
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should use transformRules.scrapedAt when scrapedAt is not available', () => {
|
|
137
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
138
|
+
getOpportunityType() { return 'test'; }
|
|
139
|
+
|
|
140
|
+
requiresPrerender() { return true; }
|
|
141
|
+
|
|
142
|
+
suggestionsToPatches() { return []; }
|
|
143
|
+
|
|
144
|
+
canDeploy() { return { eligible: true }; }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const testMapper = new TestMapper(log);
|
|
148
|
+
const transformScrapedTime = '2025-01-18T12:00:00.000Z';
|
|
149
|
+
const suggestion = {
|
|
150
|
+
getId: () => 'test-transform',
|
|
151
|
+
getData: () => ({ transformRules: { scrapedAt: transformScrapedTime } }),
|
|
152
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-transform');
|
|
156
|
+
|
|
157
|
+
expect(patch.lastUpdated).to.equal(new Date(transformScrapedTime).getTime());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle invalid date strings by using Date.now()', () => {
|
|
161
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
162
|
+
getOpportunityType() { return 'test'; }
|
|
163
|
+
|
|
164
|
+
requiresPrerender() { return true; }
|
|
165
|
+
|
|
166
|
+
suggestionsToPatches() { return []; }
|
|
167
|
+
|
|
168
|
+
canDeploy() { return { eligible: true }; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const testMapper = new TestMapper(log);
|
|
172
|
+
const suggestion = {
|
|
173
|
+
getId: () => 'test-invalid',
|
|
174
|
+
getData: () => ({}),
|
|
175
|
+
getUpdatedAt: () => 'invalid-date-string',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const beforeTime = Date.now();
|
|
179
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-invalid');
|
|
180
|
+
const afterTime = Date.now();
|
|
181
|
+
|
|
182
|
+
// Should fallback to Date.now() for invalid dates
|
|
183
|
+
expect(patch.lastUpdated).to.be.at.least(beforeTime);
|
|
184
|
+
expect(patch.lastUpdated).to.be.at.most(afterTime);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle missing getData() gracefully', () => {
|
|
188
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
189
|
+
getOpportunityType() { return 'test'; }
|
|
190
|
+
|
|
191
|
+
requiresPrerender() { return true; }
|
|
192
|
+
|
|
193
|
+
suggestionsToPatches() { return []; }
|
|
194
|
+
|
|
195
|
+
canDeploy() { return { eligible: true }; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const testMapper = new TestMapper(log);
|
|
199
|
+
const suggestion = {
|
|
200
|
+
getId: () => 'test-no-data',
|
|
201
|
+
getData: () => null,
|
|
202
|
+
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const patch = testMapper.createBasePatch(suggestion, 'opp-no-data');
|
|
206
|
+
|
|
207
|
+
expect(patch.lastUpdated).to.equal(new Date('2025-01-15T10:00:00.000Z').getTime());
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('rollbackPatches', () => {
|
|
212
|
+
let testMapper;
|
|
213
|
+
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
class TestMapper extends BaseOpportunityMapper {
|
|
216
|
+
getOpportunityType() { return 'test'; }
|
|
217
|
+
|
|
218
|
+
requiresPrerender() { return true; }
|
|
219
|
+
|
|
220
|
+
suggestionsToPatches() { return []; }
|
|
221
|
+
|
|
222
|
+
canDeploy() { return { eligible: true }; }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
testMapper = new TestMapper(log);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should remove patches by suggestion IDs using default implementation', () => {
|
|
229
|
+
const config = {
|
|
230
|
+
url: 'https://example.com/page1',
|
|
231
|
+
version: '1.0',
|
|
232
|
+
forceFail: false,
|
|
233
|
+
prerender: true,
|
|
234
|
+
patches: [
|
|
235
|
+
{
|
|
236
|
+
opportunityId: 'opp-test',
|
|
237
|
+
suggestionId: 'sugg-1',
|
|
238
|
+
op: 'replace',
|
|
239
|
+
value: 'value-1',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
opportunityId: 'opp-test',
|
|
243
|
+
suggestionId: 'sugg-2',
|
|
244
|
+
op: 'replace',
|
|
245
|
+
value: 'value-2',
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
251
|
+
|
|
252
|
+
expect(result.patches).to.have.lengthOf(1);
|
|
253
|
+
expect(result.patches[0].suggestionId).to.equal('sugg-2');
|
|
254
|
+
expect(result.removedCount).to.equal(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle null/undefined config gracefully', () => {
|
|
258
|
+
const result1 = testMapper.rollbackPatches(null, ['sugg-1'], 'opp-test');
|
|
259
|
+
expect(result1).to.be.null;
|
|
260
|
+
|
|
261
|
+
const result2 = testMapper.rollbackPatches(undefined, ['sugg-1'], 'opp-test');
|
|
262
|
+
expect(result2).to.be.undefined;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should remove patches for multiple suggestion IDs', () => {
|
|
266
|
+
const config = {
|
|
267
|
+
url: 'https://example.com/page1',
|
|
268
|
+
version: '1.0',
|
|
269
|
+
forceFail: false,
|
|
270
|
+
prerender: true,
|
|
271
|
+
patches: [
|
|
272
|
+
{
|
|
273
|
+
opportunityId: 'opp-test',
|
|
274
|
+
suggestionId: 'sugg-1',
|
|
275
|
+
op: 'replace',
|
|
276
|
+
value: 'value-1',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
opportunityId: 'opp-test',
|
|
280
|
+
suggestionId: 'sugg-2',
|
|
281
|
+
op: 'replace',
|
|
282
|
+
value: 'value-2',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
opportunityId: 'opp-test',
|
|
286
|
+
suggestionId: 'sugg-3',
|
|
287
|
+
op: 'replace',
|
|
288
|
+
value: 'value-3',
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1', 'sugg-3'], 'opp-test');
|
|
294
|
+
|
|
295
|
+
expect(result.patches).to.have.lengthOf(1);
|
|
296
|
+
expect(result.patches[0].suggestionId).to.equal('sugg-2');
|
|
297
|
+
expect(result.removedCount).to.equal(2);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should remove URL path when all patches are removed', () => {
|
|
301
|
+
const config = {
|
|
302
|
+
url: 'https://example.com/page1',
|
|
303
|
+
version: '1.0',
|
|
304
|
+
forceFail: false,
|
|
305
|
+
prerender: true,
|
|
306
|
+
patches: [
|
|
307
|
+
{
|
|
308
|
+
opportunityId: 'opp-test',
|
|
309
|
+
suggestionId: 'sugg-1',
|
|
310
|
+
op: 'replace',
|
|
311
|
+
value: 'value-1',
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
317
|
+
|
|
318
|
+
// All patches removed, patches array should be empty
|
|
319
|
+
expect(result.patches).to.have.lengthOf(0);
|
|
320
|
+
expect(result.removedCount).to.equal(1);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should preserve patches from other opportunities', () => {
|
|
324
|
+
const config = {
|
|
325
|
+
url: 'https://example.com/page1',
|
|
326
|
+
version: '1.0',
|
|
327
|
+
forceFail: false,
|
|
328
|
+
prerender: true,
|
|
329
|
+
patches: [
|
|
330
|
+
{
|
|
331
|
+
opportunityId: 'opp-test',
|
|
332
|
+
suggestionId: 'sugg-1',
|
|
333
|
+
op: 'replace',
|
|
334
|
+
value: 'test-value',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
opportunityId: 'opp-other',
|
|
338
|
+
suggestionId: 'sugg-2',
|
|
339
|
+
op: 'replace',
|
|
340
|
+
value: 'other-value',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Default implementation removes by suggestionId regardless of opportunity
|
|
346
|
+
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
347
|
+
|
|
348
|
+
expect(result.patches).to.have.lengthOf(1);
|
|
349
|
+
expect(result.patches[0].suggestionId).to.equal('sugg-2');
|
|
350
|
+
expect(result.removedCount).to.equal(1);
|
|
351
|
+
});
|
|
109
352
|
});
|
|
110
353
|
});
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
/* eslint-env mocha */
|
|
14
14
|
|
|
15
15
|
import { expect } from 'chai';
|
|
16
|
-
import sinon from 'sinon';
|
|
17
16
|
import ContentMapper from '../../src/mappers/content-summarization-mapper.js';
|
|
18
17
|
|
|
19
18
|
describe('ContentMapper', () => {
|
|
@@ -142,7 +141,7 @@ describe('ContentMapper', () => {
|
|
|
142
141
|
});
|
|
143
142
|
});
|
|
144
143
|
|
|
145
|
-
describe('
|
|
144
|
+
describe('suggestionsToPatches', () => {
|
|
146
145
|
it('should create patch with HAST value from markdown', () => {
|
|
147
146
|
const suggestion = {
|
|
148
147
|
getId: () => 'sugg-content-123',
|
|
@@ -156,7 +155,9 @@ describe('ContentMapper', () => {
|
|
|
156
155
|
}),
|
|
157
156
|
};
|
|
158
157
|
|
|
159
|
-
const
|
|
158
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-content-123');
|
|
159
|
+
expect(patches.length).to.equal(1);
|
|
160
|
+
const patch = patches[0];
|
|
160
161
|
|
|
161
162
|
expect(patch).to.exist;
|
|
162
163
|
expect(patch.op).to.equal('insertAfter');
|
|
@@ -187,7 +188,9 @@ describe('ContentMapper', () => {
|
|
|
187
188
|
}),
|
|
188
189
|
};
|
|
189
190
|
|
|
190
|
-
const
|
|
191
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-bold');
|
|
192
|
+
expect(patches.length).to.equal(1);
|
|
193
|
+
const patch = patches[0];
|
|
191
194
|
|
|
192
195
|
expect(patch).to.exist;
|
|
193
196
|
expect(patch.value.type).to.equal('root');
|
|
@@ -216,7 +219,9 @@ describe('ContentMapper', () => {
|
|
|
216
219
|
}),
|
|
217
220
|
};
|
|
218
221
|
|
|
219
|
-
const
|
|
222
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-heading');
|
|
223
|
+
expect(patches.length).to.equal(1);
|
|
224
|
+
const patch = patches[0];
|
|
220
225
|
|
|
221
226
|
expect(patch).to.exist;
|
|
222
227
|
expect(patch.value.children).to.be.an('array');
|
|
@@ -241,7 +246,9 @@ describe('ContentMapper', () => {
|
|
|
241
246
|
}),
|
|
242
247
|
};
|
|
243
248
|
|
|
244
|
-
const
|
|
249
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-list');
|
|
250
|
+
expect(patches.length).to.equal(1);
|
|
251
|
+
const patch = patches[0];
|
|
245
252
|
|
|
246
253
|
expect(patch).to.exist;
|
|
247
254
|
|
|
@@ -250,7 +257,7 @@ describe('ContentMapper', () => {
|
|
|
250
257
|
expect(hasList).to.be.true;
|
|
251
258
|
});
|
|
252
259
|
|
|
253
|
-
it('should return
|
|
260
|
+
it('should return empty array when summarizationText is missing', () => {
|
|
254
261
|
const suggestion = {
|
|
255
262
|
getId: () => 'sugg-invalid',
|
|
256
263
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
@@ -262,12 +269,11 @@ describe('ContentMapper', () => {
|
|
|
262
269
|
}),
|
|
263
270
|
};
|
|
264
271
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
expect(patch).to.be.null;
|
|
272
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-invalid');
|
|
273
|
+
expect(patches.length).to.equal(0);
|
|
268
274
|
});
|
|
269
275
|
|
|
270
|
-
it('should return
|
|
276
|
+
it('should return empty array when transformRules are incomplete', () => {
|
|
271
277
|
const suggestion = {
|
|
272
278
|
getId: () => 'sugg-invalid-2',
|
|
273
279
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
@@ -279,9 +285,8 @@ describe('ContentMapper', () => {
|
|
|
279
285
|
}),
|
|
280
286
|
};
|
|
281
287
|
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
expect(patch).to.be.null;
|
|
288
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-invalid-2');
|
|
289
|
+
expect(patches.length).to.equal(0);
|
|
285
290
|
});
|
|
286
291
|
|
|
287
292
|
it('should handle complex markdown with multiple elements', () => {
|
|
@@ -305,7 +310,9 @@ describe('ContentMapper', () => {
|
|
|
305
310
|
}),
|
|
306
311
|
};
|
|
307
312
|
|
|
308
|
-
const
|
|
313
|
+
const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-complex');
|
|
314
|
+
expect(patches.length).to.equal(1);
|
|
315
|
+
const patch = patches[0];
|
|
309
316
|
|
|
310
317
|
expect(patch).to.exist;
|
|
311
318
|
expect(patch.value.children).to.be.an('array');
|
|
@@ -329,13 +336,11 @@ describe('ContentMapper', () => {
|
|
|
329
336
|
|
|
330
337
|
const errorMapper = new ContentMapper(errorLog);
|
|
331
338
|
|
|
332
|
-
//
|
|
333
|
-
const stub = sinon.stub(errorMapper, 'markdownToHast').throws(new Error('Markdown parsing failed'));
|
|
334
|
-
|
|
339
|
+
// Pass an object instead of string to trigger natural error in markdown parser
|
|
335
340
|
const suggestion = {
|
|
336
341
|
getId: () => 'sugg-error',
|
|
337
342
|
getData: () => ({
|
|
338
|
-
summarizationText: '
|
|
343
|
+
summarizationText: { invalid: 'object' }, // This will cause markdown parser to fail
|
|
339
344
|
transformRules: {
|
|
340
345
|
action: 'insertAfter',
|
|
341
346
|
selector: '#selector',
|
|
@@ -343,13 +348,10 @@ describe('ContentMapper', () => {
|
|
|
343
348
|
}),
|
|
344
349
|
};
|
|
345
350
|
|
|
346
|
-
const
|
|
351
|
+
const patches = errorMapper.suggestionsToPatches('/path', [suggestion], 'opp-error');
|
|
347
352
|
|
|
348
|
-
expect(
|
|
353
|
+
expect(patches.length).to.equal(0);
|
|
349
354
|
expect(errorMessage).to.include('Failed to convert markdown to HAST');
|
|
350
|
-
expect(errorMessage).to.include('Markdown parsing failed');
|
|
351
|
-
|
|
352
|
-
stub.restore();
|
|
353
355
|
});
|
|
354
356
|
});
|
|
355
357
|
});
|