From 29294142c5a82324fc116d5d23231daebb7c6330 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 10:23:59 +0000 Subject: [PATCH] fix: read the singular "1 star today" from GitHub trending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The growth regex required the plural "stars", so a repo that gained exactly one star fell through to the zero fallback — ranked as if it had grown none, rendered as "(+0 today)", and stored that way. Closes #36 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ab5JNnyCu9dcG9erRXtTWs --- src/tools/trending-scrape.test.ts | 54 +++++++++++++++++++++++++++++++ src/tools/trending-scrape.tool.ts | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/tools/trending-scrape.test.ts diff --git a/src/tools/trending-scrape.test.ts b/src/tools/trending-scrape.test.ts new file mode 100644 index 0000000..728a6e5 --- /dev/null +++ b/src/tools/trending-scrape.test.ts @@ -0,0 +1,54 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { trendingScrapeTool } from './trending-scrape.tool.ts' +import type { TrendingRepo } from '../schemas/index.ts' + +// Trimmed to the fragments the parser keys off, copied from a live +// https://github.com/trending/javascript?since=daily response. +const article = (name: string, stars: string, today: string) => ` +` + +async function scrape(html: string): Promise { + const real = globalThis.fetch + + globalThis.fetch = (async () => new Response(html, { status: 200 })) as typeof fetch + + try { + return JSON.parse(await trendingScrapeTool.invoke({ languages: ['javascript'] })) as TrendingRepo[] + } finally { + globalThis.fetch = real + } +} + +test('reads the singular "1 star today" GitHub renders, not just the plural form', async () => { + const repos = await scrape(article('OWASP/threat-dragon', '1,554', '1 star today') + article('addyosmani/agent-skills', '2,100', '226 stars today')) + + assert.equal(repos[0].todayStars, 1, 'a repo that gained one star must not be recorded as gaining none') + assert.equal(repos[1].todayStars, 226) +}) + +test('still parses the plural form, thousands separator and all', async () => { + const repos = await scrape(article('TencentCloud/TencentDB-Agent-Memory', '15,584', '1,892 stars today')) + + assert.equal(repos[0].todayStars, 1892) + assert.equal(repos[0].stars, 15584) +}) + +test('falls back to zero only when the growth line is genuinely absent', async () => { + const repos = await scrape(article('foo/bar', '10', '')) + + assert.equal(repos[0].todayStars, 0) + assert.equal(repos[0].name, 'foo/bar') +}) diff --git a/src/tools/trending-scrape.tool.ts b/src/tools/trending-scrape.tool.ts index e8ac97f..a9ae484 100644 --- a/src/tools/trending-scrape.tool.ts +++ b/src/tools/trending-scrape.tool.ts @@ -29,7 +29,7 @@ function parseTrendingHtml(html: string): TrendingRepo[] { const stars = starsMatch ? parseInt(starsMatch[1].replace(/,/g, ''), 10) : 0 // Stars today - const todayMatch = block.match(/([\d,]+)\s+stars\s+today/) + const todayMatch = block.match(/([\d,]+)\s+stars?\s+today/) const todayStars = todayMatch ? parseInt(todayMatch[1].replace(/,/g, ''), 10) : 0 repos.push({