From cd1a73024ae2951a551c7fca0d914403e1bac9ff Mon Sep 17 00:00:00 2001 From: dacharyc Date: Thu, 19 Mar 2026 21:10:56 -0400 Subject: [PATCH 1/3] redirect check should not match analytics scripts or code examples --- src/checks/url-stability/redirect-behavior.ts | 8 +- test/unit/checks/redirect-behavior.test.ts | 85 +++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/checks/url-stability/redirect-behavior.ts b/src/checks/url-stability/redirect-behavior.ts index ec13378..9b46e8f 100644 --- a/src/checks/url-stability/redirect-behavior.ts +++ b/src/checks/url-stability/redirect-behavior.ts @@ -11,7 +11,10 @@ interface RedirectResult { } const JS_REDIRECT_PATTERNS = - /window\.location\s*[=.]|document\.location\s*[=.]|location\.href\s*=|location\.replace\s*\(|]+http-equiv\s*=\s*["']?refresh["']?/i; + /(?:window|document)\.location\s*=(?!=)|location\.href\s*=(?!=)|location\.(?:replace|assign)\s*\(|]+http-equiv\s*=\s*["']?refresh["']?/i; + +/** Strip
 and  block contents so code examples don't trigger false positives. */
+const CODE_BLOCK_RE = /<(pre|code)\b[^>]*>[\s\S]*?<\/\1>/gi;
 
 async function check(ctx: CheckContext): Promise {
   const id = 'redirect-behavior';
@@ -36,7 +39,8 @@ async function check(ctx: CheckContext): Promise {
             // Check for JS-based redirects in the body
             try {
               const body = await response.text();
-              if (JS_REDIRECT_PATTERNS.test(body.slice(0, 10_000))) {
+              const sample = body.slice(0, 10_000).replace(CODE_BLOCK_RE, '');
+              if (JS_REDIRECT_PATTERNS.test(sample)) {
                 return { url, status, classification: 'js-redirect' };
               }
             } catch {
diff --git a/test/unit/checks/redirect-behavior.test.ts b/test/unit/checks/redirect-behavior.test.ts
index f602a69..56d6052 100644
--- a/test/unit/checks/redirect-behavior.test.ts
+++ b/test/unit/checks/redirect-behavior.test.ts
@@ -192,6 +192,91 @@ describe('redirect-behavior', () => {
     expect(result.details?.sameHostCount).toBe(1);
   });
 
+  it('ignores window.location inside  blocks', async () => {
+    server.use(
+      http.get(
+        'http://rb-code.local/docs/page1',
+        () =>
+          new HttpResponse(
+            '

Use window.location = "/page" to navigate.

', + { status: 200, headers: { 'Content-Type': 'text/html' } }, + ), + ), + ); + + const result = await check.run(makeCtx(llms('rb-code.local', '/docs/page1'))); + expect(result.status).toBe('pass'); + expect(result.details?.jsRedirectCount).toBe(0); + }); + + it('ignores window.location inside
 blocks', async () => {
+    server.use(
+      http.get(
+        'http://rb-pre.local/docs/page1',
+        () =>
+          new HttpResponse(
+            '
window.location.href = "/new";
', + { status: 200, headers: { 'Content-Type': 'text/html' } }, + ), + ), + ); + + const result = await check.run(makeCtx(llms('rb-pre.local', '/docs/page1'))); + expect(result.status).toBe('pass'); + expect(result.details?.jsRedirectCount).toBe(0); + }); + + it('ignores meta refresh inside
 blocks', async () => {
+    server.use(
+      http.get(
+        'http://rb-premeta.local/docs/page1',
+        () =>
+          new HttpResponse(
+            '
<meta http-equiv="refresh" content="0;url=/new">
', + { status: 200, headers: { 'Content-Type': 'text/html' } }, + ), + ), + ); + + const result = await check.run(makeCtx(llms('rb-premeta.local', '/docs/page1'))); + expect(result.status).toBe('pass'); + expect(result.details?.jsRedirectCount).toBe(0); + }); + + it('ignores window.location property reads in ', + { status: 200, headers: { 'Content-Type': 'text/html' } }, + ), + ), + ); + + const result = await check.run(makeCtx(llms('rb-read.local', '/docs/page1'))); + expect(result.status).toBe('pass'); + expect(result.details?.jsRedirectCount).toBe(0); + }); + + it('still detects real JS redirects in ', + { status: 200, headers: { 'Content-Type': 'text/html' } }, + ), + ), + ); + + const result = await check.run(makeCtx(llms('rb-script.local', '/docs/page1'))); + expect(result.status).toBe('fail'); + expect(result.details?.jsRedirectCount).toBe(1); + }); + it('classifies 302 redirects the same as 301', async () => { server.use( http.get( From 0b9746a71fc0ae1a44ae0c557e17fb917981142a Mon Sep 17 00:00:00 2001 From: dacharyc Date: Thu, 19 Mar 2026 21:30:45 -0400 Subject: [PATCH 2/3] Warn instead of fail on llms.txt cross-origin link failures --- .../llms-txt/llms-txt-links-markdown.ts | 178 +++++++++------- src/checks/llms-txt/llms-txt-links-resolve.ts | 194 +++++++++++++----- .../checks/llms-txt-links-markdown.test.ts | 87 +++++--- .../checks/llms-txt-links-resolve.test.ts | 106 +++++++--- 4 files changed, 389 insertions(+), 176 deletions(-) diff --git a/src/checks/llms-txt/llms-txt-links-markdown.ts b/src/checks/llms-txt/llms-txt-links-markdown.ts index 8b6f847..200993a 100644 --- a/src/checks/llms-txt/llms-txt-links-markdown.ts +++ b/src/checks/llms-txt/llms-txt-links-markdown.ts @@ -35,18 +35,30 @@ async function checkLlmsTxtLinksMarkdown(ctx: CheckContext): Promise(); + // Collect unique links and partition by origin + const siteOrigin = ctx.effectiveOrigin ?? ctx.origin; + const sameOriginLinks: string[] = []; + const crossOriginLinks: string[] = []; for (const file of discovered) { const links = extractMarkdownLinks(file.content); for (const link of links) { if (link.url.startsWith('http://') || link.url.startsWith('https://')) { - allLinks.add(link.url); + try { + const linkOrigin = new URL(link.url).origin; + if (linkOrigin === siteOrigin) { + if (!sameOriginLinks.includes(link.url)) sameOriginLinks.push(link.url); + } else { + if (!crossOriginLinks.includes(link.url)) crossOriginLinks.push(link.url); + } + } catch { + if (!sameOriginLinks.includes(link.url)) sameOriginLinks.push(link.url); + } } } } - if (allLinks.size === 0) { + const totalLinks = sameOriginLinks.length + crossOriginLinks.length; + if (totalLinks === 0) { return { id: 'llms-txt-links-markdown', category: 'llms-txt', @@ -55,87 +67,92 @@ async function checkLlmsTxtLinksMarkdown(ctx: CheckContext): Promise ctx.options.maxLinksToTest; + // Sample same-origin links if too many + let sameToTest = sameOriginLinks; + const wasSampled = sameOriginLinks.length > ctx.options.maxLinksToTest; if (wasSampled) { - for (let i = linksToTest.length - 1; i > 0; i--) { + for (let i = sameToTest.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); - [linksToTest[i], linksToTest[j]] = [linksToTest[j], linksToTest[i]]; + [sameToTest[i], sameToTest[j]] = [sameToTest[j], sameToTest[i]]; } - linksToTest = linksToTest.slice(0, ctx.options.maxLinksToTest); + sameToTest = sameToTest.slice(0, ctx.options.maxLinksToTest); } - const results: LinkMarkdownResult[] = []; - const concurrency = ctx.options.maxConcurrency; + async function checkMarkdown(urls: string[]): Promise { + const out: LinkMarkdownResult[] = []; + const concurrency = ctx.options.maxConcurrency; + for (let i = 0; i < urls.length; i += concurrency) { + const batch = urls.slice(i, i + concurrency); + const batchResults = await Promise.all( + batch.map(async (url): Promise => { + const hasMdExt = hasMarkdownExtension(url); + + if (hasMdExt) { + return { url, hasMarkdownExtension: true, servesMarkdown: true }; + } - for (let i = 0; i < linksToTest.length; i += concurrency) { - const batch = linksToTest.slice(i, i + concurrency); - const batchResults = await Promise.all( - batch.map(async (url): Promise => { - const hasMdExt = hasMarkdownExtension(url); + // Check if the URL serves markdown via content-type + try { + const response = await ctx.http.fetch(url, { + method: 'HEAD', + headers: { Accept: 'text/markdown' }, + }); + const contentType = response.headers.get('content-type') ?? ''; + if (contentType.includes('text/markdown')) { + return { + url, + hasMarkdownExtension: false, + servesMarkdown: true, + status: response.status, + }; + } - if (hasMdExt) { - return { url, hasMarkdownExtension: true, servesMarkdown: true }; - } + // Try .md variant candidates + const candidates = toMdUrls(url); + for (const mdUrl of candidates) { + try { + const mdResponse = await ctx.http.fetch(mdUrl, { method: 'HEAD' }); + if (mdResponse.ok) { + return { + url, + hasMarkdownExtension: false, + servesMarkdown: false, + status: response.status, + mdVariantAvailable: true, + }; + } + } catch { + // Try next candidate + } + } - // Check if the URL serves markdown via content-type - try { - const response = await ctx.http.fetch(url, { - method: 'HEAD', - headers: { Accept: 'text/markdown' }, - }); - const contentType = response.headers.get('content-type') ?? ''; - if (contentType.includes('text/markdown')) { return { url, hasMarkdownExtension: false, - servesMarkdown: true, + servesMarkdown: false, status: response.status, + mdVariantAvailable: false, + }; + } catch (err) { + return { + url, + hasMarkdownExtension: false, + servesMarkdown: false, + status: 0, + error: err instanceof Error ? err.message : String(err), }; } - - // Try .md variant candidates - const candidates = toMdUrls(url); - for (const mdUrl of candidates) { - try { - const mdResponse = await ctx.http.fetch(mdUrl, { method: 'HEAD' }); - if (mdResponse.ok) { - return { - url, - hasMarkdownExtension: false, - servesMarkdown: false, - status: response.status, - mdVariantAvailable: true, - }; - } - } catch { - // Try next candidate - } - } - - return { - url, - hasMarkdownExtension: false, - servesMarkdown: false, - status: response.status, - mdVariantAvailable: false, - }; - } catch (err) { - return { - url, - hasMarkdownExtension: false, - servesMarkdown: false, - status: 0, - error: err instanceof Error ? err.message : String(err), - }; - } - }), - ); - results.push(...batchResults); + }), + ); + out.push(...batchResults); + } + return out; } + // Only check same-origin links for markdown support (cross-origin links + // are outside the site owner's control and shouldn't affect the result) + const results = await checkMarkdown(sameToTest); + const markdownLinks = results.filter((r) => r.hasMarkdownExtension || r.servesMarkdown).length; const mdVariantsAvailable = results.filter((r) => r.mdVariantAvailable).length; const markdownRate = results.length > 0 ? markdownLinks / results.length : 0; @@ -147,6 +164,11 @@ async function checkLlmsTxtLinksMarkdown(ctx: CheckContext): Promise 0 ? `; ${fetchErrors} failed to fetch` : '') + (rateLimited > 0 ? `; ${rateLimited} rate-limited (HTTP 429)` : ''); + const crossNote = + crossOriginLinks.length > 0 + ? ` (${crossOriginLinks.length} external link${crossOriginLinks.length === 1 ? '' : 's'} excluded)` + : ''; + const details: Record = { totalLinks, testedLinks: results.length, @@ -157,14 +179,26 @@ async function checkLlmsTxtLinksMarkdown(ctx: CheckContext): Promise= 0.9) { return { id: 'llms-txt-links-markdown', category: 'llms-txt', status: 'pass', - message: `${markdownLinks}/${results.length} ${linkLabel} point to markdown content (${Math.round(markdownRate * 100)}%)${suffix}`, + message: `${markdownLinks}/${results.length} same-origin ${linkLabel} point to markdown content (${Math.round(markdownRate * 100)}%)${suffix}${crossNote}`, details, }; } @@ -174,7 +208,7 @@ async function checkLlmsTxtLinksMarkdown(ctx: CheckContext): Promise }; } - // Sample links if there are too many - let linksToTest = Array.from(allLinks.keys()); - const totalLinks = linksToTest.length; - const wasSampled = totalLinks > ctx.options.maxLinksToTest; + // Partition links into same-origin and cross-origin + const siteOrigin = ctx.effectiveOrigin ?? ctx.origin; + const sameOriginLinks: string[] = []; + const crossOriginLinks: string[] = []; + for (const url of allLinks.keys()) { + try { + const linkOrigin = new URL(url).origin; + if (linkOrigin === siteOrigin) { + sameOriginLinks.push(url); + } else { + crossOriginLinks.push(url); + } + } catch { + sameOriginLinks.push(url); // treat unparseable URLs as same-origin so they get checked + } + } + + // Sample same-origin links if there are too many + let sameToTest = sameOriginLinks; + const totalSameOrigin = sameOriginLinks.length; + const wasSampled = totalSameOrigin > ctx.options.maxLinksToTest; if (wasSampled) { - // Shuffle and take a sample - for (let i = linksToTest.length - 1; i > 0; i--) { + for (let i = sameToTest.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [sameToTest[i], sameToTest[j]] = [sameToTest[j], sameToTest[i]]; + } + sameToTest = sameToTest.slice(0, ctx.options.maxLinksToTest); + } + + // Sample cross-origin links separately + let crossToTest = crossOriginLinks; + const totalCrossOrigin = crossOriginLinks.length; + if (totalCrossOrigin > ctx.options.maxLinksToTest) { + for (let i = crossToTest.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); - [linksToTest[i], linksToTest[j]] = [linksToTest[j], linksToTest[i]]; + [crossToTest[i], crossToTest[j]] = [crossToTest[j], crossToTest[i]]; } - linksToTest = linksToTest.slice(0, ctx.options.maxLinksToTest); + crossToTest = crossToTest.slice(0, ctx.options.maxLinksToTest); } // Check links with bounded concurrency - const results: LinkCheckResult[] = []; - const concurrency = ctx.options.maxConcurrency; - - for (let i = 0; i < linksToTest.length; i += concurrency) { - const batch = linksToTest.slice(i, i + concurrency); - const batchResults = await Promise.all( - batch.map(async (url): Promise => { - try { - const response = await ctx.http.fetch(url, { method: 'HEAD' }); - // Some servers don't support HEAD; fall back to GET - if (response.status === 405) { - const getResponse = await ctx.http.fetch(url); - return { url, status: getResponse.status, ok: getResponse.ok }; + async function checkLinks(urls: string[]): Promise { + const out: LinkCheckResult[] = []; + const concurrency = ctx.options.maxConcurrency; + for (let i = 0; i < urls.length; i += concurrency) { + const batch = urls.slice(i, i + concurrency); + const batchResults = await Promise.all( + batch.map(async (url): Promise => { + try { + const response = await ctx.http.fetch(url, { method: 'HEAD' }); + // Some servers don't support HEAD; fall back to GET + if (response.status === 405) { + const getResponse = await ctx.http.fetch(url); + return { url, status: getResponse.status, ok: getResponse.ok }; + } + return { url, status: response.status, ok: response.ok }; + } catch (err) { + return { + url, + status: 0, + ok: false, + error: err instanceof Error ? err.message : String(err), + }; } - return { url, status: response.status, ok: response.ok }; - } catch (err) { - return { - url, - status: 0, - ok: false, - error: err instanceof Error ? err.message : String(err), - }; - } - }), - ); - results.push(...batchResults); + }), + ); + out.push(...batchResults); + } + return out; } - const resolved = results.filter((r) => r.ok).length; - const broken = results.filter((r) => !r.ok); - const resolveRate = resolved / results.length; - const fetchErrors = results.filter((r) => r.error).length; - const rateLimited = results.filter((r) => r.status === 429).length; + const sameResults = await checkLinks(sameToTest); + const crossResults = await checkLinks(crossToTest); + + // Same-origin stats (drive pass/fail) + const sameResolved = sameResults.filter((r) => r.ok).length; + const sameBroken = sameResults.filter((r) => !r.ok); + const sameResolveRate = sameResults.length > 0 ? sameResolved / sameResults.length : 1; + const sameFetchErrors = sameResults.filter((r) => r.error).length; + const sameRateLimited = sameResults.filter((r) => r.status === 429).length; + // Cross-origin stats (reported but don't affect pass/fail) + const crossResolved = crossResults.filter((r) => r.ok).length; + const crossBroken = crossResults.filter((r) => !r.ok); + const crossFetchErrors = crossResults.filter((r) => r.error).length; + const crossRateLimited = crossResults.filter((r) => r.status === 429).length; + + const totalLinks = allLinks.size; const linkLabel = wasSampled ? 'sampled links' : 'links'; - const suffix = - (fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '') + - (rateLimited > 0 ? `; ${rateLimited} rate-limited (HTTP 429)` : ''); + const sameSuffix = + (sameFetchErrors > 0 ? `; ${sameFetchErrors} failed to fetch` : '') + + (sameRateLimited > 0 ? `; ${sameRateLimited} rate-limited (HTTP 429)` : ''); + + const crossNote = + crossBroken.length > 0 + ? ` (${crossBroken.length} external link${crossBroken.length === 1 ? '' : 's'} also failed; may be bot-detection or rate-limiting)` + : ''; const details: Record = { totalLinks, - testedLinks: results.length, + sameOrigin: { + total: totalSameOrigin, + tested: sameResults.length, + sampled: wasSampled, + resolved: sameResolved, + broken: sameBroken.map((b) => ({ url: b.url, status: b.status, error: b.error })), + resolveRate: Math.round(sameResolveRate * 100), + fetchErrors: sameFetchErrors, + rateLimited: sameRateLimited, + }, + crossOrigin: { + total: totalCrossOrigin, + tested: crossResults.length, + resolved: crossResolved, + broken: crossBroken.map((b) => ({ url: b.url, status: b.status, error: b.error })), + fetchErrors: crossFetchErrors, + rateLimited: crossRateLimited, + }, + // Flat fields kept for backward compatibility + testedLinks: sameResults.length + crossResults.length, sampled: wasSampled, - resolved, - broken: broken.map((b) => ({ url: b.url, status: b.status, error: b.error })), - resolveRate: Math.round(resolveRate * 100), - fetchErrors, - rateLimited, + resolved: sameResolved + crossResolved, + broken: [...sameBroken, ...crossBroken].map((b) => ({ + url: b.url, + status: b.status, + error: b.error, + })), + resolveRate: Math.round(sameResolveRate * 100), + fetchErrors: sameFetchErrors + crossFetchErrors, + rateLimited: sameRateLimited + crossRateLimited, }; - if (resolveRate === 1) { + if (sameResults.length === 0) { + // Only cross-origin links exist; report their status as a warning at most + const allResolved = crossBroken.length === 0; + return { + id: 'llms-txt-links-resolve', + category: 'llms-txt', + status: allResolved ? 'pass' : 'warn', + message: allResolved + ? `All ${crossResults.length} links are external and resolve (${totalLinks} total links)` + : `All links are external; ${crossResolved}/${crossResults.length} resolve (${crossBroken.length} failed; may be bot-detection or rate-limiting)`, + details, + }; + } + + if (sameResolveRate === 1) { return { id: 'llms-txt-links-resolve', category: 'llms-txt', - status: 'pass', - message: `All ${results.length} tested ${linkLabel} resolve (${totalLinks} total links)${suffix}`, + status: crossBroken.length > 0 ? 'warn' : 'pass', + message: + `All ${sameResults.length} same-origin ${linkLabel} resolve (${totalLinks} total links)${sameSuffix}` + + crossNote, details, }; } - if (resolveRate > LINK_RESOLVE_THRESHOLD) { + if (sameResolveRate > LINK_RESOLVE_THRESHOLD) { return { id: 'llms-txt-links-resolve', category: 'llms-txt', status: 'warn', - message: `${resolved}/${results.length} ${linkLabel} resolve (${Math.round(resolveRate * 100)}%); ${broken.length} broken${suffix}`, + message: + `${sameResolved}/${sameResults.length} same-origin ${linkLabel} resolve (${Math.round(sameResolveRate * 100)}%); ${sameBroken.length} broken${sameSuffix}` + + crossNote, details, }; } @@ -132,7 +218,9 @@ async function checkLlmsTxtLinksResolve(ctx: CheckContext): Promise id: 'llms-txt-links-resolve', category: 'llms-txt', status: 'fail', - message: `Only ${resolved}/${results.length} ${linkLabel} resolve (${Math.round(resolveRate * 100)}%); ${broken.length} broken${suffix}`, + message: + `Only ${sameResolved}/${sameResults.length} same-origin ${linkLabel} resolve (${Math.round(sameResolveRate * 100)}%); ${sameBroken.length} broken${sameSuffix}` + + crossNote, details, }; } diff --git a/test/unit/checks/llms-txt-links-markdown.test.ts b/test/unit/checks/llms-txt-links-markdown.test.ts index 37eac0f..3367bf7 100644 --- a/test/unit/checks/llms-txt-links-markdown.test.ts +++ b/test/unit/checks/llms-txt-links-markdown.test.ts @@ -60,41 +60,41 @@ Just text, no links here. const content = `# Test > Summary ## Links -- [Page 1](http://md.local/page1.md): First -- [Page 2](http://md.local/page2.md): Second +- [Page 1](http://test.local/page1.md): First +- [Page 2](http://test.local/page2.md): Second `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.markdownRate).toBe(100); }); - it('fails when links are HTML with no markdown alternatives', async () => { + it('fails when same-origin links are HTML with no markdown alternatives', async () => { server.use( http.head( - 'http://html.local/page1', + 'http://test.local/page1', () => new HttpResponse(null, { status: 200, headers: { 'content-type': 'text/html' }, }), ), - http.head('http://html.local/page1.md', () => new HttpResponse(null, { status: 404 })), + http.head('http://test.local/page1.md', () => new HttpResponse(null, { status: 404 })), http.head( - 'http://html.local/page2', + 'http://test.local/page2', () => new HttpResponse(null, { status: 200, headers: { 'content-type': 'text/html' }, }), ), - http.head('http://html.local/page2.md', () => new HttpResponse(null, { status: 404 })), + http.head('http://test.local/page2.md', () => new HttpResponse(null, { status: 404 })), ); const content = `# Test > Summary ## Links -- [Page 1](http://html.local/page1): First -- [Page 2](http://html.local/page2): Second +- [Page 1](http://test.local/page1): First +- [Page 2](http://test.local/page2): Second `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); @@ -103,43 +103,84 @@ Just text, no links here. it('warns when .md variants are available', async () => { server.use( http.head( - 'http://variant.local/page1', + 'http://test.local/page1', () => new HttpResponse(null, { status: 200, headers: { 'content-type': 'text/html' }, }), ), - http.head('http://variant.local/page1.md', () => new HttpResponse(null, { status: 200 })), + http.head('http://test.local/page1.md', () => new HttpResponse(null, { status: 200 })), ); const content = `# Test > Summary ## Links -- [Page 1](http://variant.local/page1): First +- [Page 1](http://test.local/page1): First `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('warn'); }); + it('excludes cross-origin links from markdown assessment', async () => { + server.use( + http.head( + 'http://test.local/page1', + () => + new HttpResponse(null, { + status: 200, + headers: { 'content-type': 'text/markdown' }, + }), + ), + // External link serves HTML, but should not affect the result + http.head( + 'http://external.example/page', + () => + new HttpResponse(null, { + status: 200, + headers: { 'content-type': 'text/html' }, + }), + ), + ); + + const content = `# Test +> Summary +## Links +- [Page 1](http://test.local/page1): First +- [External](http://external.example/page): External +`; + const result = await check.run(makeCtx(content)); + expect(result.status).toBe('pass'); + expect(result.details?.crossOriginExcluded).toBe(1); + }); + + it('skips when all links are cross-origin', async () => { + const content = `# Test +> Summary +## Links +- [External](http://external.example/page): External +`; + const result = await check.run(makeCtx(content)); + expect(result.status).toBe('skip'); + expect(result.message).toContain('external'); + }); + it('reports fetch errors in details and message', async () => { server.use( - http.head('http://err-md.local/page1', () => HttpResponse.error()), - http.get('http://err-md.local/page1', () => HttpResponse.error()), + http.head('http://test.local/page1', () => HttpResponse.error()), + http.get('http://test.local/page1', () => HttpResponse.error()), ); - const content = `# Test\n> Summary\n## Links\n- [Page 1](http://err-md.local/page1): First\n`; + const content = `# Test\n> Summary\n## Links\n- [Page 1](http://test.local/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.details?.fetchErrors).toBe(1); expect(result.message).toContain('failed to fetch'); }); it('reports rate-limited results (HTTP 429)', async () => { - server.use( - http.head('http://rl-md.local/page1', () => new HttpResponse(null, { status: 429 })), - ); + server.use(http.head('http://test.local/page1', () => new HttpResponse(null, { status: 429 }))); - const content = `# Test\n> Summary\n## Links\n- [Page 1](http://rl-md.local/page1): First\n`; + const content = `# Test\n> Summary\n## Links\n- [Page 1](http://test.local/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.details?.rateLimited).toBe(1); expect(result.message).toContain('rate-limited (HTTP 429)'); @@ -148,7 +189,7 @@ Just text, no links here. it('includes "sampled" in message when results are sampled', async () => { const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://sample-md.local/page${i}.md): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/page${i}.md): Page ${i}`, ).join('\n'); const content = `# Test\n> Summary\n## Links\n${links}\n`; @@ -172,7 +213,7 @@ Just text, no links here. it('uses toMdUrls to find .md variants (handles trailing slash and .html)', async () => { server.use( http.head( - 'http://tomd.local/guide.html', + 'http://test.local/guide.html', () => new HttpResponse(null, { status: 200, @@ -180,10 +221,10 @@ Just text, no links here. }), ), // toMdUrls should produce /guide.md (stripping .html) - http.head('http://tomd.local/guide.md', () => new HttpResponse(null, { status: 200 })), + http.head('http://test.local/guide.md', () => new HttpResponse(null, { status: 200 })), ); - const content = `# Test\n> Summary\n## Links\n- [Guide](http://tomd.local/guide.html): Guide\n`; + const content = `# Test\n> Summary\n## Links\n- [Guide](http://test.local/guide.html): Guide\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('warn'); expect(result.details?.mdVariantsAvailable).toBe(1); diff --git a/test/unit/checks/llms-txt-links-resolve.test.ts b/test/unit/checks/llms-txt-links-resolve.test.ts index 78eef28..8cf0e9b 100644 --- a/test/unit/checks/llms-txt-links-resolve.test.ts +++ b/test/unit/checks/llms-txt-links-resolve.test.ts @@ -47,29 +47,28 @@ describe('llms-txt-links-resolve', () => { it('falls back to GET when HEAD returns 405', async () => { server.use( - http.head('http://head405.local/page1', () => new HttpResponse(null, { status: 405 })), - http.get('http://head405.local/page1', () => new HttpResponse('OK', { status: 200 })), + http.head('http://test.local/page1', () => new HttpResponse(null, { status: 405 })), + http.get('http://test.local/page1', () => new HttpResponse('OK', { status: 200 })), ); - const content = `# Test\n> Summary\n## Links\n- [Page 1](http://head405.local/page1): First\n`; + const content = `# Test\n> Summary\n## Links\n- [Page 1](http://test.local/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.resolved).toBe(1); }); it('warns when resolve rate is above threshold but not 100%', async () => { - // 9 resolve, 1 broken = 90% resolve rate, which is exactly at LINK_RESOLVE_THRESHOLD (0.9) - // Need > 0.9 for warn, so use 10 resolve + 1 broken = ~91% + // 10 resolve, 1 broken = ~91% resolve rate (> 0.9 threshold → warn) const urls: string[] = []; for (let i = 0; i < 10; i++) { - urls.push(`http://warn-rl.local/page${i}`); + urls.push(`http://test.local/page${i}`); server.use( - http.head(`http://warn-rl.local/page${i}`, () => new HttpResponse(null, { status: 200 })), + http.head(`http://test.local/page${i}`, () => new HttpResponse(null, { status: 200 })), ); } - urls.push('http://warn-rl.local/broken'); + urls.push('http://test.local/broken'); server.use( - http.head('http://warn-rl.local/broken', () => new HttpResponse(null, { status: 404 })), + http.head('http://test.local/broken', () => new HttpResponse(null, { status: 404 })), ); const links = urls.map((u, i) => `- [Page ${i}](${u}): Page ${i}`).join('\n'); @@ -79,40 +78,91 @@ describe('llms-txt-links-resolve', () => { expect(result.details?.broken).toHaveLength(1); }); - it('passes when all links resolve', async () => { + it('passes when all same-origin links resolve', async () => { server.use( - http.head('http://links.local/page1', () => new HttpResponse(null, { status: 200 })), - http.head('http://links.local/page2', () => new HttpResponse(null, { status: 200 })), + http.head('http://test.local/page1', () => new HttpResponse(null, { status: 200 })), + http.head('http://test.local/page2', () => new HttpResponse(null, { status: 200 })), ); const content = `# Test > Summary ## Links -- [Page 1](http://links.local/page1): First -- [Page 2](http://links.local/page2): Second +- [Page 1](http://test.local/page1): First +- [Page 2](http://test.local/page2): Second `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); }); - it('fails when most links are broken', async () => { + it('fails when most same-origin links are broken', async () => { server.use( - http.head('http://broken.local/page1', () => new HttpResponse(null, { status: 404 })), - http.head('http://broken.local/page2', () => new HttpResponse(null, { status: 404 })), - http.head('http://broken.local/page3', () => new HttpResponse(null, { status: 200 })), + http.head('http://test.local/page1', () => new HttpResponse(null, { status: 404 })), + http.head('http://test.local/page2', () => new HttpResponse(null, { status: 404 })), + http.head('http://test.local/page3', () => new HttpResponse(null, { status: 200 })), ); const content = `# Test > Summary ## Links -- [Page 1](http://broken.local/page1): First -- [Page 2](http://broken.local/page2): Second -- [Page 3](http://broken.local/page3): Third +- [Page 1](http://test.local/page1): First +- [Page 2](http://test.local/page2): Second +- [Page 3](http://test.local/page3): Third `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); }); + it('does not fail when only cross-origin links are broken', async () => { + server.use( + http.head('http://test.local/page1', () => new HttpResponse(null, { status: 200 })), + http.head('http://external.example/pkg', () => new HttpResponse(null, { status: 403 })), + ); + + const content = `# Test +> Summary +## Links +- [Page 1](http://test.local/page1): First +- [Package](http://external.example/pkg): External +`; + const result = await check.run(makeCtx(content)); + // Same-origin link passes, so overall should warn (not fail) due to external failure + expect(result.status).toBe('warn'); + expect(result.message).toContain('same-origin'); + expect(result.message).toContain('external'); + }); + + it('passes when all same-origin resolve and no cross-origin issues', async () => { + server.use( + http.head('http://test.local/page1', () => new HttpResponse(null, { status: 200 })), + http.head('http://external.example/ok', () => new HttpResponse(null, { status: 200 })), + ); + + const content = `# Test +> Summary +## Links +- [Page 1](http://test.local/page1): First +- [External](http://external.example/ok): External +`; + const result = await check.run(makeCtx(content)); + expect(result.status).toBe('pass'); + }); + + it('warns (not fails) when only cross-origin links exist and some fail', async () => { + server.use( + http.head('http://external.example/ok', () => new HttpResponse(null, { status: 200 })), + http.head('http://other.example/blocked', () => new HttpResponse(null, { status: 403 })), + ); + + const content = `# Test +> Summary +## Links +- [External](http://external.example/ok): OK +- [Blocked](http://other.example/blocked): Blocked +`; + const result = await check.run(makeCtx(content)); + expect(result.status).toBe('warn'); + }); + it('skips when no HTTP links present', async () => { const content = `# Test > Summary @@ -125,20 +175,20 @@ Just text, no links. it('reports fetch errors in details', async () => { server.use( - http.head('http://err.local/page1', () => HttpResponse.error()), - http.get('http://err.local/page1', () => HttpResponse.error()), + http.head('http://test.local/page1', () => HttpResponse.error()), + http.get('http://test.local/page1', () => HttpResponse.error()), ); - const content = `# Test\n> Summary\n## Links\n- [Page 1](http://err.local/page1): First\n`; + const content = `# Test\n> Summary\n## Links\n- [Page 1](http://test.local/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.details?.fetchErrors).toBe(1); expect(result.message).toContain('failed to fetch'); }); it('reports rate-limited results (HTTP 429)', async () => { - server.use(http.head('http://rl.local/page1', () => new HttpResponse(null, { status: 429 }))); + server.use(http.head('http://test.local/page1', () => new HttpResponse(null, { status: 429 }))); - const content = `# Test\n> Summary\n## Links\n- [Page 1](http://rl.local/page1): First\n`; + const content = `# Test\n> Summary\n## Links\n- [Page 1](http://test.local/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.details?.rateLimited).toBe(1); expect(result.message).toContain('rate-limited (HTTP 429)'); @@ -147,12 +197,12 @@ Just text, no links. it('includes "sampled" in message when results are sampled', async () => { const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://sample-rl.local/page${i}): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/page${i}): Page ${i}`, ).join('\n'); for (let i = 0; i < 5; i++) { server.use( - http.head(`http://sample-rl.local/page${i}`, () => new HttpResponse(null, { status: 200 })), + http.head(`http://test.local/page${i}`, () => new HttpResponse(null, { status: 200 })), ); } From 85c89ce7a7dea57aa7276a21521b07e1991d870b Mon Sep 17 00:00:00 2001 From: dacharyc Date: Thu, 19 Mar 2026 22:29:41 -0400 Subject: [PATCH 3/3] Flow through skip cross-origin urls to remaining checks --- src/helpers/get-page-urls.ts | 8 +- test/unit/checks/auth-gate-detection.test.ts | 62 ++++++------- test/unit/checks/content-negotiation.test.ts | 54 ++++++------ .../checks/content-start-position.test.ts | 82 ++++++++--------- test/unit/checks/http-status-codes.test.ts | 61 ++++++------- test/unit/checks/llms-txt-directive.test.ts | 60 ++++++------- test/unit/checks/markdown-url-support.test.ts | 88 +++++++++---------- test/unit/checks/page-size-html.test.ts | 44 +++++----- test/unit/checks/redirect-behavior.test.ts | 70 +++++++-------- .../tabbed-content-serialization.test.ts | 62 ++++++------- 10 files changed, 293 insertions(+), 298 deletions(-) diff --git a/src/helpers/get-page-urls.ts b/src/helpers/get-page-urls.ts index 7dfe00c..5b1cfe5 100644 --- a/src/helpers/get-page-urls.ts +++ b/src/helpers/get-page-urls.ts @@ -77,16 +77,20 @@ async function walkAggregateLinks(ctx: CheckContext, urls: string[]): Promise { it('passes when all pages are accessible', async () => { server.use( http.get( - 'http://agd-pass.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('

Docs

Content here.

', { status: 200, @@ -60,7 +60,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-pass.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.accessible).toBe(1); @@ -69,12 +69,12 @@ describe('auth-gate-detection', () => { it('fails when page returns 401', async () => { server.use( http.get( - 'http://agd-401.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('Unauthorized', { status: 401 }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-401.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.authRequired).toBe(1); @@ -83,12 +83,12 @@ describe('auth-gate-detection', () => { it('fails when page returns 403', async () => { server.use( http.get( - 'http://agd-403.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('Forbidden', { status: 403 }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-403.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.authRequired).toBe(1); @@ -97,7 +97,7 @@ describe('auth-gate-detection', () => { it('warns when some pages are gated and some accessible', async () => { server.use( http.get( - 'http://agd-mix.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('

Docs

', { status: 200, @@ -105,12 +105,12 @@ describe('auth-gate-detection', () => { }), ), http.get( - 'http://agd-mix.local/docs/page2', + 'http://test.local/docs/page2', () => new HttpResponse('Unauthorized', { status: 401 }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-mix.local/docs/page1): First\n- [Page 2](http://agd-mix.local/docs/page2): Second\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n- [Page 2](http://test.local/docs/page2): Second\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('warn'); expect(result.details?.accessible).toBe(1); @@ -120,7 +120,7 @@ describe('auth-gate-detection', () => { it('detects SSO redirect to known domain', async () => { server.use( http.get( - 'http://agd-sso.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(null, { status: 302, @@ -131,7 +131,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-sso.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.authRedirect).toBe(1); @@ -141,7 +141,7 @@ describe('auth-gate-detection', () => { it('detects login form (password field)', async () => { server.use( http.get( - 'http://agd-form.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( '
', @@ -150,7 +150,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-form.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.softAuthGate).toBe(1); @@ -159,7 +159,7 @@ describe('auth-gate-detection', () => { it('detects login form via page title', async () => { server.use( http.get( - 'http://agd-title.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( 'Sign In - Company Portal
Please authenticate
', @@ -168,7 +168,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-title.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.softAuthGate).toBe(1); @@ -177,7 +177,7 @@ describe('auth-gate-detection', () => { it('detects login form via title with separator pattern', async () => { server.use( http.get( - 'http://agd-titlesep.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( 'Company Portal | Log In
Welcome
', @@ -186,7 +186,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-titlesep.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.softAuthGate).toBe(1); @@ -195,7 +195,7 @@ describe('auth-gate-detection', () => { it('does not flag pages that mention login as a topic', async () => { server.use( http.get( - 'http://agd-notlogin.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( 'the user is unable to login

Troubleshooting

Steps to fix login issues.

', @@ -204,7 +204,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-notlogin.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.accessible).toBe(1); @@ -213,16 +213,16 @@ describe('auth-gate-detection', () => { it('treats non-SSO redirects as accessible', async () => { server.use( http.get( - 'http://agd-noredir.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(null, { status: 301, - headers: { Location: 'http://agd-noredir.local/docs/page1-new' }, + headers: { Location: 'http://test.local/docs/page1-new' }, }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-noredir.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.accessible).toBe(1); @@ -231,7 +231,7 @@ describe('auth-gate-detection', () => { it('resolves relative Location headers in SSO redirects', async () => { server.use( http.get( - 'http://agd-relredir.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(null, { status: 302, @@ -240,7 +240,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-relredir.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); // login.* prefix matches SSO_DOMAINS expect(result.status).toBe('pass'); @@ -250,12 +250,12 @@ describe('auth-gate-detection', () => { it('treats other status codes (e.g. 500) as accessible', async () => { server.use( http.get( - 'http://agd-500.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('Internal Server Error', { status: 500 }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-500.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.accessible).toBe(1); @@ -263,11 +263,11 @@ describe('auth-gate-detection', () => { it('fails when all fetches error out', async () => { server.use( - http.get('http://agd-allfail.local/docs/page1', () => HttpResponse.error()), - http.get('http://agd-allfail.local/docs/page2', () => HttpResponse.error()), + http.get('http://test.local/docs/page1', () => HttpResponse.error()), + http.get('http://test.local/docs/page2', () => HttpResponse.error()), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-allfail.local/docs/page1): First\n- [Page 2](http://agd-allfail.local/docs/page2): Second\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n- [Page 2](http://test.local/docs/page2): Second\n`; const result = await check.run(makeCtx(content)); // All results are errors with classification 'accessible', so tested.length > 0 but no gated expect(result.details?.fetchErrors).toBe(2); @@ -276,7 +276,7 @@ describe('auth-gate-detection', () => { it('detects SSO form action as soft auth gate', async () => { server.use( http.get( - 'http://agd-ssoform.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( '
', @@ -285,7 +285,7 @@ describe('auth-gate-detection', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://agd-ssoform.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.softAuthGate).toBe(1); diff --git a/test/unit/checks/content-negotiation.test.ts b/test/unit/checks/content-negotiation.test.ts index a2cb2a7..225264a 100644 --- a/test/unit/checks/content-negotiation.test.ts +++ b/test/unit/checks/content-negotiation.test.ts @@ -46,7 +46,7 @@ describe('content-negotiation', () => { it('passes when server returns markdown with correct Content-Type', async () => { server.use( http.get( - 'http://cn-pass.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('# Page 1\n\nContent here.', { status: 200, @@ -54,7 +54,7 @@ describe('content-negotiation', () => { }), ), http.get( - 'http://cn-pass.local/docs/page2', + 'http://test.local/docs/page2', () => new HttpResponse('# Page 2\n\n[Link](http://example.com)', { status: 200, @@ -66,8 +66,8 @@ describe('content-negotiation', () => { const content = `# Docs > Summary ## Links -- [Page 1](http://cn-pass.local/docs/page1): First -- [Page 2](http://cn-pass.local/docs/page2): Second +- [Page 1](http://test.local/docs/page1): First +- [Page 2](http://test.local/docs/page2): Second `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); @@ -77,7 +77,7 @@ describe('content-negotiation', () => { it('warns when server returns markdown but with wrong Content-Type', async () => { server.use( http.get( - 'http://cn-wrong.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('# Page 1\n\nMarkdown content [link](http://example.com)', { status: 200, @@ -89,7 +89,7 @@ describe('content-negotiation', () => { const content = `# Docs > Summary ## Links -- [Page 1](http://cn-wrong.local/docs/page1): First +- [Page 1](http://test.local/docs/page1): First `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('warn'); @@ -99,7 +99,7 @@ describe('content-negotiation', () => { it('fails when server returns HTML regardless of Accept header', async () => { server.use( http.get( - 'http://cn-fail.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('

Page 1

', { status: 200, @@ -107,7 +107,7 @@ describe('content-negotiation', () => { }), ), http.get( - 'http://cn-fail.local/docs/page2', + 'http://test.local/docs/page2', () => new HttpResponse('Page 2', { status: 200, @@ -119,8 +119,8 @@ describe('content-negotiation', () => { const content = `# Docs > Summary ## Links -- [Page 1](http://cn-fail.local/docs/page1): First -- [Page 2](http://cn-fail.local/docs/page2): Second +- [Page 1](http://test.local/docs/page1): First +- [Page 2](http://test.local/docs/page2): Second `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); @@ -130,7 +130,7 @@ describe('content-negotiation', () => { it('handles mixed results across pages', async () => { server.use( http.get( - 'http://cn-mixed.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('# Page 1\n\nGood markdown.', { status: 200, @@ -138,7 +138,7 @@ describe('content-negotiation', () => { }), ), http.get( - 'http://cn-mixed.local/docs/page2', + 'http://test.local/docs/page2', () => new HttpResponse('HTML page', { status: 200, @@ -150,8 +150,8 @@ describe('content-negotiation', () => { const content = `# Docs > Summary ## Links -- [Page 1](http://cn-mixed.local/docs/page1): First -- [Page 2](http://cn-mixed.local/docs/page2): Second +- [Page 1](http://test.local/docs/page1): First +- [Page 2](http://test.local/docs/page2): Second `; const result = await check.run(makeCtx(content)); expect(result.status).toBe('warn'); @@ -162,13 +162,13 @@ describe('content-negotiation', () => { it('samples when more links than maxLinksToTest', async () => { const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://cn-sample.local/docs/page${i}): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/docs/page${i}): Page ${i}`, ).join('\n'); for (let i = 0; i < 5; i++) { server.use( http.get( - `http://cn-sample.local/docs/page${i}`, + `http://test.local/docs/page${i}`, () => new HttpResponse(`# Page ${i}\n\nContent`, { status: 200, @@ -198,9 +198,9 @@ describe('content-negotiation', () => { }); it('handles fetch errors gracefully and reports error field', async () => { - server.use(http.get('http://cn-err.local/docs/page', () => HttpResponse.error())); + server.use(http.get('http://test.local/docs/page', () => HttpResponse.error())); - const content = `# Docs\n> Summary\n## Links\n- [Page](http://cn-err.local/docs/page): A page\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page](http://test.local/docs/page): A page\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); const pageResults = result.details?.pageResults as Array<{ @@ -218,12 +218,12 @@ describe('content-negotiation', () => { it('reports rate-limited results (HTTP 429)', async () => { server.use( http.get( - 'http://cn-429.local/docs/page', + 'http://test.local/docs/page', () => new HttpResponse('Too Many Requests', { status: 429 }), ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page](http://cn-429.local/docs/page): A page\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page](http://test.local/docs/page): A page\n`; const result = await check.run(makeCtx(content)); expect(result.details?.rateLimited).toBe(1); expect(result.message).toContain('rate-limited (HTTP 429)'); @@ -232,13 +232,13 @@ describe('content-negotiation', () => { it('includes "sampled" in message when results are sampled', async () => { const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://cn-sampled.local/docs/page${i}): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/docs/page${i}): Page ${i}`, ).join('\n'); for (let i = 0; i < 5; i++) { server.use( http.get( - `http://cn-sampled.local/docs/page${i}`, + `http://test.local/docs/page${i}`, () => new HttpResponse('HTML', { status: 200, @@ -269,7 +269,7 @@ describe('content-negotiation', () => { it('does not overwrite pageCache when already populated', async () => { server.use( http.get( - 'http://cn-cached.local/docs/page', + 'http://test.local/docs/page', () => new HttpResponse('# Page\n\nContent negotiated.', { status: 200, @@ -278,16 +278,16 @@ describe('content-negotiation', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page](http://cn-cached.local/docs/page): A page\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page](http://test.local/docs/page): A page\n`; const ctx = makeCtx(content); // Pre-populate the cache as if markdown-url-support already ran - ctx.pageCache.set('http://cn-cached.local/docs/page', { - url: 'http://cn-cached.local/docs/page', + ctx.pageCache.set('http://test.local/docs/page', { + url: 'http://test.local/docs/page', markdown: { content: '# From md-url', source: 'md-url' }, }); await check.run(ctx); - const cached = ctx.pageCache.get('http://cn-cached.local/docs/page'); + const cached = ctx.pageCache.get('http://test.local/docs/page'); expect(cached?.markdown?.source).toBe('md-url'); expect(cached?.markdown?.content).toBe('# From md-url'); }); diff --git a/test/unit/checks/content-start-position.test.ts b/test/unit/checks/content-start-position.test.ts index 6565797..17faec3 100644 --- a/test/unit/checks/content-start-position.test.ts +++ b/test/unit/checks/content-start-position.test.ts @@ -43,8 +43,8 @@ describe('content-start-position', () => { return ctx; } - function singlePageCtx(domain: string) { - return makeCtx(`# Docs\n> Summary\n## Links\n- [Page](http://${domain}/docs/page): A page\n`); + function singlePageCtx(path: string) { + return makeCtx(`# Docs\n> Summary\n## Links\n- [Page](http://test.local${path}): A page\n`); } // ── Setext heading detection ── @@ -53,7 +53,7 @@ describe('content-start-position', () => { // Turndown converts

to setext (underline) style by default server.use( http.get( - 'http://csp-pass.local/docs/page', + 'http://test.local/docs/csp-pass', () => new HttpResponse( '

Getting Started

Welcome to our documentation.

', @@ -62,7 +62,7 @@ describe('content-start-position', () => { ), ); - const result = await check.run(singlePageCtx('csp-pass.local')); + const result = await check.run(singlePageCtx('/docs/csp-pass')); expect(result.status).toBe('pass'); expect(result.details?.medianPercent).toBe(0); }); @@ -73,7 +73,7 @@ describe('content-start-position', () => { // Turndown uses ATX style for h3+ server.use( http.get( - 'http://csp-atx.local/docs/page', + 'http://test.local/docs/csp-atx', () => new HttpResponse( '

API Reference

Endpoint details below.

', @@ -82,7 +82,7 @@ describe('content-start-position', () => { ), ); - const result = await check.run(singlePageCtx('csp-atx.local')); + const result = await check.run(singlePageCtx('/docs/csp-atx')); expect(result.status).toBe('pass'); const pageResults = result.details?.pageResults as Array<{ contentStartPercent: number }>; expect(pageResults[0].contentStartPercent).toBe(0); @@ -102,12 +102,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-css.local/docs/page', + 'http://test.local/docs/csp-css', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-css.local')); + const result = await check.run(singlePageCtx('/docs/csp-css')); // Content should be found (the heading after CSS), not at position 0 const pageResults = result.details?.pageResults as Array<{ contentStartChar: number; @@ -130,12 +130,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-js.local/docs/page', + 'http://test.local/docs/csp-js', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-js.local')); + const result = await check.run(singlePageCtx('/docs/csp-js')); const pageResults = result.details?.pageResults as Array<{ contentStartChar: number }>; expect(pageResults[0].contentStartChar).toBeGreaterThan(0); }); @@ -153,12 +153,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-nav.local/docs/page', + 'http://test.local/docs/csp-nav', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-nav.local')); + const result = await check.run(singlePageCtx('/docs/csp-nav')); const pageResults = result.details?.pageResults as Array<{ contentStartChar: number }>; expect(pageResults[0].contentStartChar).toBeGreaterThan(0); }); @@ -173,12 +173,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-prose.local/docs/page', + 'http://test.local/docs/csp-prose', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-prose.local')); + const result = await check.run(singlePageCtx('/docs/csp-prose')); const pageResults = result.details?.pageResults as Array<{ contentStartChar: number; totalChars: number; @@ -200,12 +200,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-empty.local/docs/page', + 'http://test.local/docs/csp-empty', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-empty.local')); + const result = await check.run(singlePageCtx('/docs/csp-empty')); expect(result.status).toBe('fail'); const pageResults = result.details?.pageResults as Array<{ contentStartPercent: number }>; expect(pageResults[0].contentStartPercent).toBeGreaterThan(50); @@ -216,12 +216,12 @@ describe('content-start-position', () => { it('handles empty HTML gracefully', async () => { server.use( http.get( - 'http://csp-blank.local/docs/page', + 'http://test.local/docs/csp-blank', () => new HttpResponse('', { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-blank.local')); + const result = await check.run(singlePageCtx('/docs/csp-blank')); const pageResults = result.details?.pageResults as Array<{ contentStartPercent: number }>; // 0% when totalChars is 0 expect(pageResults[0].contentStartPercent).toBe(0); @@ -248,12 +248,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-warnp.local/docs/page', + 'http://test.local/docs/csp-warnp', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-warnp.local')); + const result = await check.run(singlePageCtx('/docs/csp-warnp')); const pageResults = result.details?.pageResults as Array<{ contentStartPercent: number; status: string; @@ -277,12 +277,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-failp.local/docs/page', + 'http://test.local/docs/csp-failp', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-failp.local')); + const result = await check.run(singlePageCtx('/docs/csp-failp')); expect(result.status).toBe('fail'); expect(result.details?.failBucket).toBe(1); expect(result.message).toContain('past 50%'); @@ -294,7 +294,7 @@ describe('content-start-position', () => { // Page 1: content starts immediately (pass) server.use( http.get( - 'http://csp-worst.local/docs/good', + 'http://test.local/docs/good', () => new HttpResponse('

Docs

Good page.

', { status: 200, @@ -310,7 +310,7 @@ describe('content-start-position', () => { ).join('\n'); server.use( http.get( - 'http://csp-worst.local/docs/bad', + 'http://test.local/docs/bad', () => new HttpResponse( `

Late Content

`, @@ -319,7 +319,7 @@ describe('content-start-position', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Good](http://csp-worst.local/docs/good): Good\n- [Bad](http://csp-worst.local/docs/bad): Bad\n`; + const content = `# Docs\n> Summary\n## Links\n- [Good](http://test.local/docs/good): Good\n- [Bad](http://test.local/docs/bad): Bad\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.passBucket).toBeGreaterThanOrEqual(1); @@ -329,9 +329,9 @@ describe('content-start-position', () => { // ── Fetch errors ── it('handles fetch errors gracefully and includes count in message', async () => { - server.use(http.get('http://csp-err.local/docs/page', () => HttpResponse.error())); + server.use(http.get('http://test.local/docs/csp-err', () => HttpResponse.error())); - const result = await check.run(singlePageCtx('csp-err.local')); + const result = await check.run(singlePageCtx('/docs/csp-err')); expect(result.status).toBe('fail'); expect(result.details?.fetchErrors).toBe(1); expect(result.message).toContain('failed to fetch'); @@ -342,17 +342,17 @@ describe('content-start-position', () => { it('appends fetch error count when some pages succeed', async () => { server.use( http.get( - 'http://csp-partial.local/docs/good', + 'http://test.local/docs/good', () => new HttpResponse('

Works

Content.

', { status: 200, headers: { 'Content-Type': 'text/html' }, }), ), - http.get('http://csp-partial.local/docs/broken', () => HttpResponse.error()), + http.get('http://test.local/docs/broken', () => HttpResponse.error()), ); - const content = `# Docs\n> Summary\n## Links\n- [Good](http://csp-partial.local/docs/good): OK\n- [Broken](http://csp-partial.local/docs/broken): Broken\n`; + const content = `# Docs\n> Summary\n## Links\n- [Good](http://test.local/docs/good): OK\n- [Broken](http://test.local/docs/broken): Broken\n`; const result = await check.run(makeCtx(content)); expect(result.details?.fetchErrors).toBe(1); expect(result.message).toContain('1 failed to fetch'); @@ -363,13 +363,13 @@ describe('content-start-position', () => { it('samples when more links than maxLinksToTest', async () => { const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://csp-sample.local/docs/page${i}): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/docs/page${i}): Page ${i}`, ).join('\n'); for (let i = 0; i < 5; i++) { server.use( http.get( - `http://csp-sample.local/docs/page${i}`, + `http://test.local/docs/page${i}`, () => new HttpResponse(`

Page ${i}

Content here.

`, { status: 200, @@ -404,7 +404,7 @@ describe('content-start-position', () => { it('reports per-page position details', async () => { server.use( http.get( - 'http://csp-detail.local/docs/page', + 'http://test.local/docs/csp-detail', () => new HttpResponse( '

Title

A paragraph of meaningful documentation content for testing.

', @@ -413,7 +413,7 @@ describe('content-start-position', () => { ), ); - const result = await check.run(singlePageCtx('csp-detail.local')); + const result = await check.run(singlePageCtx('/docs/csp-detail')); const pageResults = result.details?.pageResults as Array<{ url: string; contentStartChar: number; @@ -421,7 +421,7 @@ describe('content-start-position', () => { contentStartPercent: number; }>; expect(pageResults).toHaveLength(1); - expect(pageResults[0].url).toBe('http://csp-detail.local/docs/page'); + expect(pageResults[0].url).toBe('http://test.local/docs/csp-detail'); expect(pageResults[0].totalChars).toBeGreaterThan(0); expect(pageResults[0].contentStartChar).toBeGreaterThanOrEqual(0); expect(pageResults[0].contentStartPercent).toBeGreaterThanOrEqual(0); @@ -433,7 +433,7 @@ describe('content-start-position', () => { const markdownContent = '# API Guide\n\nThis page documents the `` element.\n'; server.use( http.get( - 'http://csp-md.local/docs/page', + 'http://test.local/docs/csp-md', () => new HttpResponse(markdownContent, { status: 200, @@ -442,7 +442,7 @@ describe('content-start-position', () => { ), ); - const result = await check.run(singlePageCtx('csp-md.local')); + const result = await check.run(singlePageCtx('/docs/csp-md')); expect(result.status).toBe('pass'); const pageResults = result.details?.pageResults as Array<{ contentStartChar: number; @@ -457,7 +457,7 @@ describe('content-start-position', () => { const markdownContent = '# Checkout\n\nSet up `` tags in your HTML page.\n'; server.use( http.get( - 'http://csp-plain.local/docs/page', + 'http://test.local/docs/csp-plain', () => new HttpResponse(markdownContent, { status: 200, @@ -466,7 +466,7 @@ describe('content-start-position', () => { ), ); - const result = await check.run(singlePageCtx('csp-plain.local')); + const result = await check.run(singlePageCtx('/docs/csp-plain')); expect(result.status).toBe('pass'); const pageResults = result.details?.pageResults as Array<{ contentStartPercent: number; @@ -521,12 +521,12 @@ describe('content-start-position', () => { server.use( http.get( - 'http://csp-breadcrumb.local/docs/page', + 'http://test.local/docs/csp-breadcrumb', () => new HttpResponse(html, { status: 200, headers: { 'Content-Type': 'text/html' } }), ), ); - const result = await check.run(singlePageCtx('csp-breadcrumb.local')); + const result = await check.run(singlePageCtx('/docs/csp-breadcrumb')); const pageResults = result.details?.pageResults as Array<{ contentStartChar: number }>; // "Go back" and "Up next" should be skipped, content starts at heading expect(pageResults[0].contentStartChar).toBeGreaterThan(0); diff --git a/test/unit/checks/http-status-codes.test.ts b/test/unit/checks/http-status-codes.test.ts index e5d92ef..0b953cd 100644 --- a/test/unit/checks/http-status-codes.test.ts +++ b/test/unit/checks/http-status-codes.test.ts @@ -51,12 +51,12 @@ describe('http-status-codes', () => { it('passes when bad URLs return 404', async () => { server.use( http.get( - 'http://hsc-pass.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse('Not Found', { status: 404 }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-pass.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.soft404Count).toBe(0); @@ -66,7 +66,7 @@ describe('http-status-codes', () => { it('fails when bad URL returns 200 (soft 404)', async () => { server.use( http.get( - 'http://hsc-soft.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse('

Welcome!

', { status: 200, @@ -75,7 +75,7 @@ describe('http-status-codes', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-soft.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.soft404Count).toBe(1); @@ -84,7 +84,7 @@ describe('http-status-codes', () => { it('detects body hints in soft 404 responses', async () => { server.use( http.get( - 'http://hsc-hint.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse('

Page Not Found

', { status: 200, @@ -93,7 +93,7 @@ describe('http-status-codes', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-hint.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); const pageResults = result.details?.pageResults as Array<{ bodyHint?: string }>; @@ -103,16 +103,16 @@ describe('http-status-codes', () => { it('handles mixed results (some 404, some soft 404)', async () => { server.use( http.get( - 'http://hsc-mix.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://hsc-mix.local/docs/page2-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page2-afdocs-nonexistent-8f3a', () => new HttpResponse('OK', { status: 200 }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-mix.local/docs/page1): First\n- [Page 2](http://hsc-mix.local/docs/page2): Second\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n- [Page 2](http://test.local/docs/page2): Second\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.soft404Count).toBe(1); @@ -121,12 +121,10 @@ describe('http-status-codes', () => { it('handles fetch errors gracefully', async () => { server.use( - http.get('http://hsc-err.local/docs/page1-afdocs-nonexistent-8f3a', () => - HttpResponse.error(), - ), + http.get('http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => HttpResponse.error()), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-err.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.details?.fetchErrors).toBe(1); }); @@ -134,20 +132,17 @@ describe('http-status-codes', () => { it('passes when redirect leads to 404', async () => { server.use( http.get( - 'http://hsc-r404.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse(null, { status: 301, - headers: { Location: 'http://hsc-r404.local/not-found' }, + headers: { Location: 'http://test.local/not-found' }, }), ), - http.get( - 'http://hsc-r404.local/not-found', - () => new HttpResponse('Not Found', { status: 404 }), - ), + http.get('http://test.local/not-found', () => new HttpResponse('Not Found', { status: 404 })), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-r404.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.correctErrorCount).toBe(1); @@ -156,15 +151,15 @@ describe('http-status-codes', () => { it('fails when redirect leads to 200 (soft 404)', async () => { server.use( http.get( - 'http://hsc-r200.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse(null, { status: 301, - headers: { Location: 'http://hsc-r200.local/' }, + headers: { Location: 'http://test.local/' }, }), ), http.get( - 'http://hsc-r200.local/', + 'http://test.local/', () => new HttpResponse('

Home

', { status: 200, @@ -173,7 +168,7 @@ describe('http-status-codes', () => { ), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-r200.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.soft404Count).toBe(1); @@ -181,12 +176,10 @@ describe('http-status-codes', () => { it('fails when all fetches error out', async () => { server.use( - http.get('http://hsc-allfail.local/docs/page1-afdocs-nonexistent-8f3a', () => - HttpResponse.error(), - ), + http.get('http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => HttpResponse.error()), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-allfail.local/docs/page1): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); // The one result is a fetch-error, so tested.length === 0 expect(result.status).toBe('fail'); @@ -197,15 +190,13 @@ describe('http-status-codes', () => { it('includes fetch error count in pass message suffix', async () => { server.use( http.get( - 'http://hsc-partial.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse('Not Found', { status: 404 }), ), - http.get('http://hsc-partial.local/docs/page2-afdocs-nonexistent-8f3a', () => - HttpResponse.error(), - ), + http.get('http://test.local/docs/page2-afdocs-nonexistent-8f3a', () => HttpResponse.error()), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-partial.local/docs/page1): First\n- [Page 2](http://hsc-partial.local/docs/page2): Second\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1): First\n- [Page 2](http://test.local/docs/page2): Second\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.message).toContain('1 failed to fetch'); @@ -214,12 +205,12 @@ describe('http-status-codes', () => { it('strips fragments from test URLs', async () => { server.use( http.get( - 'http://hsc-frag.local/docs/page1-afdocs-nonexistent-8f3a', + 'http://test.local/docs/page1-afdocs-nonexistent-8f3a', () => new HttpResponse('Not Found', { status: 404 }), ), ); - const content = `# Docs\n## Links\n- [Page 1](http://hsc-frag.local/docs/page1#section): First\n`; + const content = `# Docs\n## Links\n- [Page 1](http://test.local/docs/page1#section): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); const pageResults = result.details?.pageResults as Array<{ testUrl: string }>; diff --git a/test/unit/checks/llms-txt-directive.test.ts b/test/unit/checks/llms-txt-directive.test.ts index ef13cb4..5ebe4bb 100644 --- a/test/unit/checks/llms-txt-directive.test.ts +++ b/test/unit/checks/llms-txt-directive.test.ts @@ -48,13 +48,13 @@ describe('llms-txt-directive', () => { return ctx; } - const llms = (host: string, ...pages: string[]) => - `# Docs\n## Links\n${pages.map((p, i) => `- [Page ${i + 1}](http://${host}${p}): Page\n`).join('')}`; + const llms = (...pages: string[]) => + `# Docs\n## Links\n${pages.map((p, i) => `- [Page ${i + 1}](http://test.local${p}): Page\n`).join('')}`; it('passes when directive link is near the top of page', async () => { server.use( http.get( - 'http://ld-pass.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( 'Documentation index for AI agents

Welcome

Content here...

', @@ -63,7 +63,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-pass.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); expect(result.details?.nearTopCount).toBe(1); @@ -73,7 +73,7 @@ describe('llms-txt-directive', () => { it('passes when llms.txt is mentioned as text near the top', async () => { server.use( http.get( - 'http://ld-text.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( '

See our llms.txt for a full documentation index.

Docs

Content...

', @@ -82,7 +82,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-text.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); }); @@ -90,7 +90,7 @@ describe('llms-txt-directive', () => { it('passes with visually hidden directive using sr-only', async () => { server.use( http.get( - 'http://ld-hidden.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( 'Full documentation index

Docs

Content...

', @@ -99,7 +99,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-hidden.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); }); @@ -108,7 +108,7 @@ describe('llms-txt-directive', () => { const padding = '

Lorem ipsum dolor sit amet.

'.repeat(200); server.use( http.get( - 'http://ld-deep.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(`${padding}Index`, { status: 200, @@ -117,7 +117,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-deep.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('warn'); expect(result.details?.buriedCount).toBe(1); expect(result.message).toContain('buried deep'); @@ -126,7 +126,7 @@ describe('llms-txt-directive', () => { it('warns when some pages have directive and some do not', async () => { server.use( http.get( - 'http://ld-partial.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( 'Index

Content

', @@ -134,7 +134,7 @@ describe('llms-txt-directive', () => { ), ), http.get( - 'http://ld-partial.local/docs/page2', + 'http://test.local/docs/page2', () => new HttpResponse('

No directive here

Content

', { status: 200, @@ -143,7 +143,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-partial.local', '/docs/page1', '/docs/page2'))); + const result = await check.run(makeCtx(llms('/docs/page1', '/docs/page2'))); expect(result.status).toBe('warn'); expect(result.details?.foundCount).toBe(1); expect(result.details?.notFoundCount).toBe(1); @@ -153,7 +153,7 @@ describe('llms-txt-directive', () => { it('fails when no directive found in any page', async () => { server.use( http.get( - 'http://ld-none.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( '

Welcome

No agent directive here.

', @@ -162,15 +162,15 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-none.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('fail'); expect(result.details?.foundCount).toBe(0); }); it('fails when all pages fail to fetch', async () => { - server.use(http.get('http://ld-err.local/docs/page1', () => HttpResponse.error())); + server.use(http.get('http://test.local/docs/page1', () => HttpResponse.error())); - const result = await check.run(makeCtx(llms('ld-err.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('fail'); expect(result.message).toContain('Could not test'); }); @@ -178,12 +178,12 @@ describe('llms-txt-directive', () => { it('handles non-200 responses gracefully', async () => { server.use( http.get( - 'http://ld-404.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('Not Found', { status: 404 }), ), ); - const result = await check.run(makeCtx(llms('ld-404.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('fail'); expect(result.details?.fetchErrors).toBe(1); }); @@ -191,7 +191,7 @@ describe('llms-txt-directive', () => { it('detects full URL links to llms.txt', async () => { server.use( http.get( - 'http://ld-full.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( 'Documentation Index

Content

', @@ -200,7 +200,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-full.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); }); @@ -208,7 +208,7 @@ describe('llms-txt-directive', () => { it('ignores pages without body tags', async () => { server.use( http.get( - 'http://ld-nobody.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('Index

No body tags in this response

', { status: 200, @@ -218,7 +218,7 @@ describe('llms-txt-directive', () => { ); // Should still work by falling back to full HTML - const result = await check.run(makeCtx(llms('ld-nobody.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/page1'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); }); @@ -228,7 +228,7 @@ describe('llms-txt-directive', () => { const content = '

Documentation content.

'.repeat(10); server.use( http.get( - 'http://ld-md.local/docs/page1/', + 'http://test.local/docs/page1/', () => new HttpResponse( `Documentation index

Docs

${content}`, @@ -237,7 +237,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-md.local', '/docs/page1/index.md'))); + const result = await check.run(makeCtx(llms('/docs/page1/index.md'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); }); @@ -246,7 +246,7 @@ describe('llms-txt-directive', () => { // HTML URL returns the markdown directly (some sites do this) server.use( http.get( - 'http://ld-mdcontent.local/docs/page1/', + 'http://test.local/docs/page1/', () => new HttpResponse( 'For AI agents: see [documentation index](/llms.txt) for navigation.\n\n# Welcome\n\nContent here.', @@ -255,7 +255,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-mdcontent.local', '/docs/page1/index.md'))); + const result = await check.run(makeCtx(llms('/docs/page1/index.md'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); const pages = result.details?.pageResults as Array<{ source?: string }>; @@ -265,7 +265,7 @@ describe('llms-txt-directive', () => { it('falls back to .md URL when HTML version has no directive', async () => { server.use( http.get( - 'http://ld-fallback.local/docs/page1/', + 'http://test.local/docs/page1/', () => new HttpResponse('

Docs

No directive here

', { status: 200, @@ -273,7 +273,7 @@ describe('llms-txt-directive', () => { }), ), http.get( - 'http://ld-fallback.local/docs/page1/index.md', + 'http://test.local/docs/page1/index.md', () => new HttpResponse( 'For AI agents: see /llms.txt for a documentation index.\n\n# Docs\n\nContent.', @@ -282,7 +282,7 @@ describe('llms-txt-directive', () => { ), ); - const result = await check.run(makeCtx(llms('ld-fallback.local', '/docs/page1/index.md'))); + const result = await check.run(makeCtx(llms('/docs/page1/index.md'))); expect(result.status).toBe('pass'); expect(result.details?.foundCount).toBe(1); const pages = result.details?.pageResults as Array<{ source?: string }>; diff --git a/test/unit/checks/markdown-url-support.test.ts b/test/unit/checks/markdown-url-support.test.ts index b56b02b..db81f31 100644 --- a/test/unit/checks/markdown-url-support.test.ts +++ b/test/unit/checks/markdown-url-support.test.ts @@ -54,7 +54,7 @@ describe('markdown-url-support', () => { server.use( http.get( - 'http://md.local/docs/getting-started.md', + 'http://test.local/docs/getting-started.md', () => new HttpResponse(mdContent, { status: 200, @@ -62,7 +62,7 @@ describe('markdown-url-support', () => { }), ), http.get( - 'http://md.local/docs/api.md', + 'http://test.local/docs/api.md', () => new HttpResponse('# API Reference\n\n[Link](http://example.com)', { status: 200, @@ -74,8 +74,8 @@ describe('markdown-url-support', () => { const content = `# Docs > Summary ## Links -- [Getting Started](http://md.local/docs/getting-started): Guide -- [API](http://md.local/docs/api): Reference +- [Getting Started](http://test.local/docs/getting-started): Guide +- [API](http://test.local/docs/api): Reference `; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('pass'); @@ -85,19 +85,19 @@ describe('markdown-url-support', () => { it('fails when .md URLs return 404', async () => { server.use( http.get( - 'http://no-md.local/docs/page1.md', + 'http://test.local/docs/page1.md', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://no-md.local/docs/page1/index.md', + 'http://test.local/docs/page1/index.md', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://no-md.local/docs/page2.md', + 'http://test.local/docs/page2.md', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://no-md.local/docs/page2/index.md', + 'http://test.local/docs/page2/index.md', () => new HttpResponse('Not Found', { status: 404 }), ), ); @@ -105,8 +105,8 @@ describe('markdown-url-support', () => { const content = `# Docs > Summary ## Links -- [Page 1](http://no-md.local/docs/page1): First -- [Page 2](http://no-md.local/docs/page2): Second +- [Page 1](http://test.local/docs/page1): First +- [Page 2](http://test.local/docs/page2): Second `; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('fail'); @@ -116,7 +116,7 @@ describe('markdown-url-support', () => { it('fails when .md URLs return HTML (soft 404)', async () => { server.use( http.get( - 'http://soft404.local/page.md', + 'http://test.local/soft404-page.md', () => new HttpResponse('Error', { status: 200, @@ -124,7 +124,7 @@ describe('markdown-url-support', () => { }), ), http.get( - 'http://soft404.local/page/index.md', + 'http://test.local/soft404-page/index.md', () => new HttpResponse('Error', { status: 200, @@ -136,7 +136,7 @@ describe('markdown-url-support', () => { const content = `# Docs > Summary ## Links -- [Page](http://soft404.local/page): A page +- [Page](http://test.local/soft404-page): A page `; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('fail'); @@ -145,7 +145,7 @@ describe('markdown-url-support', () => { it('warns when some pages support .md and others do not', async () => { server.use( http.get( - 'http://mixed.local/docs/page1.md', + 'http://test.local/docs/mixed-page1.md', () => new HttpResponse('# Page 1\n\nContent here', { status: 200, @@ -153,19 +153,19 @@ describe('markdown-url-support', () => { }), ), http.get( - 'http://mixed.local/docs/page2.md', + 'http://test.local/docs/mixed-page2.md', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://mixed.local/docs/page2/index.md', + 'http://test.local/docs/mixed-page2/index.md', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://mixed.local/docs/page3.md', + 'http://test.local/docs/mixed-page3.md', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://mixed.local/docs/page3/index.md', + 'http://test.local/docs/mixed-page3/index.md', () => new HttpResponse('Not Found', { status: 404 }), ), ); @@ -173,9 +173,9 @@ describe('markdown-url-support', () => { const content = `# Docs > Summary ## Links -- [Page 1](http://mixed.local/docs/page1): First -- [Page 2](http://mixed.local/docs/page2): Second -- [Page 3](http://mixed.local/docs/page3): Third +- [Page 1](http://test.local/docs/mixed-page1): First +- [Page 2](http://test.local/docs/mixed-page2): Second +- [Page 3](http://test.local/docs/mixed-page3): Third `; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('warn'); @@ -203,13 +203,13 @@ describe('markdown-url-support', () => { // Generate 5 links but set maxLinksToTest to 2 const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://sample.local/docs/page${i}): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/docs/sample-page${i}): Page ${i}`, ).join('\n'); for (let i = 0; i < 5; i++) { server.use( http.get( - `http://sample.local/docs/page${i}.md`, + `http://test.local/docs/sample-page${i}.md`, () => new HttpResponse(`# Page ${i}\n\nContent`, { status: 200, @@ -240,11 +240,11 @@ describe('markdown-url-support', () => { it('handles fetch errors gracefully and reports error field', async () => { server.use( - http.get('http://err.local/docs/page.md', () => HttpResponse.error()), - http.get('http://err.local/docs/page/index.md', () => HttpResponse.error()), + http.get('http://test.local/docs/err-page.md', () => HttpResponse.error()), + http.get('http://test.local/docs/err-page/index.md', () => HttpResponse.error()), ); - const content = `# Docs\n> Summary\n## Links\n- [Page](http://err.local/docs/page): A page\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page](http://test.local/docs/err-page): A page\n`; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('fail'); const pageResults = result.details?.pageResults as Array<{ @@ -262,16 +262,16 @@ describe('markdown-url-support', () => { it('reports rate-limited results (HTTP 429)', async () => { server.use( http.get( - 'http://rl.local/docs/page.md', + 'http://test.local/docs/rl-page.md', () => new HttpResponse('Too Many Requests', { status: 429 }), ), http.get( - 'http://rl.local/docs/page/index.md', + 'http://test.local/docs/rl-page/index.md', () => new HttpResponse('Too Many Requests', { status: 429 }), ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page](http://rl.local/docs/page): A page\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page](http://test.local/docs/rl-page): A page\n`; const result = await check.run(makeCtx({ content })); // 429 with non-markdown body won't count as supported, so status 0 (last candidate fails) // but the page result itself won't have status 429 because none succeeded @@ -284,17 +284,17 @@ describe('markdown-url-support', () => { it('includes "sampled" in message when results are sampled', async () => { const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://sample-msg.local/docs/page${i}): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/docs/sample-msg-page${i}): Page ${i}`, ).join('\n'); for (let i = 0; i < 5; i++) { server.use( http.get( - `http://sample-msg.local/docs/page${i}.md`, + `http://test.local/docs/sample-msg-page${i}.md`, () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - `http://sample-msg.local/docs/page${i}/index.md`, + `http://test.local/docs/sample-msg-page${i}/index.md`, () => new HttpResponse('Not Found', { status: 404 }), ), ); @@ -321,7 +321,7 @@ describe('markdown-url-support', () => { it('uses URL directly when it already ends in .md', async () => { server.use( http.get( - 'http://already-md.local/spec/index.md', + 'http://test.local/spec/index.md', () => new HttpResponse('# Spec\n\nThe full specification.', { status: 200, @@ -330,22 +330,22 @@ describe('markdown-url-support', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Spec](http://already-md.local/spec/index.md): Full spec\n`; + const content = `# Docs\n> Summary\n## Links\n- [Spec](http://test.local/spec/index.md): Full spec\n`; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('pass'); expect(result.details?.mdSupported).toBe(1); const pageResults = result.details?.pageResults as Array<{ mdUrl: string }>; - expect(pageResults[0].mdUrl).toBe('http://already-md.local/spec/index.md'); + expect(pageResults[0].mdUrl).toBe('http://test.local/spec/index.md'); }); it('finds markdown at index.md when direct .md fails', async () => { server.use( http.get( - 'http://indexmd.local/docs/guide.md', + 'http://test.local/docs/guide.md', () => new HttpResponse('Not Found', { status: 404 }), ), http.get( - 'http://indexmd.local/docs/guide/index.md', + 'http://test.local/docs/guide/index.md', () => new HttpResponse('# Guide\n\nThis is the guide.', { status: 200, @@ -354,18 +354,18 @@ describe('markdown-url-support', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Guide](http://indexmd.local/docs/guide): Guide\n`; + const content = `# Docs\n> Summary\n## Links\n- [Guide](http://test.local/docs/guide): Guide\n`; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('pass'); expect(result.details?.mdSupported).toBe(1); const pageResults = result.details?.pageResults as Array<{ mdUrl: string }>; - expect(pageResults[0].mdUrl).toBe('http://indexmd.local/docs/guide/index.md'); + expect(pageResults[0].mdUrl).toBe('http://test.local/docs/guide/index.md'); }); it('supports markdown detected by body only (no text/markdown content-type)', async () => { server.use( http.get( - 'http://bodyonly.local/docs/page.md', + 'http://test.local/docs/bodyonly-page.md', () => new HttpResponse('# Hello\n\nSome [link](http://example.com) here.\n\n```js\ncode\n```', { status: 200, @@ -374,7 +374,7 @@ describe('markdown-url-support', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page](http://bodyonly.local/docs/page): A page\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page](http://test.local/docs/bodyonly-page): A page\n`; const result = await check.run(makeCtx({ content })); expect(result.status).toBe('pass'); expect(result.details?.mdSupported).toBe(1); @@ -384,7 +384,7 @@ describe('markdown-url-support', () => { const mdContent = '# Cached\n\nThis should be cached.'; server.use( http.get( - 'http://cache.local/docs/page.md', + 'http://test.local/docs/cache-page.md', () => new HttpResponse(mdContent, { status: 200, @@ -396,11 +396,11 @@ describe('markdown-url-support', () => { const content = `# Docs > Summary ## Links -- [Page](http://cache.local/docs/page): A page +- [Page](http://test.local/docs/cache-page): A page `; const ctx = makeCtx({ content }); await check.run(ctx); - const cached = ctx.pageCache.get('http://cache.local/docs/page'); + const cached = ctx.pageCache.get('http://test.local/docs/cache-page'); expect(cached).toBeDefined(); expect(cached?.markdown?.content).toBe(mdContent); expect(cached?.markdown?.source).toBe('md-url'); diff --git a/test/unit/checks/page-size-html.test.ts b/test/unit/checks/page-size-html.test.ts index 628f7ca..7940419 100644 --- a/test/unit/checks/page-size-html.test.ts +++ b/test/unit/checks/page-size-html.test.ts @@ -46,7 +46,7 @@ describe('page-size-html', () => { it('passes when HTML converts to small markdown', async () => { server.use( http.get( - 'http://ps-html-pass.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('

Hello

Short page.

', { status: 200, @@ -55,7 +55,7 @@ describe('page-size-html', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-pass.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.testedPages).toBe(1); @@ -71,7 +71,7 @@ describe('page-size-html', () => { const bigContent = '

' + 'x'.repeat(200) + '

'; server.use( http.get( - 'http://ps-html-fail.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(`${bigContent}`, { status: 200, @@ -80,7 +80,7 @@ describe('page-size-html', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-fail.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const ctx = createContext('http://test.local', { requestDelay: 0, thresholds: { pass: 10, fail: 50 }, @@ -105,7 +105,7 @@ describe('page-size-html', () => { const html = `

Title

`; server.use( http.get( - 'http://ps-html-ratio.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(html, { status: 200, @@ -114,7 +114,7 @@ describe('page-size-html', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-ratio.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); const pageResults = result.details?.pageResults as Array<{ conversionRatio: number; @@ -124,9 +124,9 @@ describe('page-size-html', () => { }); it('handles fetch errors gracefully', async () => { - server.use(http.get('http://ps-html-err.local/docs/page1', () => HttpResponse.error())); + server.use(http.get('http://test.local/docs/page1', () => HttpResponse.error())); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-err.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.fetchErrors).toBe(1); @@ -135,13 +135,13 @@ describe('page-size-html', () => { it('samples when more links than maxLinksToTest', async () => { const links = Array.from( { length: 5 }, - (_, i) => `- [Page ${i}](http://ps-html-sample.local/docs/page${i}): Page ${i}`, + (_, i) => `- [Page ${i}](http://test.local/docs/page${i}): Page ${i}`, ).join('\n'); for (let i = 0; i < 5; i++) { server.use( http.get( - `http://ps-html-sample.local/docs/page${i}`, + `http://test.local/docs/page${i}`, () => new HttpResponse(`

Page ${i}

`, { status: 200, @@ -175,7 +175,7 @@ describe('page-size-html', () => { '# API Guide\n\nThis is a markdown page about the `` element.\n\nMore content here.'; server.use( http.get( - 'http://ps-html-md.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(markdownContent, { status: 200, @@ -184,7 +184,7 @@ describe('page-size-html', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-md.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); const pageResults = result.details?.pageResults as Array<{ @@ -200,7 +200,7 @@ describe('page-size-html', () => { const markdownContent = '# Checkout Guide\n\nSet up `` tags in your HTML page.\n'; server.use( http.get( - 'http://ps-html-plain.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(markdownContent, { status: 200, @@ -209,7 +209,7 @@ describe('page-size-html', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-plain.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); const pageResults = result.details?.pageResults as Array<{ htmlCharacters: number; @@ -223,7 +223,7 @@ describe('page-size-html', () => { // Create HTML that converts to ~25 chars (between pass=10 and fail=50) server.use( http.get( - 'http://ps-html-warn.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('

Some medium length content here.

', { status: 200, @@ -232,7 +232,7 @@ describe('page-size-html', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-warn.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const ctx = createContext('http://test.local', { requestDelay: 0, thresholds: { pass: 10, fail: 50 }, @@ -257,7 +257,7 @@ describe('page-size-html', () => { // Response with no standard content-type but body is clearly HTML server.use( http.get( - 'http://ps-html-detect.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( '

Detected

HTML content.

', @@ -269,7 +269,7 @@ describe('page-size-html', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://ps-html-detect.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); const pageResults = result.details?.pageResults as Array<{ htmlCharacters: number; @@ -283,19 +283,19 @@ describe('page-size-html', () => { it('includes fetch error count in message suffix', async () => { server.use( http.get( - 'http://ps-html-suffix.local/docs/good', + 'http://test.local/docs/good', () => new HttpResponse('

OK

', { status: 200, headers: { 'Content-Type': 'text/html' }, }), ), - http.get('http://ps-html-suffix.local/docs/broken', () => HttpResponse.error()), + http.get('http://test.local/docs/broken', () => HttpResponse.error()), ); const content = `# Docs\n> Summary\n## Links -- [Good](http://ps-html-suffix.local/docs/good): OK -- [Broken](http://ps-html-suffix.local/docs/broken): Broken +- [Good](http://test.local/docs/good): OK +- [Broken](http://test.local/docs/broken): Broken `; const result = await check.run(makeCtx(content)); expect(result.details?.fetchErrors).toBe(1); diff --git a/test/unit/checks/redirect-behavior.test.ts b/test/unit/checks/redirect-behavior.test.ts index 56d6052..45936b5 100644 --- a/test/unit/checks/redirect-behavior.test.ts +++ b/test/unit/checks/redirect-behavior.test.ts @@ -48,15 +48,15 @@ describe('redirect-behavior', () => { return ctx; } - const llms = (host: string, ...pages: string[]) => - `# Docs\n## Links\n${pages.map((p, i) => `- [Page ${i + 1}](http://${host}${p}): Page\n`).join('')}`; + const llms = (...pages: string[]) => + `# Docs\n## Links\n${pages.map((p, i) => `- [Page ${i + 1}](http://test.local${p}): Page\n`).join('')}`; it('passes when pages return 200 with no redirects', async () => { server.use( - http.get('http://rb-pass.local/docs/page1', () => new HttpResponse('OK', { status: 200 })), + http.get('http://test.local/docs/rb-pass', () => new HttpResponse('OK', { status: 200 })), ); - const result = await check.run(makeCtx(llms('rb-pass.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-pass'))); expect(result.status).toBe('pass'); expect(result.details?.noRedirectCount).toBe(1); }); @@ -64,16 +64,16 @@ describe('redirect-behavior', () => { it('passes when redirects are same-host', async () => { server.use( http.get( - 'http://rb-same.local/old-page', + 'http://test.local/rb-same/old-page', () => new HttpResponse(null, { status: 301, - headers: { Location: 'http://rb-same.local/new-page' }, + headers: { Location: 'http://test.local/rb-same/new-page' }, }), ), ); - const result = await check.run(makeCtx(llms('rb-same.local', '/old-page'))); + const result = await check.run(makeCtx(llms('/rb-same/old-page'))); expect(result.status).toBe('pass'); expect(result.details?.sameHostCount).toBe(1); expect(result.message).toContain('same-host'); @@ -82,7 +82,7 @@ describe('redirect-behavior', () => { it('warns on cross-host redirects', async () => { server.use( http.get( - 'http://rb-cross.local/docs/page1', + 'http://test.local/docs/rb-cross', () => new HttpResponse(null, { status: 301, @@ -91,7 +91,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-cross.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-cross'))); expect(result.status).toBe('warn'); expect(result.details?.crossHostCount).toBe(1); expect(result.message).toContain('cross-host'); @@ -100,7 +100,7 @@ describe('redirect-behavior', () => { it('fails on JavaScript redirects', async () => { server.use( http.get( - 'http://rb-js.local/docs/page1', + 'http://test.local/docs/rb-js', () => new HttpResponse('', { status: 200, @@ -109,7 +109,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-js.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-js'))); expect(result.status).toBe('fail'); expect(result.details?.jsRedirectCount).toBe(1); expect(result.message).toContain('JavaScript'); @@ -118,7 +118,7 @@ describe('redirect-behavior', () => { it('detects meta refresh redirects', async () => { server.use( http.get( - 'http://rb-meta.local/docs/page1', + 'http://test.local/docs/rb-meta', () => new HttpResponse( '', @@ -127,7 +127,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-meta.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-meta'))); expect(result.status).toBe('fail'); expect(result.details?.jsRedirectCount).toBe(1); }); @@ -135,7 +135,7 @@ describe('redirect-behavior', () => { it('handles mixed results: js redirect takes precedence over cross-host', async () => { server.use( http.get( - 'http://rb-mix.local/docs/page1', + 'http://test.local/docs/rb-mix1', () => new HttpResponse(null, { status: 302, @@ -143,7 +143,7 @@ describe('redirect-behavior', () => { }), ), http.get( - 'http://rb-mix.local/docs/page2', + 'http://test.local/docs/rb-mix2', () => new HttpResponse('', { status: 200, @@ -152,7 +152,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-mix.local', '/docs/page1', '/docs/page2'))); + const result = await check.run(makeCtx(llms('/docs/rb-mix1', '/docs/rb-mix2'))); expect(result.status).toBe('fail'); expect(result.details?.crossHostCount).toBe(1); expect(result.details?.jsRedirectCount).toBe(1); @@ -161,16 +161,16 @@ describe('redirect-behavior', () => { }); it('handles fetch errors gracefully', async () => { - server.use(http.get('http://rb-err.local/docs/page1', () => HttpResponse.error())); + server.use(http.get('http://test.local/docs/rb-err', () => HttpResponse.error())); - const result = await check.run(makeCtx(llms('rb-err.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-err'))); expect(result.details?.fetchErrors).toBe(1); }); it('fails when all fetches error out', async () => { - server.use(http.get('http://rb-allfail.local/docs/page1', () => HttpResponse.error())); + server.use(http.get('http://test.local/docs/rb-allfail', () => HttpResponse.error())); - const result = await check.run(makeCtx(llms('rb-allfail.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-allfail'))); expect(result.status).toBe('fail'); expect(result.message).toContain('Could not test any URLs'); }); @@ -178,16 +178,16 @@ describe('redirect-behavior', () => { it('handles relative Location headers', async () => { server.use( http.get( - 'http://rb-rel.local/docs/old', + 'http://test.local/docs/rb-rel-old', () => new HttpResponse(null, { status: 301, - headers: { Location: '/docs/new' }, + headers: { Location: '/docs/rb-rel-new' }, }), ), ); - const result = await check.run(makeCtx(llms('rb-rel.local', '/docs/old'))); + const result = await check.run(makeCtx(llms('/docs/rb-rel-old'))); expect(result.status).toBe('pass'); expect(result.details?.sameHostCount).toBe(1); }); @@ -195,7 +195,7 @@ describe('redirect-behavior', () => { it('ignores window.location inside blocks', async () => { server.use( http.get( - 'http://rb-code.local/docs/page1', + 'http://test.local/docs/rb-code', () => new HttpResponse( '

Use window.location = "/page" to navigate.

', @@ -204,7 +204,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-code.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-code'))); expect(result.status).toBe('pass'); expect(result.details?.jsRedirectCount).toBe(0); }); @@ -212,7 +212,7 @@ describe('redirect-behavior', () => { it('ignores window.location inside
 blocks', async () => {
     server.use(
       http.get(
-        'http://rb-pre.local/docs/page1',
+        'http://test.local/docs/rb-pre',
         () =>
           new HttpResponse(
             '
window.location.href = "/new";
', @@ -221,7 +221,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-pre.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-pre'))); expect(result.status).toBe('pass'); expect(result.details?.jsRedirectCount).toBe(0); }); @@ -229,7 +229,7 @@ describe('redirect-behavior', () => { it('ignores meta refresh inside
 blocks', async () => {
     server.use(
       http.get(
-        'http://rb-premeta.local/docs/page1',
+        'http://test.local/docs/rb-premeta',
         () =>
           new HttpResponse(
             '
<meta http-equiv="refresh" content="0;url=/new">
', @@ -238,7 +238,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-premeta.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-premeta'))); expect(result.status).toBe('pass'); expect(result.details?.jsRedirectCount).toBe(0); }); @@ -246,7 +246,7 @@ describe('redirect-behavior', () => { it('ignores window.location property reads in ', @@ -255,7 +255,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-read.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-read'))); expect(result.status).toBe('pass'); expect(result.details?.jsRedirectCount).toBe(0); }); @@ -263,7 +263,7 @@ describe('redirect-behavior', () => { it('still detects real JS redirects in ', @@ -272,7 +272,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-script.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-script'))); expect(result.status).toBe('fail'); expect(result.details?.jsRedirectCount).toBe(1); }); @@ -280,7 +280,7 @@ describe('redirect-behavior', () => { it('classifies 302 redirects the same as 301', async () => { server.use( http.get( - 'http://rb-302.local/docs/page1', + 'http://test.local/docs/rb-302', () => new HttpResponse(null, { status: 302, @@ -289,7 +289,7 @@ describe('redirect-behavior', () => { ), ); - const result = await check.run(makeCtx(llms('rb-302.local', '/docs/page1'))); + const result = await check.run(makeCtx(llms('/docs/rb-302'))); expect(result.status).toBe('warn'); expect(result.details?.crossHostCount).toBe(1); }); diff --git a/test/unit/checks/tabbed-content-serialization.test.ts b/test/unit/checks/tabbed-content-serialization.test.ts index b16a6ea..a6835d8 100644 --- a/test/unit/checks/tabbed-content-serialization.test.ts +++ b/test/unit/checks/tabbed-content-serialization.test.ts @@ -46,7 +46,7 @@ describe('tabbed-content-serialization', () => { it('passes when page has no tabbed content', async () => { server.use( http.get( - 'http://tcs-notabs.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('

Hello

No tabs here.

', { status: 200, @@ -55,7 +55,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-notabs.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.message).toContain('No tabbed content'); @@ -72,7 +72,7 @@ describe('tabbed-content-serialization', () => { `; server.use( http.get( - 'http://tcs-small.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(`${tabHtml}`, { status: 200, @@ -81,7 +81,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-small.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); expect(result.details?.totalGroupsFound).toBe(1); @@ -101,7 +101,7 @@ describe('tabbed-content-serialization', () => { `; server.use( http.get( - 'http://tcs-big.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(`${tabHtml}`, { status: 200, @@ -110,16 +110,16 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-big.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.message).toContain('over 100K'); }); it('handles fetch errors gracefully', async () => { - server.use(http.get('http://tcs-err.local/docs/page1', () => HttpResponse.error())); + server.use(http.get('http://test.local/docs/page1', () => HttpResponse.error())); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-err.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('fail'); expect(result.details?.fetchErrors).toBe(1); @@ -128,7 +128,7 @@ describe('tabbed-content-serialization', () => { it('skips conversion for markdown responses', async () => { server.use( http.get( - 'http://tcs-md.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse('# Hello\n\nNo tabs in markdown.', { status: 200, @@ -137,7 +137,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-md.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); const tabbedPages = result.details?.tabbedPages as Array<{ totalTabbedChars: number }>; @@ -153,7 +153,7 @@ describe('tabbed-content-serialization', () => { `; server.use( http.get( - 'http://tcs-details.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(`${tabHtml}`, { status: 200, @@ -162,7 +162,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-details.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.details?.tabbedPages).toBeDefined(); const tabbedPages = result.details?.tabbedPages as Array<{ @@ -176,7 +176,7 @@ describe('tabbed-content-serialization', () => { const mdContent = `# Guide\n\n\n\n\npip install foo\n\n\n\n\nnpm install foo\n\n\n\n`; server.use( http.get( - 'http://tcs-mdx.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(mdContent, { status: 200, @@ -185,7 +185,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-mdx.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('pass'); const tabbedPages = result.details?.tabbedPages as Array<{ @@ -207,7 +207,7 @@ describe('tabbed-content-serialization', () => { server.use( http.get( - 'http://tcs-spa.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(spaHtml, { status: 200, @@ -215,7 +215,7 @@ describe('tabbed-content-serialization', () => { }), ), http.get( - 'http://tcs-spa.local/docs/page1.md', + 'http://test.local/docs/page1.md', () => new HttpResponse(mdContent, { status: 200, @@ -224,7 +224,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-spa.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const ctx = makeCtx(content); // Simulate rendering-strategy having flagged this URL as an SPA shell ctx.previousResults.set('rendering-strategy', { @@ -233,7 +233,7 @@ describe('tabbed-content-serialization', () => { status: 'fail', message: 'SPA shell detected', details: { - pageResults: [{ url: 'http://tcs-spa.local/docs/page1', status: 'fail' }], + pageResults: [{ url: 'http://test.local/docs/page1', status: 'fail' }], }, }); const result = await check.run(ctx); @@ -259,7 +259,7 @@ describe('tabbed-content-serialization', () => { `; server.use( http.get( - 'http://tcs-warn.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(`${tabHtml}`, { status: 200, @@ -268,7 +268,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-warn.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.status).toBe('warn'); expect(result.message).toContain('50K–100K'); @@ -285,17 +285,17 @@ describe('tabbed-content-serialization', () => { `; server.use( http.get( - 'http://tcs-partial1.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(`${tabHtml}`, { status: 200, headers: { 'Content-Type': 'text/html' }, }), ), - http.get('http://tcs-partial2.local/docs/page2', () => HttpResponse.error()), + http.get('http://test.local/docs/page2', () => HttpResponse.error()), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-partial1.local/docs/page1): First\n- [Page 2](http://tcs-partial2.local/docs/page2): Second\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n- [Page 2](http://test.local/docs/page2): Second\n`; const result = await check.run(makeCtx(content)); expect(result.message).toContain('1 failed to fetch'); expect(result.details?.fetchErrors).toBe(1); @@ -311,7 +311,7 @@ describe('tabbed-content-serialization', () => { server.use( http.get( - 'http://tcs-spa-notabs.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse(spaHtml, { status: 200, @@ -320,16 +320,16 @@ describe('tabbed-content-serialization', () => { ), // .md candidate returns 404 so tryMdFallback returns null http.get( - 'http://tcs-spa-notabs.local/docs/page1.md', + 'http://test.local/docs/page1.md', () => new HttpResponse('Not found', { status: 404 }), ), http.get( - 'http://tcs-spa-notabs.local/docs/page1/index.md', + 'http://test.local/docs/page1/index.md', () => new HttpResponse('Not found', { status: 404 }), ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-spa-notabs.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const ctx = makeCtx(content); ctx.previousResults.set('rendering-strategy', { id: 'rendering-strategy', @@ -337,7 +337,7 @@ describe('tabbed-content-serialization', () => { status: 'fail', message: 'SPA shell detected', details: { - pageResults: [{ url: 'http://tcs-spa-notabs.local/docs/page1', status: 'fail' }], + pageResults: [{ url: 'http://test.local/docs/page1', status: 'fail' }], }, }); const result = await check.run(ctx); @@ -355,7 +355,7 @@ describe('tabbed-content-serialization', () => { // Regular server-rendered HTML with no tabs server.use( http.get( - 'http://tcs-nospa.local/docs/page1', + 'http://test.local/docs/page1', () => new HttpResponse( '

Hello

' + 'Real content. '.repeat(100) + '

', @@ -367,7 +367,7 @@ describe('tabbed-content-serialization', () => { ), // This .md URL has tabs, but should NOT be fetched http.get( - 'http://tcs-nospa.local/docs/page1.md', + 'http://test.local/docs/page1.md', () => new HttpResponse('A', { status: 200, @@ -376,7 +376,7 @@ describe('tabbed-content-serialization', () => { ), ); - const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://tcs-nospa.local/docs/page1): First\n`; + const content = `# Docs\n> Summary\n## Links\n- [Page 1](http://test.local/docs/page1): First\n`; const result = await check.run(makeCtx(content)); expect(result.message).toContain('No tabbed content'); const tabbedPages = result.details?.tabbedPages as Array<{ source: string }>;