Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions web/scripts/__tests__/check-visibility.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
buildRepositoryApiUrl,
hasAtomAutodiscoveryLink,
hasTwitterImageAltText,
isValidOpenGraphImageType,
normalizeHttpsUrl,
Expand Down Expand Up @@ -183,6 +184,44 @@ describe('hasTwitterImageAltText', () => {
});
});

describe('hasAtomAutodiscoveryLink', () => {
it('detects a well-formed Atom autodiscovery link tag', () => {
const html = `<head>
<link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Colony Proposals">
</head>`;
expect(hasAtomAutodiscoveryLink(html)).toBe(true);
});

it('returns false when the autodiscovery link is absent', () => {
const html = `<head>
<link rel="stylesheet" href="/style.css">
</head>`;
expect(hasAtomAutodiscoveryLink(html)).toBe(false);
});

it('returns false for an empty string', () => {
expect(hasAtomAutodiscoveryLink('')).toBe(false);
});

it('handles attribute order variations', () => {
const html = `<link type="application/atom+xml" rel="alternate" href="/feed.xml">`;
expect(hasAtomAutodiscoveryLink(html)).toBe(true);
});

it('is case-insensitive for the type attribute', () => {
const html = `<link rel="alternate" type="Application/Atom+XML" href="/feed.xml">`;
expect(hasAtomAutodiscoveryLink(html)).toBe(true);
});

it('detects Atom link when another alternate link comes first', () => {
const html = `<head>
<link rel="alternate" type="application/json" href="/api/feed">
<link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Colony Proposals">
</head>`;
expect(hasAtomAutodiscoveryLink(html)).toBe(true);
});
});

describe('VisibilityReport', () => {
it('has the expected shape with summary and checks fields', () => {
const checks: CheckResult[] = [
Expand Down
92 changes: 72 additions & 20 deletions web/scripts/check-visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,42 @@ function extractFileBackedFaviconHref(html: string): string {
return '';
}

export function hasAtomAutodiscoveryLink(html: string): boolean {
const tagPattern = /<link\b[^>]*>/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<CheckResult[]> {
const indexHtml = readIfExists(INDEX_HTML_PATH);
const sitemapXml = readIfExists(SITEMAP_PATH);
Expand Down Expand Up @@ -352,28 +388,44 @@ async function runChecks(): Promise<CheckResult[]> {
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 <link rel="alternate" type="application/atom+xml"> on /proposals/ hub'
: proposalsHubRes?.status === 200
? 'Missing <link rel="alternate" type="application/atom+xml"> on /proposals/ hub'
: `Could not fetch /proposals/ hub: ${proposalsHubRes?.status ?? 'no response'}`,
});

let deployedRootHtml = '';
let deployedJsonLd = false;
Expand Down
Loading