@rankcli/agent-runtime 0.0.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.
Files changed (178) hide show
  1. package/README.md +242 -0
  2. package/dist/analyzer-2CSWIQGD.mjs +6 -0
  3. package/dist/chunk-YNZYHEYM.mjs +774 -0
  4. package/dist/index.d.mts +4012 -0
  5. package/dist/index.d.ts +4012 -0
  6. package/dist/index.js +29672 -0
  7. package/dist/index.mjs +28602 -0
  8. package/package.json +53 -0
  9. package/scripts/build-deno.ts +134 -0
  10. package/src/audit/ai/analyzer.ts +347 -0
  11. package/src/audit/ai/index.ts +29 -0
  12. package/src/audit/ai/prompts/content-analysis.ts +271 -0
  13. package/src/audit/ai/types.ts +179 -0
  14. package/src/audit/checks/additional-checks.ts +439 -0
  15. package/src/audit/checks/ai-citation-worthiness.ts +399 -0
  16. package/src/audit/checks/ai-content-structure.ts +325 -0
  17. package/src/audit/checks/ai-readiness.ts +339 -0
  18. package/src/audit/checks/anchor-text.ts +179 -0
  19. package/src/audit/checks/answer-conciseness.ts +322 -0
  20. package/src/audit/checks/asset-minification.ts +270 -0
  21. package/src/audit/checks/bing-optimization.ts +206 -0
  22. package/src/audit/checks/brand-mention-optimization.ts +349 -0
  23. package/src/audit/checks/caching-headers.ts +305 -0
  24. package/src/audit/checks/canonical-advanced.ts +150 -0
  25. package/src/audit/checks/canonical-domain.ts +196 -0
  26. package/src/audit/checks/citation-quality.ts +358 -0
  27. package/src/audit/checks/client-rendering.ts +542 -0
  28. package/src/audit/checks/color-contrast.ts +342 -0
  29. package/src/audit/checks/content-freshness.ts +170 -0
  30. package/src/audit/checks/content-science.ts +589 -0
  31. package/src/audit/checks/conversion-elements.ts +526 -0
  32. package/src/audit/checks/crawlability.ts +220 -0
  33. package/src/audit/checks/directory-listing.ts +172 -0
  34. package/src/audit/checks/dom-analysis.ts +191 -0
  35. package/src/audit/checks/dom-size.ts +246 -0
  36. package/src/audit/checks/duplicate-content.ts +194 -0
  37. package/src/audit/checks/eeat-signals.ts +990 -0
  38. package/src/audit/checks/entity-seo.ts +396 -0
  39. package/src/audit/checks/featured-snippet.ts +473 -0
  40. package/src/audit/checks/freshness-signals.ts +443 -0
  41. package/src/audit/checks/funnel-intent.ts +463 -0
  42. package/src/audit/checks/hreflang.ts +174 -0
  43. package/src/audit/checks/html-compliance.ts +302 -0
  44. package/src/audit/checks/image-dimensions.ts +167 -0
  45. package/src/audit/checks/images.ts +160 -0
  46. package/src/audit/checks/indexnow.ts +275 -0
  47. package/src/audit/checks/interactive-tools.ts +475 -0
  48. package/src/audit/checks/internal-link-graph.ts +436 -0
  49. package/src/audit/checks/keyword-analysis.ts +239 -0
  50. package/src/audit/checks/keyword-cannibalization.ts +385 -0
  51. package/src/audit/checks/keyword-placement.ts +471 -0
  52. package/src/audit/checks/links.ts +203 -0
  53. package/src/audit/checks/llms-txt.ts +224 -0
  54. package/src/audit/checks/local-seo.ts +296 -0
  55. package/src/audit/checks/mobile.ts +167 -0
  56. package/src/audit/checks/modern-images.ts +226 -0
  57. package/src/audit/checks/navboost-signals.ts +395 -0
  58. package/src/audit/checks/on-page.ts +209 -0
  59. package/src/audit/checks/page-resources.ts +285 -0
  60. package/src/audit/checks/pagination.ts +180 -0
  61. package/src/audit/checks/performance.ts +153 -0
  62. package/src/audit/checks/platform-presence.ts +580 -0
  63. package/src/audit/checks/redirect-analysis.ts +153 -0
  64. package/src/audit/checks/redirect-chain.ts +389 -0
  65. package/src/audit/checks/resource-hints.ts +420 -0
  66. package/src/audit/checks/responsive-css.ts +247 -0
  67. package/src/audit/checks/responsive-images.ts +396 -0
  68. package/src/audit/checks/review-ecosystem.ts +415 -0
  69. package/src/audit/checks/robots-validation.ts +373 -0
  70. package/src/audit/checks/security-headers.ts +172 -0
  71. package/src/audit/checks/security.ts +144 -0
  72. package/src/audit/checks/serp-preview.ts +251 -0
  73. package/src/audit/checks/site-maturity.ts +444 -0
  74. package/src/audit/checks/social-meta.test.ts +275 -0
  75. package/src/audit/checks/social-meta.ts +134 -0
  76. package/src/audit/checks/soft-404.ts +151 -0
  77. package/src/audit/checks/structured-data.ts +238 -0
  78. package/src/audit/checks/tech-detection.ts +496 -0
  79. package/src/audit/checks/topical-clusters.ts +435 -0
  80. package/src/audit/checks/tracker-bloat.ts +462 -0
  81. package/src/audit/checks/tracking-verification.test.ts +371 -0
  82. package/src/audit/checks/tracking-verification.ts +636 -0
  83. package/src/audit/checks/url-safety.ts +682 -0
  84. package/src/audit/deno-entry.ts +66 -0
  85. package/src/audit/discovery/index.ts +15 -0
  86. package/src/audit/discovery/link-crawler.ts +232 -0
  87. package/src/audit/discovery/repo-routes.ts +347 -0
  88. package/src/audit/engine.ts +620 -0
  89. package/src/audit/fixes/index.ts +209 -0
  90. package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
  91. package/src/audit/fixes/social-meta-fixes.ts +463 -0
  92. package/src/audit/index.ts +74 -0
  93. package/src/audit/runner.test.ts +299 -0
  94. package/src/audit/runner.ts +130 -0
  95. package/src/audit/types.ts +1953 -0
  96. package/src/content/featured-snippet.ts +367 -0
  97. package/src/content/generator.test.ts +534 -0
  98. package/src/content/generator.ts +501 -0
  99. package/src/content/headline.ts +317 -0
  100. package/src/content/index.ts +62 -0
  101. package/src/content/intent.ts +258 -0
  102. package/src/content/keyword-density.ts +349 -0
  103. package/src/content/readability.ts +262 -0
  104. package/src/executor.ts +336 -0
  105. package/src/fixer.ts +416 -0
  106. package/src/frameworks/detector.test.ts +248 -0
  107. package/src/frameworks/detector.ts +371 -0
  108. package/src/frameworks/index.ts +68 -0
  109. package/src/frameworks/recipes/angular.yaml +171 -0
  110. package/src/frameworks/recipes/astro.yaml +206 -0
  111. package/src/frameworks/recipes/django.yaml +180 -0
  112. package/src/frameworks/recipes/laravel.yaml +137 -0
  113. package/src/frameworks/recipes/nextjs.yaml +268 -0
  114. package/src/frameworks/recipes/nuxt.yaml +175 -0
  115. package/src/frameworks/recipes/rails.yaml +188 -0
  116. package/src/frameworks/recipes/react.yaml +202 -0
  117. package/src/frameworks/recipes/sveltekit.yaml +154 -0
  118. package/src/frameworks/recipes/vue.yaml +137 -0
  119. package/src/frameworks/recipes/wordpress.yaml +209 -0
  120. package/src/frameworks/suggestion-engine.ts +320 -0
  121. package/src/geo/geo-content.test.ts +305 -0
  122. package/src/geo/geo-content.ts +266 -0
  123. package/src/geo/geo-history.test.ts +473 -0
  124. package/src/geo/geo-history.ts +433 -0
  125. package/src/geo/geo-tracker.test.ts +359 -0
  126. package/src/geo/geo-tracker.ts +411 -0
  127. package/src/geo/index.ts +10 -0
  128. package/src/git/commit-helper.test.ts +261 -0
  129. package/src/git/commit-helper.ts +329 -0
  130. package/src/git/index.ts +12 -0
  131. package/src/git/pr-helper.test.ts +284 -0
  132. package/src/git/pr-helper.ts +307 -0
  133. package/src/index.ts +66 -0
  134. package/src/keywords/ai-keyword-engine.ts +1062 -0
  135. package/src/keywords/ai-summarizer.ts +387 -0
  136. package/src/keywords/ci-mode.ts +555 -0
  137. package/src/keywords/engine.ts +359 -0
  138. package/src/keywords/index.ts +151 -0
  139. package/src/keywords/llm-judge.ts +357 -0
  140. package/src/keywords/nlp-analysis.ts +706 -0
  141. package/src/keywords/prioritizer.ts +295 -0
  142. package/src/keywords/site-crawler.ts +342 -0
  143. package/src/keywords/sources/autocomplete.ts +139 -0
  144. package/src/keywords/sources/competitive-search.ts +450 -0
  145. package/src/keywords/sources/competitor-analysis.ts +374 -0
  146. package/src/keywords/sources/dataforseo.ts +206 -0
  147. package/src/keywords/sources/free-sources.ts +294 -0
  148. package/src/keywords/sources/gsc.ts +123 -0
  149. package/src/keywords/topic-grouping.ts +327 -0
  150. package/src/keywords/types.ts +144 -0
  151. package/src/keywords/wizard.ts +457 -0
  152. package/src/loader.ts +40 -0
  153. package/src/reports/index.ts +7 -0
  154. package/src/reports/report-generator.test.ts +293 -0
  155. package/src/reports/report-generator.ts +713 -0
  156. package/src/scheduler/alerts.test.ts +458 -0
  157. package/src/scheduler/alerts.ts +328 -0
  158. package/src/scheduler/index.ts +8 -0
  159. package/src/scheduler/scheduled-audit.test.ts +377 -0
  160. package/src/scheduler/scheduled-audit.ts +149 -0
  161. package/src/test/integration-test.ts +325 -0
  162. package/src/tools/analyzer.ts +373 -0
  163. package/src/tools/crawl.ts +293 -0
  164. package/src/tools/files.ts +301 -0
  165. package/src/tools/h1-fixer.ts +249 -0
  166. package/src/tools/index.ts +67 -0
  167. package/src/tracking/github-action.ts +326 -0
  168. package/src/tracking/google-analytics.ts +265 -0
  169. package/src/tracking/index.ts +45 -0
  170. package/src/tracking/report-generator.ts +386 -0
  171. package/src/tracking/search-console.ts +335 -0
  172. package/src/types.ts +134 -0
  173. package/src/utils/http.ts +302 -0
  174. package/src/wasm-adapter.ts +297 -0
  175. package/src/wasm-entry.ts +14 -0
  176. package/tsconfig.json +17 -0
  177. package/tsup.wasm.config.ts +26 -0
  178. package/vitest.config.ts +15 -0
