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
59 changes: 59 additions & 0 deletions src/__tests__/releases-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { GET } from '../pages/api/releases';
import * as githubReleases from '../utils/github-releases';
import { LATEST_RELEASE_SNAPSHOT } from '../data/latest-release';
import { toVisitorRelease } from '../utils/visitor-changelog';

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

function createContext(request = new Request('https://example.com/api/releases')) {
return {
request,
locals: {},
} as unknown as GetContext;
}

const mockRelease: githubReleases.SiteRelease = {
body: '- 41fe7ae feat: rewrite /experimental/now/ for visitors (#523)',
publishedAt: '2026-08-16T21:12:02Z',
title: '2026.08.16.2111',
url: 'https://github.com/alehar9320/astro-cloudflare-personal-website/releases/tag/2026.08.16.2111',
version: '2026.08.16.2111',
};

describe('releases API route', () => {
beforeEach(() => {
vi.restoreAllMocks();
});

it('returns formatted visitor releases from GitHub', async () => {
vi.spyOn(githubReleases, 'fetchGitHubReleases').mockResolvedValue([mockRelease]);

const response = await GET(createContext());
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('application/json');

const json = await response.json();
expect(json).toEqual([toVisitorRelease(mockRelease)]);
});

it('falls back to LATEST_RELEASE_SNAPSHOT if GitHub releases are empty', async () => {
vi.spyOn(githubReleases, 'fetchGitHubReleases').mockResolvedValue([]);

const response = await GET(createContext());
expect(response.status).toBe(200);

const json = await response.json();
expect(json).toEqual([toVisitorRelease(LATEST_RELEASE_SNAPSHOT)]);
});

it('handles errors gracefully by falling back to LATEST_RELEASE_SNAPSHOT', async () => {
vi.spyOn(githubReleases, 'fetchGitHubReleases').mockRejectedValue(new Error('GitHub error'));

const response = await GET(createContext());
expect(response.status).toBe(200);

const json = await response.json();
expect(json).toEqual([toVisitorRelease(LATEST_RELEASE_SNAPSHOT)]);
});
});
3 changes: 2 additions & 1 deletion src/pages/api/release-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ async function summarize(request: Request) {
}

if (!latest) {
const releases = await fetchGitHubReleases();
const token = bindings.GITHUB_TOKEN?.trim();
const releases = await fetchGitHubReleases(fetch, undefined, token ? { token } : undefined);
latest = releases[0] ?? null;
if (latest) source = 'github';
}
Expand Down
19 changes: 4 additions & 15 deletions src/pages/api/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { APIRoute } from 'astro';
import { env } from 'cloudflare:workers';
import { LATEST_RELEASE_SNAPSHOT } from '../../data/latest-release';
import { fetchGitHubReleases, type SiteRelease } from '../../utils/github-releases';
import { toVisitorChangelogTitle, toVisitorRelease } from '../../utils/visitor-changelog';
import { toVisitorRelease } from '../../utils/visitor-changelog';

// toVisitorRelease formats each release using toVisitorChangelogTitle

const jsonHeaders = {
'content-type': 'application/json',
Expand All @@ -29,20 +31,7 @@ function readEnv(): ReleasesEnv {
}

function visitorReleases(releases: SiteRelease[]) {
return releases.map((release) => {
const visitor = toVisitorRelease(release);
return {
...visitor,
body: visitor.body
.split('\n')
.map((line) => {
const bullet = line.match(/^([-*+]\s+)(.*)$/);
if (!bullet) return toVisitorChangelogTitle(line);
return `${bullet[1]}${toVisitorChangelogTitle(bullet[2])}`;
})
.join('\n'),
};
});
return releases.map(toVisitorRelease);
}

export const GET: APIRoute = async () => {
Expand Down
12 changes: 12 additions & 0 deletions src/utils/chat-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,17 @@ describe('chat logic utilities', () => {
expect(pruned).toHaveLength(1);
expect(pruned[0].content).toBe('short');
});

it('drops leading assistant messages so pruned history begins with a user turn', () => {
const longContent = 'x'.repeat(MAX_TOTAL_CONTENT_LENGTH - 20);
const messages: ChatMessage[] = [
{ role: 'user', content: longContent },
{ role: 'assistant', content: 'Middle assistant reply' },
{ role: 'user', content: 'Final user question' },
];
const pruned = pruneMessages(messages);
expect(pruned[0].role).toBe('user');
expect(pruned[0].content).toBe('Final user question');
});
});
});
5 changes: 5 additions & 0 deletions src/utils/chat-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export function pruneMessages(messages: ChatMessage[]): ChatMessage[] {
totalLength -= pruned.shift()!.content.length;
}

while (pruned.length > 1 && pruned[0].role === 'assistant') {
// biome-ignore lint/style/noNonNullAssertion: loop guard ensures shift() returns an element
totalLength -= pruned.shift()!.content.length;
}

return pruned;
}

Expand Down
Loading