@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,377 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
runScheduledAudit,
|
|
4
|
+
shouldRunAudit,
|
|
5
|
+
calculateNextRun,
|
|
6
|
+
ScheduledAuditConfig,
|
|
7
|
+
ScheduledAuditResult,
|
|
8
|
+
} from './scheduled-audit.js';
|
|
9
|
+
|
|
10
|
+
// Mock the runner
|
|
11
|
+
vi.mock('../audit/runner.js', () => ({
|
|
12
|
+
runAuditWithFixes: vi.fn(),
|
|
13
|
+
createAuditPR: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('scheduled-audit', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('shouldRunAudit', () => {
|
|
27
|
+
it('returns true when schedule is "daily" and last run was more than 24 hours ago', () => {
|
|
28
|
+
const config: ScheduledAuditConfig = {
|
|
29
|
+
projectId: 'proj-1',
|
|
30
|
+
url: 'https://example.com',
|
|
31
|
+
schedule: 'daily',
|
|
32
|
+
lastRunAt: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(), // 25 hours ago
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
expect(shouldRunAudit(config)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false when schedule is "daily" and last run was less than 24 hours ago', () => {
|
|
39
|
+
const config: ScheduledAuditConfig = {
|
|
40
|
+
projectId: 'proj-1',
|
|
41
|
+
url: 'https://example.com',
|
|
42
|
+
schedule: 'daily',
|
|
43
|
+
lastRunAt: new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString(), // 12 hours ago
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
expect(shouldRunAudit(config)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns true when schedule is "weekly" and last run was more than 7 days ago', () => {
|
|
50
|
+
const config: ScheduledAuditConfig = {
|
|
51
|
+
projectId: 'proj-1',
|
|
52
|
+
url: 'https://example.com',
|
|
53
|
+
schedule: 'weekly',
|
|
54
|
+
lastRunAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(), // 8 days ago
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
expect(shouldRunAudit(config)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns false when schedule is "weekly" and last run was less than 7 days ago', () => {
|
|
61
|
+
const config: ScheduledAuditConfig = {
|
|
62
|
+
projectId: 'proj-1',
|
|
63
|
+
url: 'https://example.com',
|
|
64
|
+
schedule: 'weekly',
|
|
65
|
+
lastRunAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
expect(shouldRunAudit(config)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns true when no last run exists', () => {
|
|
72
|
+
const config: ScheduledAuditConfig = {
|
|
73
|
+
projectId: 'proj-1',
|
|
74
|
+
url: 'https://example.com',
|
|
75
|
+
schedule: 'daily',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
expect(shouldRunAudit(config)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns false when schedule is "manual"', () => {
|
|
82
|
+
const config: ScheduledAuditConfig = {
|
|
83
|
+
projectId: 'proj-1',
|
|
84
|
+
url: 'https://example.com',
|
|
85
|
+
schedule: 'manual',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(shouldRunAudit(config)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns true when schedule is "on-demand"', () => {
|
|
92
|
+
const config: ScheduledAuditConfig = {
|
|
93
|
+
projectId: 'proj-1',
|
|
94
|
+
url: 'https://example.com',
|
|
95
|
+
schedule: 'on-demand',
|
|
96
|
+
lastRunAt: new Date().toISOString(), // Just now, but on-demand should always run
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
expect(shouldRunAudit(config)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('calculateNextRun', () => {
|
|
104
|
+
it('returns next day for daily schedule', () => {
|
|
105
|
+
vi.setSystemTime(new Date('2024-01-15T10:00:00Z'));
|
|
106
|
+
|
|
107
|
+
const result = calculateNextRun('daily');
|
|
108
|
+
expect(result).toBe('2024-01-16T10:00:00.000Z');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns next week for weekly schedule', () => {
|
|
112
|
+
vi.setSystemTime(new Date('2024-01-15T10:00:00Z'));
|
|
113
|
+
|
|
114
|
+
const result = calculateNextRun('weekly');
|
|
115
|
+
expect(result).toBe('2024-01-22T10:00:00.000Z');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns null for manual schedule', () => {
|
|
119
|
+
const result = calculateNextRun('manual');
|
|
120
|
+
expect(result).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns null for on-demand schedule', () => {
|
|
124
|
+
const result = calculateNextRun('on-demand');
|
|
125
|
+
expect(result).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('runScheduledAudit', () => {
|
|
130
|
+
it('runs audit and returns result with score', async () => {
|
|
131
|
+
const { runAuditWithFixes } = await import('../audit/runner.js');
|
|
132
|
+
|
|
133
|
+
vi.mocked(runAuditWithFixes).mockResolvedValue({
|
|
134
|
+
report: {
|
|
135
|
+
url: 'https://example.com',
|
|
136
|
+
domain: 'example.com',
|
|
137
|
+
timestamp: '2024-01-15T10:00:00Z',
|
|
138
|
+
crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
|
|
139
|
+
healthScore: { overall: 75, crawlability: 80, indexability: 75, onPage: 70, content: 80, links: 75, performance: 70, security: 80, aiReadiness: 75, social: 70, localSeo: 70 },
|
|
140
|
+
issues: [],
|
|
141
|
+
pages: [],
|
|
142
|
+
summary: { errors: 0, warnings: 2, notices: 1, passed: 82 },
|
|
143
|
+
},
|
|
144
|
+
fixes: [],
|
|
145
|
+
score: 75,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const config: ScheduledAuditConfig = {
|
|
149
|
+
projectId: 'proj-1',
|
|
150
|
+
url: 'https://example.com',
|
|
151
|
+
schedule: 'daily',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = await runScheduledAudit(config);
|
|
155
|
+
|
|
156
|
+
expect(result.success).toBe(true);
|
|
157
|
+
expect(result.score).toBe(75);
|
|
158
|
+
expect(result.issuesFound).toBe(0);
|
|
159
|
+
expect(runAuditWithFixes).toHaveBeenCalledWith(
|
|
160
|
+
expect.objectContaining({ url: 'https://example.com' })
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('includes previous score when available', async () => {
|
|
165
|
+
const { runAuditWithFixes } = await import('../audit/runner.js');
|
|
166
|
+
|
|
167
|
+
vi.mocked(runAuditWithFixes).mockResolvedValue({
|
|
168
|
+
report: {
|
|
169
|
+
url: 'https://example.com',
|
|
170
|
+
domain: 'example.com',
|
|
171
|
+
timestamp: '2024-01-15T10:00:00Z',
|
|
172
|
+
crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
|
|
173
|
+
healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
|
|
174
|
+
issues: [],
|
|
175
|
+
pages: [],
|
|
176
|
+
summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
|
|
177
|
+
},
|
|
178
|
+
fixes: [],
|
|
179
|
+
score: 80,
|
|
180
|
+
previousScore: 65,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const config: ScheduledAuditConfig = {
|
|
184
|
+
projectId: 'proj-1',
|
|
185
|
+
url: 'https://example.com',
|
|
186
|
+
schedule: 'daily',
|
|
187
|
+
previousScore: 65,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = await runScheduledAudit(config);
|
|
191
|
+
|
|
192
|
+
expect(result.scoreChange).toBe(15);
|
|
193
|
+
expect(runAuditWithFixes).toHaveBeenCalledWith(
|
|
194
|
+
expect.objectContaining({ previousScore: 65 })
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('creates PR when auto-fix is enabled and fixes are available', async () => {
|
|
199
|
+
const { runAuditWithFixes, createAuditPR } = await import('../audit/runner.js');
|
|
200
|
+
|
|
201
|
+
vi.mocked(runAuditWithFixes).mockResolvedValue({
|
|
202
|
+
report: {
|
|
203
|
+
url: 'https://example.com',
|
|
204
|
+
domain: 'example.com',
|
|
205
|
+
timestamp: '2024-01-15T10:00:00Z',
|
|
206
|
+
crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
|
|
207
|
+
healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 65, social: 60, localSeo: 60 },
|
|
208
|
+
issues: [{ code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: [] }],
|
|
209
|
+
pages: [],
|
|
210
|
+
summary: { errors: 1, warnings: 0, notices: 0, passed: 84 },
|
|
211
|
+
},
|
|
212
|
+
fixes: [{ category: 'on-page', issues: ['TITLE_MISSING'], files: [{ path: 'index.html', content: '<title>Fixed</title>' }] }],
|
|
213
|
+
score: 65,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
vi.mocked(createAuditPR).mockResolvedValue({
|
|
217
|
+
success: true,
|
|
218
|
+
prUrl: 'https://github.com/owner/repo/pull/99',
|
|
219
|
+
prNumber: 99,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const config: ScheduledAuditConfig = {
|
|
223
|
+
projectId: 'proj-1',
|
|
224
|
+
url: 'https://example.com',
|
|
225
|
+
schedule: 'daily',
|
|
226
|
+
autoFix: true,
|
|
227
|
+
baseBranch: 'main',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const result = await runScheduledAudit(config);
|
|
231
|
+
|
|
232
|
+
expect(result.success).toBe(true);
|
|
233
|
+
expect(result.prCreated).toBe(true);
|
|
234
|
+
expect(result.prUrl).toBe('https://github.com/owner/repo/pull/99');
|
|
235
|
+
expect(createAuditPR).toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('does not create PR when auto-fix is disabled', async () => {
|
|
239
|
+
const { runAuditWithFixes, createAuditPR } = await import('../audit/runner.js');
|
|
240
|
+
|
|
241
|
+
vi.mocked(runAuditWithFixes).mockResolvedValue({
|
|
242
|
+
report: {
|
|
243
|
+
url: 'https://example.com',
|
|
244
|
+
domain: 'example.com',
|
|
245
|
+
timestamp: '2024-01-15T10:00:00Z',
|
|
246
|
+
crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
|
|
247
|
+
healthScore: { overall: 65, crawlability: 80, indexability: 70, onPage: 50, content: 70, links: 60, performance: 75, security: 80, aiReadiness: 65, social: 60, localSeo: 60 },
|
|
248
|
+
issues: [{ code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: [] }],
|
|
249
|
+
pages: [],
|
|
250
|
+
summary: { errors: 1, warnings: 0, notices: 0, passed: 84 },
|
|
251
|
+
},
|
|
252
|
+
fixes: [{ category: 'on-page', issues: ['TITLE_MISSING'], files: [{ path: 'index.html', content: '<title>Fixed</title>' }] }],
|
|
253
|
+
score: 65,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const config: ScheduledAuditConfig = {
|
|
257
|
+
projectId: 'proj-1',
|
|
258
|
+
url: 'https://example.com',
|
|
259
|
+
schedule: 'daily',
|
|
260
|
+
autoFix: false,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const result = await runScheduledAudit(config);
|
|
264
|
+
|
|
265
|
+
expect(result.success).toBe(true);
|
|
266
|
+
expect(result.prCreated).toBeFalsy();
|
|
267
|
+
expect(createAuditPR).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('handles audit errors gracefully', async () => {
|
|
271
|
+
const { runAuditWithFixes } = await import('../audit/runner.js');
|
|
272
|
+
|
|
273
|
+
vi.mocked(runAuditWithFixes).mockRejectedValue(new Error('Network timeout'));
|
|
274
|
+
|
|
275
|
+
const config: ScheduledAuditConfig = {
|
|
276
|
+
projectId: 'proj-1',
|
|
277
|
+
url: 'https://example.com',
|
|
278
|
+
schedule: 'daily',
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const result = await runScheduledAudit(config);
|
|
282
|
+
|
|
283
|
+
expect(result.success).toBe(false);
|
|
284
|
+
expect(result.error).toContain('Network timeout');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('respects generateFixes option', async () => {
|
|
288
|
+
const { runAuditWithFixes } = await import('../audit/runner.js');
|
|
289
|
+
|
|
290
|
+
vi.mocked(runAuditWithFixes).mockResolvedValue({
|
|
291
|
+
report: {
|
|
292
|
+
url: 'https://example.com',
|
|
293
|
+
domain: 'example.com',
|
|
294
|
+
timestamp: '2024-01-15T10:00:00Z',
|
|
295
|
+
crawlStats: { totalUrls: 1, crawledUrls: 1, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
|
|
296
|
+
healthScore: { overall: 80, crawlability: 80, indexability: 80, onPage: 80, content: 80, links: 80, performance: 80, security: 80, aiReadiness: 80, social: 80, localSeo: 80 },
|
|
297
|
+
issues: [],
|
|
298
|
+
pages: [],
|
|
299
|
+
summary: { errors: 0, warnings: 0, notices: 0, passed: 85 },
|
|
300
|
+
},
|
|
301
|
+
fixes: [],
|
|
302
|
+
score: 80,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const config: ScheduledAuditConfig = {
|
|
306
|
+
projectId: 'proj-1',
|
|
307
|
+
url: 'https://example.com',
|
|
308
|
+
schedule: 'daily',
|
|
309
|
+
generateFixes: false,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
await runScheduledAudit(config);
|
|
313
|
+
|
|
314
|
+
expect(runAuditWithFixes).toHaveBeenCalledWith(
|
|
315
|
+
expect.objectContaining({ generateFixes: false })
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('integration scenarios', () => {
|
|
321
|
+
it('handles complete scheduled audit workflow', async () => {
|
|
322
|
+
const { runAuditWithFixes, createAuditPR } = await import('../audit/runner.js');
|
|
323
|
+
|
|
324
|
+
// Mock a typical audit result with issues
|
|
325
|
+
vi.mocked(runAuditWithFixes).mockResolvedValue({
|
|
326
|
+
report: {
|
|
327
|
+
url: 'https://myapp.com',
|
|
328
|
+
domain: 'myapp.com',
|
|
329
|
+
timestamp: '2024-01-15T10:00:00Z',
|
|
330
|
+
crawlStats: { totalUrls: 5, crawledUrls: 5, errorUrls: 0, redirectUrls: 0, blockedUrls: 0 },
|
|
331
|
+
healthScore: { overall: 58, crawlability: 70, indexability: 60, onPage: 45, content: 55, links: 60, performance: 65, security: 75, aiReadiness: 55, social: 50, localSeo: 50 },
|
|
332
|
+
issues: [
|
|
333
|
+
{ code: 'TITLE_MISSING', severity: 'error', category: 'on-page', title: 'Missing title', description: 'No title', impact: 'Critical ranking factor missing.', howToFix: 'Add a title tag.', affectedUrls: ['https://myapp.com/about'] },
|
|
334
|
+
{ code: 'META_DESC_MISSING', severity: 'error', category: 'on-page', title: 'Missing meta description', description: 'No meta', impact: 'Search results will show auto-generated snippets.', howToFix: 'Add a meta description.', affectedUrls: ['https://myapp.com/about'] },
|
|
335
|
+
{ code: 'OG_IMAGE_MISSING', severity: 'warning', category: 'social', title: 'Missing OG image', description: 'No og:image', impact: 'Social shares will not display a preview image.', howToFix: 'Add og:image meta tag.', affectedUrls: ['https://myapp.com'] },
|
|
336
|
+
],
|
|
337
|
+
pages: [],
|
|
338
|
+
summary: { errors: 2, warnings: 1, notices: 0, passed: 78 },
|
|
339
|
+
},
|
|
340
|
+
fixes: [
|
|
341
|
+
{ category: 'on-page', issues: ['TITLE_MISSING', 'META_DESC_MISSING'], files: [{ path: 'about.html', content: '<title>About</title>' }] },
|
|
342
|
+
{ category: 'social', issues: ['OG_IMAGE_MISSING'], files: [{ path: 'index.html', content: '<meta property="og:image">' }] },
|
|
343
|
+
],
|
|
344
|
+
score: 58,
|
|
345
|
+
previousScore: 45,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
vi.mocked(createAuditPR).mockResolvedValue({
|
|
349
|
+
success: true,
|
|
350
|
+
prUrl: 'https://github.com/owner/myapp/pull/123',
|
|
351
|
+
prNumber: 123,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const config: ScheduledAuditConfig = {
|
|
355
|
+
projectId: 'proj-myapp',
|
|
356
|
+
url: 'https://myapp.com',
|
|
357
|
+
schedule: 'daily',
|
|
358
|
+
autoFix: true,
|
|
359
|
+
baseBranch: 'main',
|
|
360
|
+
previousScore: 45,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const result = await runScheduledAudit(config);
|
|
364
|
+
|
|
365
|
+
expect(result).toMatchObject({
|
|
366
|
+
success: true,
|
|
367
|
+
projectId: 'proj-myapp',
|
|
368
|
+
score: 58,
|
|
369
|
+
scoreChange: 13,
|
|
370
|
+
issuesFound: 3,
|
|
371
|
+
fixesGenerated: 2,
|
|
372
|
+
prCreated: true,
|
|
373
|
+
prUrl: 'https://github.com/owner/myapp/pull/123',
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduled Audit Module
|
|
3
|
+
*
|
|
4
|
+
* Handles scheduled execution of SEO audits with optional auto-fix PR creation.
|
|
5
|
+
* Can be used from Edge Functions, CLI cron jobs, or other scheduling mechanisms.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { runAuditWithFixes, createAuditPR } from '../audit/runner.js';
|
|
9
|
+
import { generateBranchName } from '../git/pr-helper.js';
|
|
10
|
+
|
|
11
|
+
export type AuditSchedule = 'manual' | 'daily' | 'weekly' | 'on-demand';
|
|
12
|
+
|
|
13
|
+
export interface ScheduledAuditConfig {
|
|
14
|
+
projectId: string;
|
|
15
|
+
url: string;
|
|
16
|
+
schedule: AuditSchedule;
|
|
17
|
+
lastRunAt?: string;
|
|
18
|
+
previousScore?: number;
|
|
19
|
+
autoFix?: boolean;
|
|
20
|
+
baseBranch?: string;
|
|
21
|
+
generateFixes?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ScheduledAuditResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
projectId: string;
|
|
27
|
+
score?: number;
|
|
28
|
+
scoreChange?: number;
|
|
29
|
+
issuesFound?: number;
|
|
30
|
+
fixesGenerated?: number;
|
|
31
|
+
prCreated?: boolean;
|
|
32
|
+
prUrl?: string;
|
|
33
|
+
prNumber?: number;
|
|
34
|
+
error?: string;
|
|
35
|
+
completedAt: string;
|
|
36
|
+
nextRunAt?: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SCHEDULE_INTERVALS: Record<AuditSchedule, number | null> = {
|
|
40
|
+
manual: null,
|
|
41
|
+
daily: 24 * 60 * 60 * 1000, // 24 hours
|
|
42
|
+
weekly: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
43
|
+
'on-demand': null,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Determines if an audit should run based on schedule and last run time
|
|
48
|
+
*/
|
|
49
|
+
export function shouldRunAudit(config: ScheduledAuditConfig): boolean {
|
|
50
|
+
const { schedule, lastRunAt } = config;
|
|
51
|
+
|
|
52
|
+
// Manual schedule never auto-runs
|
|
53
|
+
if (schedule === 'manual') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// On-demand always runs when requested
|
|
58
|
+
if (schedule === 'on-demand') {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If never run before, should run
|
|
63
|
+
if (!lastRunAt) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const interval = SCHEDULE_INTERVALS[schedule];
|
|
68
|
+
if (!interval) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lastRun = new Date(lastRunAt).getTime();
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const elapsed = now - lastRun;
|
|
75
|
+
|
|
76
|
+
return elapsed >= interval;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Calculates the next scheduled run time
|
|
81
|
+
*/
|
|
82
|
+
export function calculateNextRun(schedule: AuditSchedule): string | null {
|
|
83
|
+
const interval = SCHEDULE_INTERVALS[schedule];
|
|
84
|
+
|
|
85
|
+
if (!interval) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const nextRun = new Date(Date.now() + interval);
|
|
90
|
+
return nextRun.toISOString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Runs a scheduled audit with optional auto-fix PR creation
|
|
95
|
+
*/
|
|
96
|
+
export async function runScheduledAudit(
|
|
97
|
+
config: ScheduledAuditConfig
|
|
98
|
+
): Promise<ScheduledAuditResult> {
|
|
99
|
+
const { projectId, url, previousScore, autoFix, baseBranch, generateFixes = true, schedule } = config;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Run the audit
|
|
103
|
+
const auditResult = await runAuditWithFixes({
|
|
104
|
+
url,
|
|
105
|
+
generateFixes,
|
|
106
|
+
previousScore,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result: ScheduledAuditResult = {
|
|
110
|
+
success: true,
|
|
111
|
+
projectId,
|
|
112
|
+
score: auditResult.score,
|
|
113
|
+
issuesFound: auditResult.report.issues.length,
|
|
114
|
+
fixesGenerated: auditResult.fixes.length,
|
|
115
|
+
completedAt: new Date().toISOString(),
|
|
116
|
+
nextRunAt: calculateNextRun(schedule),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Calculate score change if previous score available
|
|
120
|
+
if (previousScore !== undefined) {
|
|
121
|
+
result.scoreChange = auditResult.score - previousScore;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Create PR if auto-fix is enabled and fixes are available
|
|
125
|
+
if (autoFix && auditResult.fixes.length > 0 && baseBranch) {
|
|
126
|
+
const headBranch = generateBranchName('seo-fixes');
|
|
127
|
+
const prResult = await createAuditPR(auditResult, {
|
|
128
|
+
baseBranch,
|
|
129
|
+
headBranch,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (prResult.success) {
|
|
133
|
+
result.prCreated = true;
|
|
134
|
+
result.prUrl = prResult.prUrl;
|
|
135
|
+
result.prNumber = prResult.prNumber;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
projectId,
|
|
144
|
+
error: (error as Error).message,
|
|
145
|
+
completedAt: new Date().toISOString(),
|
|
146
|
+
nextRunAt: calculateNextRun(schedule),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|