@@ -0,0 +1,473 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ GEOHistory,
4
+ GEOTrend,
5
+ GEOAlert,
6
+ GEOAlertType,
7
+ createGEOHistory,
8
+ addTrackingResult,
9
+ getVisibilityTrend,
10
+ detectVisibilityChanges,
11
+ generateGEOReport,
12
+ compareCompetitorVisibility,
13
+ CompetitorComparison,
14
+ } from './geo-history.js';
15
+ import { GEOResult } from './geo-tracker.js';
16
+
17
+ describe('geo-history', () => {
18
+ const mockResult: GEOResult = {
19
+ provider: 'openai',
20
+ keyword: 'best seo cli tool',
21
+ mentioned: true,
22
+ position: 2,
23
+ sentiment: 'positive',
24
+ score: 75,
25
+ timestamp: '2024-01-15T10:00:00Z',
26
+ };
27
+
28
+ describe('createGEOHistory', () => {
29
+ it('creates empty history', () => {
30
+ const history = createGEOHistory();
31
+
32
+ expect(history.results).toEqual([]);
33
+ expect(history.brandName).toBeUndefined();
34
+ });
35
+
36
+ it('creates history with brand name', () => {
37
+ const history = createGEOHistory({ brandName: 'SEO Autopilot' });
38
+
39
+ expect(history.brandName).toBe('SEO Autopilot');
40
+ });
41
+ });
42
+
43
+ describe('addTrackingResult', () => {
44
+ it('adds result to history', () => {
45
+ const history = createGEOHistory();
46
+ const updated = addTrackingResult(history, mockResult);
47
+
48
+ expect(updated.results).toHaveLength(1);
49
+ expect(updated.results[0]).toEqual(mockResult);
50
+ });
51
+
52
+ it('maintains chronological order', () => {
53
+ const history = createGEOHistory();
54
+ const older = { ...mockResult, timestamp: '2024-01-14T10:00:00Z' };
55
+ const newer = { ...mockResult, timestamp: '2024-01-16T10:00:00Z' };
56
+
57
+ let updated = addTrackingResult(history, newer);
58
+ updated = addTrackingResult(updated, older);
59
+
60
+ expect(updated.results[0].timestamp).toBe('2024-01-14T10:00:00Z');
61
+ expect(updated.results[1].timestamp).toBe('2024-01-16T10:00:00Z');
62
+ });
63
+
64
+ it('limits history size when maxResults specified', () => {
65
+ const history = createGEOHistory({ maxResults: 3 });
66
+ let updated = history;
67
+
68
+ for (let i = 0; i < 5; i++) {
69
+ updated = addTrackingResult(updated, {
70
+ ...mockResult,
71
+ timestamp: `2024-01-${10 + i}T10:00:00Z`,
72
+ });
73
+ }
74
+
75
+ expect(updated.results).toHaveLength(3);
76
+ // Should keep most recent
77
+ expect(updated.results[2].timestamp).toBe('2024-01-14T10:00:00Z');
78
+ });
79
+ });
80
+
81
+ describe('getVisibilityTrend', () => {
82
+ it('calculates trend from history', () => {
83
+ const history = createGEOHistory();
84
+ const results: GEOResult[] = [
85
+ { ...mockResult, timestamp: '2024-01-10T10:00:00Z', score: 50, mentioned: true },
86
+ { ...mockResult, timestamp: '2024-01-11T10:00:00Z', score: 60, mentioned: true },
87
+ { ...mockResult, timestamp: '2024-01-12T10:00:00Z', score: 70, mentioned: true },
88
+ { ...mockResult, timestamp: '2024-01-13T10:00:00Z', score: 80, mentioned: true },
89
+ ];
90
+
91
+ let updated = history;
92
+ for (const r of results) {
93
+ updated = addTrackingResult(updated, r);
94
+ }
95
+
96
+ const trend = getVisibilityTrend(updated, 'openai', 'best seo cli tool');
97
+
98
+ expect(trend.direction).toBe('improving');
99
+ expect(trend.averageScore).toBe(65); // (50+60+70+80)/4
100
+ expect(trend.mentionRate).toBe(100); // 4/4 * 100
101
+ });
102
+
103
+ it('detects declining trend', () => {
104
+ const history = createGEOHistory();
105
+ const results: GEOResult[] = [
106
+ { ...mockResult, timestamp: '2024-01-10T10:00:00Z', score: 80 },
107
+ { ...mockResult, timestamp: '2024-01-11T10:00:00Z', score: 70 },
108
+ { ...mockResult, timestamp: '2024-01-12T10:00:00Z', score: 50 },
109
+ { ...mockResult, timestamp: '2024-01-13T10:00:00Z', score: 30 },
110
+ ];
111
+
112
+ let updated = history;
113
+ for (const r of results) {
114
+ updated = addTrackingResult(updated, r);
115
+ }
116
+
117
+ const trend = getVisibilityTrend(updated, 'openai', 'best seo cli tool');
118
+
119
+ expect(trend.direction).toBe('declining');
120
+ });
121
+
122
+ it('detects stable trend', () => {
123
+ const history = createGEOHistory();
124
+ const results: GEOResult[] = [
125
+ { ...mockResult, timestamp: '2024-01-10T10:00:00Z', score: 70 },
126
+ { ...mockResult, timestamp: '2024-01-11T10:00:00Z', score: 72 },
127
+ { ...mockResult, timestamp: '2024-01-12T10:00:00Z', score: 68 },
128
+ { ...mockResult, timestamp: '2024-01-13T10:00:00Z', score: 71 },
129
+ ];
130
+
131
+ let updated = history;
132
+ for (const r of results) {
133
+ updated = addTrackingResult(updated, r);
134
+ }
135
+
136
+ const trend = getVisibilityTrend(updated, 'openai', 'best seo cli tool');
137
+
138
+ expect(trend.direction).toBe('stable');
139
+ });
140
+
141
+ it('returns empty trend for no data', () => {
142
+ const history = createGEOHistory();
143
+ const trend = getVisibilityTrend(history, 'openai', 'unknown keyword');
144
+
145
+ expect(trend.direction).toBe('unknown');
146
+ expect(trend.dataPoints).toBe(0);
147
+ });
148
+ });
149
+
150
+ describe('detectVisibilityChanges', () => {
151
+ it('detects new mention', () => {
152
+ const history = createGEOHistory();
153
+ const oldResults: GEOResult[] = [
154
+ { ...mockResult, mentioned: false, timestamp: '2024-01-10T10:00:00Z' },
155
+ { ...mockResult, mentioned: false, timestamp: '2024-01-11T10:00:00Z' },
156
+ ];
157
+ const newResult = { ...mockResult, mentioned: true, timestamp: '2024-01-12T10:00:00Z' };
158
+
159
+ let updated = history;
160
+ for (const r of oldResults) {
161
+ updated = addTrackingResult(updated, r);
162
+ }
163
+
164
+ const alerts = detectVisibilityChanges(updated, newResult);
165
+
166
+ expect(alerts).toHaveLength(1);
167
+ expect(alerts[0].type).toBe('new_mention');
168
+ });
169
+
170
+ it('detects lost mention', () => {
171
+ const history = createGEOHistory();
172
+ const oldResults: GEOResult[] = [
173
+ { ...mockResult, mentioned: true, timestamp: '2024-01-10T10:00:00Z' },
174
+ { ...mockResult, mentioned: true, timestamp: '2024-01-11T10:00:00Z' },
175
+ ];
176
+ const newResult = { ...mockResult, mentioned: false, timestamp: '2024-01-12T10:00:00Z' };
177
+
178
+ let updated = history;
179
+ for (const r of oldResults) {
180
+ updated = addTrackingResult(updated, r);
181
+ }
182
+
183
+ const alerts = detectVisibilityChanges(updated, newResult);
184
+
185
+ expect(alerts).toHaveLength(1);
186
+ expect(alerts[0].type).toBe('lost_mention');
187
+ });
188
+
189
+ it('detects position improvement', () => {
190
+ const history = createGEOHistory();
191
+ const oldResults: GEOResult[] = [
192
+ { ...mockResult, position: 5, timestamp: '2024-01-10T10:00:00Z' },
193
+ { ...mockResult, position: 5, timestamp: '2024-01-11T10:00:00Z' },
194
+ ];
195
+ const newResult = { ...mockResult, position: 1, timestamp: '2024-01-12T10:00:00Z' };
196
+
197
+ let updated = history;
198
+ for (const r of oldResults) {
199
+ updated = addTrackingResult(updated, r);
200
+ }
201
+
202
+ const alerts = detectVisibilityChanges(updated, newResult);
203
+
204
+ expect(alerts.some(a => a.type === 'position_improved')).toBe(true);
205
+ });
206
+
207
+ it('detects position drop', () => {
208
+ const history = createGEOHistory();
209
+ const oldResults: GEOResult[] = [
210
+ { ...mockResult, position: 1, timestamp: '2024-01-10T10:00:00Z' },
211
+ { ...mockResult, position: 1, timestamp: '2024-01-11T10:00:00Z' },
212
+ ];
213
+ const newResult = { ...mockResult, position: 5, timestamp: '2024-01-12T10:00:00Z' };
214
+
215
+ let updated = history;
216
+ for (const r of oldResults) {
217
+ updated = addTrackingResult(updated, r);
218
+ }
219
+
220
+ const alerts = detectVisibilityChanges(updated, newResult);
221
+
222
+ expect(alerts.some(a => a.type === 'position_dropped')).toBe(true);
223
+ });
224
+
225
+ it('detects sentiment change', () => {
226
+ const history = createGEOHistory();
227
+ const oldResults: GEOResult[] = [
228
+ { ...mockResult, sentiment: 'positive', timestamp: '2024-01-10T10:00:00Z' },
229
+ { ...mockResult, sentiment: 'positive', timestamp: '2024-01-11T10:00:00Z' },
230
+ ];
231
+ const newResult: GEOResult = { ...mockResult, sentiment: 'negative' as const, timestamp: '2024-01-12T10:00:00Z' };
232
+
233
+ let updated = history;
234
+ for (const r of oldResults) {
235
+ updated = addTrackingResult(updated, r);
236
+ }
237
+
238
+ const alerts = detectVisibilityChanges(updated, newResult);
239
+
240
+ expect(alerts.some(a => a.type === 'sentiment_changed')).toBe(true);
241
+ });
242
+
243
+ it('returns empty alerts for no significant change', () => {
244
+ const history = createGEOHistory();
245
+ const oldResults: GEOResult[] = [
246
+ { ...mockResult, timestamp: '2024-01-10T10:00:00Z' },
247
+ { ...mockResult, timestamp: '2024-01-11T10:00:00Z' },
248
+ ];
249
+ const newResult = { ...mockResult, timestamp: '2024-01-12T10:00:00Z' };
250
+
251
+ let updated = history;
252
+ for (const r of oldResults) {
253
+ updated = addTrackingResult(updated, r);
254
+ }
255
+
256
+ const alerts = detectVisibilityChanges(updated, newResult);
257
+
258
+ expect(alerts).toHaveLength(0);
259
+ });
260
+ });
261
+
262
+ describe('generateGEOReport', () => {
263
+ it('generates summary report', () => {
264
+ const history = createGEOHistory({ brandName: 'SEO Autopilot' });
265
+ const results: GEOResult[] = [
266
+ { ...mockResult, provider: 'openai', mentioned: true, score: 80 },
267
+ { ...mockResult, provider: 'anthropic', mentioned: true, score: 70 },
268
+ { ...mockResult, provider: 'google', mentioned: false, score: 0 },
269
+ ];
270
+
271
+ let updated = history;
272
+ for (const r of results) {
273
+ updated = addTrackingResult(updated, r);
274
+ }
275
+
276
+ const report = generateGEOReport(updated);
277
+
278
+ expect(report.brandName).toBe('SEO Autopilot');
279
+ expect(report.totalQueries).toBe(3);
280
+ expect(report.mentionRate).toBeCloseTo(66.67, 1);
281
+ expect(report.averageScore).toBe(50); // (80+70+0)/3
282
+ expect(report.byProvider).toHaveProperty('openai');
283
+ expect(report.byProvider).toHaveProperty('anthropic');
284
+ });
285
+
286
+ it('includes provider breakdown', () => {
287
+ const history = createGEOHistory();
288
+ const results: GEOResult[] = [
289
+ { ...mockResult, provider: 'openai', keyword: 'kw1', score: 80 },
290
+ { ...mockResult, provider: 'openai', keyword: 'kw2', score: 60 },
291
+ { ...mockResult, provider: 'anthropic', keyword: 'kw1', score: 90 },
292
+ ];
293
+
294
+ let updated = history;
295
+ for (const r of results) {
296
+ updated = addTrackingResult(updated, r);
297
+ }
298
+
299
+ const report = generateGEOReport(updated);
300
+
301
+ expect(report.byProvider.openai.queries).toBe(2);
302
+ expect(report.byProvider.openai.averageScore).toBe(70);
303
+ expect(report.byProvider.anthropic.queries).toBe(1);
304
+ });
305
+
306
+ it('includes keyword breakdown', () => {
307
+ const history = createGEOHistory();
308
+ const results: GEOResult[] = [
309
+ { ...mockResult, keyword: 'seo tool', score: 80 },
310
+ { ...mockResult, keyword: 'seo tool', score: 70 },
311
+ { ...mockResult, keyword: 'cli seo', score: 60 },
312
+ ];
313
+
314
+ let updated = history;
315
+ for (const r of results) {
316
+ updated = addTrackingResult(updated, r);
317
+ }
318
+
319
+ const report = generateGEOReport(updated);
320
+
321
+ expect(report.byKeyword['seo tool'].queries).toBe(2);
322
+ expect(report.byKeyword['seo tool'].averageScore).toBe(75);
323
+ });
324
+ });
325
+
326
+ describe('compareCompetitorVisibility', () => {
327
+ it('compares brand against competitors', () => {
328
+ const brandHistory = createGEOHistory({ brandName: 'SEO Autopilot' });
329
+ let brandUpdated = brandHistory;
330
+ brandUpdated = addTrackingResult(brandUpdated, {
331
+ ...mockResult,
332
+ keyword: 'best seo tool',
333
+ mentioned: true,
334
+ position: 2,
335
+ score: 75,
336
+ });
337
+
338
+ const competitorResults: GEOResult[] = [
339
+ { ...mockResult, keyword: 'best seo tool', mentioned: true, position: 1, score: 90 },
340
+ ];
341
+
342
+ const comparison = compareCompetitorVisibility(
343
+ brandUpdated,
344
+ 'Ahrefs',
345
+ competitorResults
346
+ );
347
+
348
+ expect(comparison.brand).toBe('SEO Autopilot');
349
+ expect(comparison.competitor).toBe('Ahrefs');
350
+ expect(comparison.brandMentionRate).toBe(100);
351
+ expect(comparison.competitorMentionRate).toBe(100);
352
+ expect(comparison.brandAveragePosition).toBe(2);
353
+ expect(comparison.competitorAveragePosition).toBe(1);
354
+ });
355
+
356
+ it('handles unmentioned competitor', () => {
357
+ const brandHistory = createGEOHistory({ brandName: 'SEO Autopilot' });
358
+ let brandUpdated = brandHistory;
359
+ brandUpdated = addTrackingResult(brandUpdated, {
360
+ ...mockResult,
361
+ mentioned: true,
362
+ position: 1,
363
+ });
364
+
365
+ const competitorResults: GEOResult[] = [
366
+ { ...mockResult, mentioned: false, position: null, score: 0 },
367
+ ];
368
+
369
+ const comparison = compareCompetitorVisibility(
370
+ brandUpdated,
371
+ 'Unknown Tool',
372
+ competitorResults
373
+ );
374
+
375
+ expect(comparison.competitorMentionRate).toBe(0);
376
+ expect(comparison.winner).toBe('SEO Autopilot');
377
+ });
378
+
379
+ it('determines winner by mention rate and position', () => {
380
+ const brandHistory = createGEOHistory({ brandName: 'SEO Autopilot' });
381
+ let brandUpdated = brandHistory;
382
+ brandUpdated = addTrackingResult(brandUpdated, {
383
+ ...mockResult,
384
+ mentioned: true,
385
+ position: 3,
386
+ score: 60,
387
+ });
388
+
389
+ const competitorResults: GEOResult[] = [
390
+ { ...mockResult, mentioned: true, position: 1, score: 90 },
391
+ ];
392
+
393
+ const comparison = compareCompetitorVisibility(
394
+ brandUpdated,
395
+ 'Ahrefs',
396
+ competitorResults
397
+ );
398
+
399
+ expect(comparison.winner).toBe('Ahrefs');
400
+ });
401
+ });
402
+
403
+ describe('integration scenarios', () => {
404
+ it('tracks visibility over time and generates alerts', () => {
405
+ const history = createGEOHistory({ brandName: 'SEO Autopilot' });
406
+
407
+ // Day 1: Not mentioned
408
+ let updated = addTrackingResult(history, {
409
+ ...mockResult,
410
+ mentioned: false,
411
+ timestamp: '2024-01-10T10:00:00Z',
412
+ });
413
+
414
+ // Day 2: Still not mentioned
415
+ updated = addTrackingResult(updated, {
416
+ ...mockResult,
417
+ mentioned: false,
418
+ timestamp: '2024-01-11T10:00:00Z',
419
+ });
420
+
421
+ // Day 3: Now mentioned!
422
+ const newResult: GEOResult = {
423
+ ...mockResult,
424
+ mentioned: true,
425
+ position: 3,
426
+ timestamp: '2024-01-12T10:00:00Z',
427
+ };
428
+
429
+ const alerts = detectVisibilityChanges(updated, newResult);
430
+
431
+ expect(alerts.some(a => a.type === 'new_mention')).toBe(true);
432
+
433
+ // Add to history
434
+ updated = addTrackingResult(updated, newResult);
435
+
436
+ // Check trend
437
+ const trend = getVisibilityTrend(updated, 'openai', 'best seo cli tool');
438
+ expect(trend.mentionRate).toBeCloseTo(33.33, 1); // 1/3
439
+ });
440
+
441
+ it('full workflow with report generation', () => {
442
+ const history = createGEOHistory({ brandName: 'SEO Autopilot' });
443
+
444
+ // Simulate a week of tracking across providers
445
+ const providers = ['openai', 'anthropic', 'google'] as const;
446
+ const keywords = ['seo cli tool', 'developer seo'];
447
+
448
+ let updated = history;
449
+ let day = 10;
450
+
451
+ for (const provider of providers) {
452
+ for (const keyword of keywords) {
453
+ updated = addTrackingResult(updated, {
454
+ provider,
455
+ keyword,
456
+ mentioned: Math.random() > 0.3,
457
+ position: Math.floor(Math.random() * 5) + 1,
458
+ sentiment: 'positive',
459
+ score: Math.floor(Math.random() * 50) + 50,
460
+ timestamp: `2024-01-${day}T10:00:00Z`,
461
+ });
462
+ day++;
463
+ }
464
+ }
465
+
466
+ const report = generateGEOReport(updated);
467
+
468
+ expect(report.totalQueries).toBe(6);
469
+ expect(Object.keys(report.byProvider)).toHaveLength(3);
470
+ expect(Object.keys(report.byKeyword)).toHaveLength(2);
471
+ });
472
+ });
473
+ });