@adobe/spacecat-shared-tokowaka-client 1.7.8 → 1.8.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.
@@ -0,0 +1,332 @@
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 { readFileSync } from 'fs';
17
+ import { dirname, join } from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import SemanticValueVisibilityMapper from '../../src/mappers/semantic-value-visibility-mapper.js';
20
+
21
+ const filename = fileURLToPath(import.meta.url);
22
+ const fixturesPath = join(dirname(filename), '../fixtures/semantic-value-visibility');
23
+
24
+ // Load real Mystique response fixtures
25
+ // Carahsoft proves the logic is correct, others prove it doesn't crash with different real inputs.
26
+ const carahsoftFixture = JSON.parse(readFileSync(join(fixturesPath, 'Carahsoft.json'), 'utf8'));
27
+ const koffievoordeeelFixture = JSON.parse(readFileSync(join(fixturesPath, 'Koffievoordeel.json'), 'utf8'));
28
+ const krisshopFixture = JSON.parse(readFileSync(join(fixturesPath, 'Krisshop.json'), 'utf8'));
29
+ const veserisFixture = JSON.parse(readFileSync(join(fixturesPath, 'Veseris.json'), 'utf8'));
30
+ const vuseFixture = JSON.parse(readFileSync(join(fixturesPath, 'Vuse.json'), 'utf8'));
31
+
32
+ describe('SemanticValueVisibilityMapper', () => {
33
+ let mapper;
34
+ let log;
35
+
36
+ beforeEach(() => {
37
+ log = {
38
+ debug: () => {},
39
+ info: () => {},
40
+ warn: () => {},
41
+ error: () => {},
42
+ };
43
+ mapper = new SemanticValueVisibilityMapper(log);
44
+ });
45
+
46
+ describe('getOpportunityType', () => {
47
+ it('should return semantic-value-visibility', () => {
48
+ expect(mapper.getOpportunityType()).to.equal('semantic-value-visibility');
49
+ });
50
+ });
51
+
52
+ describe('requiresPrerender', () => {
53
+ it('should return true', () => {
54
+ expect(mapper.requiresPrerender()).to.be.true;
55
+ });
56
+ });
57
+
58
+ describe('canDeploy', () => {
59
+ it('should return eligible for valid suggestion', () => {
60
+ const suggestion = {
61
+ getData: () => ({
62
+ semanticHtml: '<section><h2>Test</h2></section>',
63
+ transformRules: {
64
+ action: 'insertAfter',
65
+ selector: 'img[src="https://example.com/image.jpg"]',
66
+ },
67
+ }),
68
+ };
69
+
70
+ const result = mapper.canDeploy(suggestion);
71
+ expect(result).to.deep.equal({ eligible: true });
72
+ });
73
+
74
+ it('should return ineligible when semanticHtml is missing', () => {
75
+ const suggestion = {
76
+ getData: () => ({
77
+ transformRules: {
78
+ action: 'insertAfter',
79
+ selector: 'img[src="https://example.com/image.jpg"]',
80
+ },
81
+ }),
82
+ };
83
+
84
+ const result = mapper.canDeploy(suggestion);
85
+ expect(result).to.deep.equal({
86
+ eligible: false,
87
+ reason: 'semanticHtml is required',
88
+ });
89
+ });
90
+
91
+ it('should return ineligible when transformRules is missing', () => {
92
+ const suggestion = {
93
+ getData: () => ({
94
+ semanticHtml: '<section><h2>Test</h2></section>',
95
+ }),
96
+ };
97
+
98
+ const result = mapper.canDeploy(suggestion);
99
+ expect(result).to.deep.equal({
100
+ eligible: false,
101
+ reason: 'transformRules is required',
102
+ });
103
+ });
104
+
105
+ it('should return ineligible when selector is missing', () => {
106
+ const suggestion = {
107
+ getData: () => ({
108
+ semanticHtml: '<section><h2>Test</h2></section>',
109
+ transformRules: {
110
+ action: 'insertAfter',
111
+ },
112
+ }),
113
+ };
114
+
115
+ const result = mapper.canDeploy(suggestion);
116
+ expect(result).to.deep.equal({
117
+ eligible: false,
118
+ reason: 'transformRules.selector is required',
119
+ });
120
+ });
121
+
122
+ it('should return ineligible when action is invalid', () => {
123
+ const suggestion = {
124
+ getData: () => ({
125
+ semanticHtml: '<section><h2>Test</h2></section>',
126
+ transformRules: {
127
+ action: 'replace',
128
+ selector: 'img[src="https://example.com/image.jpg"]',
129
+ },
130
+ }),
131
+ };
132
+
133
+ const result = mapper.canDeploy(suggestion);
134
+ expect(result).to.deep.equal({
135
+ eligible: false,
136
+ reason: 'transformRules.action must be one of: insertAfter, insertBefore, appendChild',
137
+ });
138
+ });
139
+
140
+ it('should return ineligible when data is null', () => {
141
+ const suggestion = {
142
+ getData: () => null,
143
+ };
144
+
145
+ const result = mapper.canDeploy(suggestion);
146
+ expect(result).to.deep.equal({
147
+ eligible: false,
148
+ reason: 'semanticHtml is required',
149
+ });
150
+ });
151
+ });
152
+
153
+ describe('suggestionsToPatches - with Carahsoft fixture', () => {
154
+ it('should convert Carahsoft suggestions to patches', () => {
155
+ // Create suggestion mocks from fixture data
156
+ const suggestions = carahsoftFixture.suggestions.map((s, index) => ({
157
+ getId: () => `sugg-carahsoft-${index}`,
158
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
159
+ getData: () => s.data,
160
+ }));
161
+
162
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-carahsoft');
163
+
164
+ expect(patches.length).to.equal(carahsoftFixture.suggestions.length);
165
+
166
+ // Verify first patch structure
167
+ const firstPatch = patches[0];
168
+ expect(firstPatch.op).to.equal('insertAfter');
169
+ expect(firstPatch.selector).to.include('img[src=');
170
+ expect(firstPatch.valueFormat).to.equal('hast');
171
+ expect(firstPatch.target).to.equal('ai-bots');
172
+ expect(firstPatch.prerenderRequired).to.be.true;
173
+ expect(firstPatch.opportunityId).to.equal('opp-carahsoft');
174
+ expect(firstPatch.suggestionId).to.equal('sugg-carahsoft-0');
175
+ expect(firstPatch.lastUpdated).to.be.a('number');
176
+
177
+ // Verify HAST structure
178
+ expect(firstPatch.value).to.be.an('object');
179
+ expect(firstPatch.value.type).to.equal('root');
180
+ expect(firstPatch.value.children).to.be.an('array');
181
+ });
182
+
183
+ it('should preserve data-llm attributes in HAST', () => {
184
+ const suggestions = carahsoftFixture.suggestions.slice(0, 1).map((s, index) => ({
185
+ getId: () => `sugg-${index}`,
186
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
187
+ getData: () => s.data,
188
+ }));
189
+
190
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-1');
191
+ const section = patches[0].value.children.find((c) => c.tagName === 'section');
192
+
193
+ expect(section.properties.dataLlmContext).to.equal('image');
194
+ expect(section.properties.dataLlmShadow).to.equal('image-text');
195
+ });
196
+ });
197
+
198
+ describe('suggestionsToPatches - with Koffievoordeel fixture', () => {
199
+ it('should convert Koffievoordeel suggestions to patches', () => {
200
+ const suggestions = koffievoordeeelFixture.suggestions.map((s, index) => ({
201
+ getId: () => `sugg-koffie-${index}`,
202
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
203
+ getData: () => s.data,
204
+ }));
205
+
206
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-koffie');
207
+
208
+ expect(patches.length).to.equal(koffievoordeeelFixture.suggestions.length);
209
+
210
+ // Verify all patches have required fields
211
+ patches.forEach((patch, index) => {
212
+ expect(patch.op).to.equal('insertAfter');
213
+ expect(patch.selector).to.be.a('string');
214
+ expect(patch.valueFormat).to.equal('hast');
215
+ expect(patch.target).to.equal('ai-bots');
216
+ expect(patch.suggestionId).to.equal(`sugg-koffie-${index}`);
217
+ });
218
+ });
219
+ });
220
+
221
+ describe('suggestionsToPatches - with Krisshop fixture', () => {
222
+ it('should convert Krisshop suggestions to patches', () => {
223
+ const suggestions = krisshopFixture.suggestions.map((s, index) => ({
224
+ getId: () => `sugg-krisshop-${index}`,
225
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
226
+ getData: () => s.data,
227
+ }));
228
+
229
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-krisshop');
230
+
231
+ expect(patches.length).to.equal(krisshopFixture.suggestions.length);
232
+
233
+ patches.forEach((patch, index) => {
234
+ expect(patch.op).to.equal('insertAfter');
235
+ expect(patch.selector).to.be.a('string');
236
+ expect(patch.valueFormat).to.equal('hast');
237
+ expect(patch.target).to.equal('ai-bots');
238
+ expect(patch.suggestionId).to.equal(`sugg-krisshop-${index}`);
239
+ });
240
+ });
241
+ });
242
+
243
+ describe('suggestionsToPatches - with Veseris fixture', () => {
244
+ it('should convert Veseris suggestions to patches', () => {
245
+ const suggestions = veserisFixture.suggestions.map((s, index) => ({
246
+ getId: () => `sugg-veseris-${index}`,
247
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
248
+ getData: () => s.data,
249
+ }));
250
+
251
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-veseris');
252
+
253
+ expect(patches.length).to.equal(veserisFixture.suggestions.length);
254
+
255
+ patches.forEach((patch, index) => {
256
+ expect(patch.op).to.equal('insertAfter');
257
+ expect(patch.selector).to.be.a('string');
258
+ expect(patch.valueFormat).to.equal('hast');
259
+ expect(patch.target).to.equal('ai-bots');
260
+ expect(patch.suggestionId).to.equal(`sugg-veseris-${index}`);
261
+ });
262
+ });
263
+ });
264
+
265
+ describe('suggestionsToPatches - with Vuse fixture', () => {
266
+ it('should convert Vuse suggestions to patches', () => {
267
+ const suggestions = vuseFixture.suggestions.map((s, index) => ({
268
+ getId: () => `sugg-vuse-${index}`,
269
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
270
+ getData: () => s.data,
271
+ }));
272
+
273
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-vuse');
274
+
275
+ expect(patches.length).to.equal(vuseFixture.suggestions.length);
276
+
277
+ patches.forEach((patch, index) => {
278
+ expect(patch.op).to.equal('insertAfter');
279
+ expect(patch.selector).to.be.a('string');
280
+ expect(patch.valueFormat).to.equal('hast');
281
+ expect(patch.target).to.equal('ai-bots');
282
+ expect(patch.suggestionId).to.equal(`sugg-vuse-${index}`);
283
+ });
284
+ });
285
+ });
286
+
287
+ describe('suggestionsToPatches - edge cases', () => {
288
+ it('should return empty array for invalid suggestions', () => {
289
+ const suggestions = [{
290
+ getId: () => 'sugg-invalid',
291
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
292
+ getData: () => ({
293
+ // Missing semanticHtml
294
+ transformRules: {
295
+ action: 'insertAfter',
296
+ selector: 'img',
297
+ },
298
+ }),
299
+ }];
300
+
301
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-1');
302
+ expect(patches.length).to.equal(0);
303
+ });
304
+
305
+ it('should skip invalid suggestions but process valid ones', () => {
306
+ const suggestions = [
307
+ {
308
+ getId: () => 'sugg-invalid',
309
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
310
+ getData: () => ({ transformRules: { action: 'insertAfter', selector: 'img' } }),
311
+ },
312
+ {
313
+ getId: () => 'sugg-valid',
314
+ getUpdatedAt: () => '2025-02-13T10:00:00.000Z',
315
+ getData: () => ({
316
+ semanticHtml: '<section><h2>Valid</h2></section>',
317
+ transformRules: { action: 'insertAfter', selector: 'img[src="test.jpg"]' },
318
+ }),
319
+ },
320
+ ];
321
+
322
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-1');
323
+ expect(patches.length).to.equal(1);
324
+ expect(patches[0].suggestionId).to.equal('sugg-valid');
325
+ });
326
+
327
+ it('should handle empty suggestions array', () => {
328
+ const patches = mapper.suggestionsToPatches('/page', [], 'opp-1');
329
+ expect(patches.length).to.equal(0);
330
+ });
331
+ });
332
+ });
@@ -0,0 +1,88 @@
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 { htmlToHast } from '../../src/utils/html-utils.js';
17
+
18
+ describe('htmlToHast', () => {
19
+ it('should convert simple HTML to HAST', () => {
20
+ const html = '<section><h2>Hello</h2></section>';
21
+ const result = htmlToHast(html);
22
+
23
+ expect(result).to.be.an('object');
24
+ expect(result.type).to.equal('root');
25
+ expect(result.children).to.be.an('array');
26
+ });
27
+
28
+ it('should convert HTML with data attributes to HAST', () => {
29
+ const html = '<section data-llm-context="image" data-llm-shadow="image-text"><h2>Test</h2></section>';
30
+ const result = htmlToHast(html);
31
+
32
+ expect(result.type).to.equal('root');
33
+ const section = result.children.find((c) => c.tagName === 'section');
34
+ expect(section).to.exist;
35
+ expect(section.properties.dataLlmContext).to.equal('image');
36
+ expect(section.properties.dataLlmShadow).to.equal('image-text');
37
+ });
38
+
39
+ it('should convert HTML with nested elements', () => {
40
+ const html = '<section><h2>Headline</h2><p>Description</p><button>CTA</button></section>';
41
+ const result = htmlToHast(html);
42
+
43
+ const section = result.children.find((c) => c.tagName === 'section');
44
+ expect(section.children).to.be.an('array');
45
+
46
+ const tagNames = section.children
47
+ .filter((c) => c.type === 'element')
48
+ .map((c) => c.tagName);
49
+ expect(tagNames).to.include('h2');
50
+ expect(tagNames).to.include('p');
51
+ expect(tagNames).to.include('button');
52
+ });
53
+
54
+ it('should handle real semantic HTML from Mystique', () => {
55
+ const html = `<section data-llm-context="image" data-llm-shadow="image-text" data-image-id="https://example.com/image.jpg">
56
+ <h2>Carahsoft and Partners at WEST 2026</h2>
57
+ <span>February 10 - 12, 2026 • San Diego, CA</span>
58
+ <button>Learn More</button>
59
+ </section>`;
60
+
61
+ const result = htmlToHast(html);
62
+
63
+ expect(result.type).to.equal('root');
64
+ const section = result.children.find((c) => c.tagName === 'section');
65
+ expect(section).to.exist;
66
+ expect(section.properties.dataLlmContext).to.equal('image');
67
+ expect(section.properties.dataImageId).to.equal('https://example.com/image.jpg');
68
+ });
69
+
70
+ it('should handle empty HTML', () => {
71
+ const html = '';
72
+ const result = htmlToHast(html);
73
+
74
+ expect(result.type).to.equal('root');
75
+ expect(result.children).to.be.an('array');
76
+ });
77
+
78
+ it('should handle HTML with special characters', () => {
79
+ const html = '<section><p>Price: €10 &amp; more</p></section>';
80
+ const result = htmlToHast(html);
81
+
82
+ const section = result.children.find((c) => c.tagName === 'section');
83
+ const p = section.children.find((c) => c.tagName === 'p');
84
+ const textContent = p.children.find((c) => c.type === 'text');
85
+ expect(textContent.value).to.include('€10');
86
+ expect(textContent.value).to.include('&');
87
+ });
88
+ });