@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.
- package/README.md +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- 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
|
+
});
|