From 5d587140d08d1e1d06e0ce02d167e0e204b50cec Mon Sep 17 00:00:00 2001 From: forager Date: Sun, 15 Mar 2026 04:26:44 +0000 Subject: [PATCH] chore: add Atom autodiscovery link check to check-visibility.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds hasAtomAutodiscoveryLink() that scans all tags and returns true when any advertises application/atom+xml. Wires the check into runChecks() against the /proposals/ hub page — the Atom feed for Colony proposals should be discoverable there. 6 unit tests cover: well-formed tag, absent tag, empty string, attribute order, case-insensitivity, and JSON-first/Atom-second ordering. Closes #587 --- .../__tests__/check-visibility.test.ts | 39 ++++++++ web/scripts/check-visibility.ts | 92 +++++++++++++++---- 2 files changed, 111 insertions(+), 20 deletions(-) diff --git a/web/scripts/__tests__/check-visibility.test.ts b/web/scripts/__tests__/check-visibility.test.ts index dc4ef394..95cc0ae6 100644 --- a/web/scripts/__tests__/check-visibility.test.ts +++ b/web/scripts/__tests__/check-visibility.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { buildRepositoryApiUrl, + hasAtomAutodiscoveryLink, hasTwitterImageAltText, isValidOpenGraphImageType, normalizeHttpsUrl, @@ -183,6 +184,44 @@ describe('hasTwitterImageAltText', () => { }); }); +describe('hasAtomAutodiscoveryLink', () => { + it('detects a well-formed Atom autodiscovery link tag', () => { + const html = ` + + `; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); + + it('returns false when the autodiscovery link is absent', () => { + const html = ` + + `; + expect(hasAtomAutodiscoveryLink(html)).toBe(false); + }); + + it('returns false for an empty string', () => { + expect(hasAtomAutodiscoveryLink('')).toBe(false); + }); + + it('handles attribute order variations', () => { + const html = ``; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); + + it('is case-insensitive for the type attribute', () => { + const html = ``; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); + + it('detects Atom link when another alternate link comes first', () => { + const html = ` + + + `; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); +}); + describe('VisibilityReport', () => { it('has the expected shape with summary and checks fields', () => { const checks: CheckResult[] = [ diff --git a/web/scripts/check-visibility.ts b/web/scripts/check-visibility.ts index 7adf8650..373899c0 100644 --- a/web/scripts/check-visibility.ts +++ b/web/scripts/check-visibility.ts @@ -243,6 +243,42 @@ function extractFileBackedFaviconHref(html: string): string { return ''; } +export function hasAtomAutodiscoveryLink(html: string): boolean { + const tagPattern = /]*>/gi; + const attrPattern = (attribute: string): RegExp => + new RegExp( + `\\b${attribute}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'=<>]+))`, + 'i' + ); + + for (const match of html.matchAll(tagPattern)) { + const tag = match[0]; + const relMatch = tag.match(attrPattern('rel')); + const relValue = ( + relMatch?.[1] ?? + relMatch?.[2] ?? + relMatch?.[3] ?? + '' + ).trim(); + if (!relValue.toLowerCase().split(/\s+/).includes('alternate')) { + continue; + } + + const typeMatch = tag.match(attrPattern('type')); + const typeValue = ( + typeMatch?.[1] ?? + typeMatch?.[2] ?? + typeMatch?.[3] ?? + '' + ).trim(); + if (typeValue.toLowerCase() === 'application/atom+xml') { + return true; + } + } + + return false; +} + async function runChecks(): Promise { const indexHtml = readIfExists(INDEX_HTML_PATH); const sitemapXml = readIfExists(SITEMAP_PATH); @@ -352,28 +388,44 @@ async function runChecks(): Promise { ok: rootRes?.status === 200, }); - const deployedHubChecks = await Promise.all( - [ - { label: 'Deployed /agents/ hub is reachable', path: 'agents/' }, - { - label: 'Deployed /proposals/ hub is reachable', - path: 'proposals/', - }, - ].map(async ({ label, path }) => { - const url = resolveDeployedPageUrl(baseUrl, path); - const response = await fetchWithTimeout(url); - const ok = response?.status === 200; - return { - label, - ok, - details: ok - ? `GET ${url} returned 200` - : `GET ${url} returned ${response?.status ?? 'no response'}`, - }; - }) + const agentsHubUrl = resolveDeployedPageUrl(baseUrl, 'agents/'); + const proposalsHubUrl = resolveDeployedPageUrl(baseUrl, 'proposals/'); + const [agentsHubRes, proposalsHubRes] = await Promise.all([ + fetchWithTimeout(agentsHubUrl), + fetchWithTimeout(proposalsHubUrl), + ]); + + const agentsOk = agentsHubRes?.status === 200; + const proposalsOk = proposalsHubRes?.status === 200; + results.push( + { + label: 'Deployed /agents/ hub is reachable', + ok: agentsOk, + details: agentsOk + ? `GET ${agentsHubUrl} returned 200` + : `GET ${agentsHubUrl} returned ${agentsHubRes?.status ?? 'no response'}`, + }, + { + label: 'Deployed /proposals/ hub is reachable', + ok: proposalsOk, + details: proposalsOk + ? `GET ${proposalsHubUrl} returned 200` + : `GET ${proposalsHubUrl} returned ${proposalsHubRes?.status ?? 'no response'}`, + } ); - results.push(...deployedHubChecks); + const proposalsHubHtml = + proposalsHubRes?.status === 200 ? await proposalsHubRes.text() : ''; + const atomAutodiscoveryPresent = hasAtomAutodiscoveryLink(proposalsHubHtml); + results.push({ + label: 'Deployed /proposals/ hub exposes Atom feed autodiscovery', + ok: atomAutodiscoveryPresent, + details: atomAutodiscoveryPresent + ? 'Found on /proposals/ hub' + : proposalsHubRes?.status === 200 + ? 'Missing on /proposals/ hub' + : `Could not fetch /proposals/ hub: ${proposalsHubRes?.status ?? 'no response'}`, + }); let deployedRootHtml = ''; let deployedJsonLd = false;