Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .Jules/vantage.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
## 2026-05-20 - Hardening Telemetry Verification and Refining Stream Types | **Learning:** Index-based assertions in telemetry tests are brittle; using `.find()` ensures robust verification. Standardizing on `String(error)` across all API catch blocks prevents context loss during failure events. | **Action:** Refactored `chat-api.test.ts` and `github-releases.test.ts` to use search-based spy assertions, and added explicit return types to `chat-stream.ts` to resolve Astro compiler hints.

## 2026-05-24 - Comprehensive Unit Testing for Visitor Changelog Formatting | **Learning:** Single-pass Edge transformations require dedicated unit tests covering edge cases like empty inputs, internal agent filtering, and fallback formatting to prevent silent regressions in SSR release streams. | **Action:** Added `src/utils/visitor-changelog.test.ts` to test `toVisitorChangelogTitle`, `stripChangelogChrome`, `toVisitorReleaseBody`, and `toVisitorRelease`.

## 2026-05-28 - Testing RSS and Sitemap Route Endpoints | **Learning:** Standalone route handlers that export `GET` responses (like RSS feeds and Sitemap XML generators) require unit test coverage for content type headers, XML character escaping, element structure, and sorting logic to prevent regressions in search indexing and RSS feed syndication. | **Action:** Added `src/__tests__/rss-sitemap.test.ts` and updated `src/__tests__/mocks/astro-content.ts` to export `getCollection`, achieving 100% test coverage for `rss.xml.ts` and `sitemap.xml.ts`.
2 changes: 2 additions & 0 deletions src/__tests__/mocks/astro-content.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
import { vi } from 'vitest';
export const defineCollection = vi.fn((config) => config);
export const getCollection = vi.fn();
export const getEntry = vi.fn();
118 changes: 118 additions & 0 deletions src/__tests__/rss-sitemap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import * as astroContent from 'astro:content';
import { GET as getRss } from '../pages/rss.xml';
import { GET as getSitemap } from '../pages/sitemap.xml';

type GetContext = Parameters<typeof getRss>[0];

function createContext(url = 'https://me.alehar.workers.dev/rss.xml') {
return {
request: new Request(url),
locals: {},
} as unknown as GetContext;
}

const mockWorkEntries = [
{
id: 'older-project',
data: {
title: 'Older Project & Test <Tag>',
description: 'An older "project" description',
publishDate: new Date('2023-01-01T12:00:00Z'),
},
},
{
id: 'newer-project',
data: {
title: "Newer Project 'Rock'",
description: 'A > newer project',
publishDate: new Date('2024-06-15T12:00:00Z'),
},
},
{
id: 'lidkoping-stenhuggeri',
data: {
title: 'Lidköping Stenhuggeri',
description: 'Filtered project',
publishDate: new Date('2022-01-01T12:00:00Z'),
},
},
];

describe('RSS XML endpoint (src/pages/rss.xml.ts)', () => {
beforeEach(() => {
vi.restoreAllMocks();
});

it('generates valid RSS XML feed with properly escaped and sorted items', async () => {
vi.spyOn(astroContent, 'getCollection').mockResolvedValue(
mockWorkEntries as unknown as Awaited<ReturnType<typeof astroContent.getCollection>>,
);

const response = await getRss(createContext());

expect(response.status).toBe(200);
expect(response.headers.get('Content-Type')).toBe('application/rss+xml; charset=utf-8');

const xml = await response.text();

expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(xml).toContain('<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">');
expect(xml).toContain(
'<title>Alexander Härenstam | Product Manager, Developer Experience at IFS</title>',
);

// Check character escaping
expect(xml).toContain('Older Project &amp; Test &lt;Tag&gt;');
expect(xml).toContain('An older &quot;project&quot; description');
expect(xml).toContain('Newer Project &apos;Rock&apos;');
expect(xml).toContain('A &gt; newer project');

// Check item sorting (newer-project should appear before older-project in the XML output)
const indexNewer = xml.indexOf('newer-project');
const indexOlder = xml.indexOf('older-project');
expect(indexNewer).toBeGreaterThan(-1);
expect(indexOlder).toBeGreaterThan(-1);
expect(indexNewer).toBeLessThan(indexOlder);

// Filtered project shouldn't be included
expect(xml).not.toContain('lidkoping-stenhuggeri');
});
});

describe('Sitemap XML endpoint (src/pages/sitemap.xml.ts)', () => {
beforeEach(() => {
vi.restoreAllMocks();
});

it('generates valid Sitemap XML with static paths and work collection items', async () => {
vi.spyOn(astroContent, 'getCollection').mockResolvedValue(
mockWorkEntries as unknown as Awaited<ReturnType<typeof astroContent.getCollection>>,
);

const response = await getSitemap(createContext('https://me.alehar.workers.dev/sitemap.xml'));

expect(response.status).toBe(200);
expect(response.headers.get('Content-Type')).toBe('application/xml; charset=utf-8');

const xml = await response.text();

expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(xml).toContain('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');

// Static paths check
expect(xml).toContain('<loc>https://me.alehar.workers.dev/</loc>');
expect(xml).toContain('<loc>https://me.alehar.workers.dev/work/</loc>');
expect(xml).toContain('<loc>https://me.alehar.workers.dev/biography/</loc>');

// Dynamic work entry paths check
expect(xml).toContain('<loc>https://me.alehar.workers.dev/work/newer-project/</loc>');
expect(xml).toContain('<lastmod>2024-06-15</lastmod>');

expect(xml).toContain('<loc>https://me.alehar.workers.dev/work/older-project/</loc>');
expect(xml).toContain('<lastmod>2023-01-01</lastmod>');

// Filtered project check
expect(xml).not.toContain('lidkoping-stenhuggeri');
});
});
Loading