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
31 changes: 31 additions & 0 deletions src/__tests__/china-sources.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it, vi } from "vitest";

const adapterMocks = vi.hoisted(() => ({
kr36: vi.fn().mockRejectedValue(new Error("kr36 failed")),
infoqCn: vi.fn().mockRejectedValue(new Error("infoq failed")),
gitee: vi.fn().mockRejectedValue(new Error("gitee failed")),
oschina: vi.fn().mockRejectedValue(new Error("oschina failed")),
juejin: vi.fn().mockRejectedValue(new Error("juejin failed")),
}));

vi.mock("../kr36.ts", () => ({ fetchKr36Data: adapterMocks.kr36 }));
vi.mock("../infoq-cn.ts", () => ({ fetchInfoqCnData: adapterMocks.infoqCn }));
vi.mock("../gitee.ts", () => ({ fetchGiteeData: adapterMocks.gitee }));
vi.mock("../oschina.ts", () => ({ fetchOschinaData: adapterMocks.oschina }));
vi.mock("../juejin.ts", () => ({ fetchJuejinData: adapterMocks.juejin }));

import { fetchChinaSourcesData } from "../china-sources.ts";

describe("fetchChinaSourcesData", () => {
it("returns structured error statuses when adapters reject", async () => {
const result = await fetchChinaSourcesData();

expect(Object.values(result).map(({ status }) => status.state)).toEqual([
"error",
"error",
"error",
"error",
"error",
]);
});
});
41 changes: 39 additions & 2 deletions src/__tests__/generate-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, it, expect } from "vitest";
import { toRfc822, escapeXml, getReportFiles } from "../generate-manifest.ts";
import fs from "node:fs";
import { afterEach, describe, it, expect, vi } from "vitest";
import { toRfc822, escapeXml, getReportFiles, generateSearchIndex } from "../generate-manifest.ts";

afterEach(() => {
vi.restoreAllMocks();
});

// ---------------------------------------------------------------------------
// toRfc822
Expand Down Expand Up @@ -79,3 +84,35 @@ describe("getReportFiles", () => {
expect(reports).toContain("ai-weekly-en");
});
});

describe("generateSearchIndex", () => {
it("indexes topic-pool candidates and legacy topics", () => {
vi.spyOn(fs, "existsSync").mockImplementation((file) => String(file).endsWith("topic-pool.json"));
vi.spyOn(fs, "readFileSync").mockImplementation((file) => {
const text = String(file);
if (text.includes("2026-06-01")) {
return JSON.stringify({
candidates: [
{ title: "New candidate", score: 88, category: "AI 产品与用户入口", source: "Dev.to" },
],
});
}
return JSON.stringify({
topics: [{ topic: "Legacy topic", score: 70, category: "模型与技术突破", evidence: ["来源:ArXiv"] }],
});
});
const writeSpy = vi.spyOn(fs, "writeFileSync").mockImplementation(() => undefined);

generateSearchIndex([
{ date: "2026-06-01", reports: ["ai-topic-radar"] },
{ date: "2026-05-31", reports: ["ai-topic-radar"] },
]);

const [, content] = writeSpy.mock.calls.find(([file]) => String(file).endsWith("search-index.json"))!;
const index = JSON.parse(String(content));
expect(index.topics).toEqual([
expect.objectContaining({ title: "New candidate", source: "Dev.to" }),
expect.objectContaining({ title: "Legacy topic", source: "来源:ArXiv" }),
]);
});
});
25 changes: 23 additions & 2 deletions src/__tests__/prompt-builders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { RepoDigest } from "../prompts.ts";
import type { TrendingData } from "../trending.ts";
import type { HnData } from "../hn.ts";
import type { WebFetchResult } from "../web.ts";
import type { SourceState, SourceStatus } from "../source-status.ts";

// ---------------------------------------------------------------------------
// Fixtures
Expand Down Expand Up @@ -189,6 +190,7 @@ describe("buildTrendingPrompt", () => {
],
searchRepos: [],
trendingFetchSuccess: true,
status: status("github-trending"),
};
const result = buildTrendingPrompt(data, "2026-03-09");
expect(result).toContain("org/repo");
Expand All @@ -198,7 +200,12 @@ describe("buildTrendingPrompt", () => {
});

it("shows fetch failure message when trending fails", () => {
const data: TrendingData = { trendingRepos: [], searchRepos: [], trendingFetchSuccess: false };
const data: TrendingData = {
trendingRepos: [],
searchRepos: [],
trendingFetchSuccess: false,
status: status("github-trending", "error"),
};
const result = buildTrendingPrompt(data, "2026-03-09");
expect(result).toContain("未能抓取");
});
Expand All @@ -218,6 +225,7 @@ describe("buildTrendingPrompt", () => {
},
],
trendingFetchSuccess: false,
status: status("github-trending", "error"),
};
const result = buildTrendingPrompt(data, "2026-03-09");
expect(result).toContain("[topic:ai-agent]");
Expand Down Expand Up @@ -247,6 +255,7 @@ describe("buildWebReportPrompt", () => {
},
],
totalDiscovered: 50,
status: status("web-anthropic"),
},
];
const result = buildWebReportPrompt(results, "2026-03-09");
Expand All @@ -257,7 +266,14 @@ describe("buildWebReportPrompt", () => {

it("shows incremental mode for non-first-run", () => {
const results: WebFetchResult[] = [
{ site: "openai", siteName: "OpenAI", isFirstRun: false, newItems: [], totalDiscovered: 100 },
{
site: "openai",
siteName: "OpenAI",
isFirstRun: false,
newItems: [],
totalDiscovered: 100,
status: status("web-openai", "empty"),
},
];
const result = buildWebReportPrompt(results, "2026-03-09");
expect(result).toContain("增量更新");
Expand Down Expand Up @@ -352,6 +368,7 @@ describe("buildHnPrompt", () => {
},
],
fetchSuccess: true,
status: status("hn"),
};
const result = buildHnPrompt(data, "2026-03-09");
expect(result).toContain("AI News");
Expand All @@ -376,10 +393,14 @@ describe("buildHnPrompt", () => {
},
],
fetchSuccess: true,
status: status("hn"),
};
const result = buildHnPrompt(data, "2026-03-09", "en");
expect(result).toContain("Score: 10");
expect(result).toContain("Comments: 2");
expect(result).toContain("Hacker News");
});
});
function status(id: string, state: SourceState = "ok"): SourceStatus {
return { id, label: id, state, fetchedCount: 0, acceptedCount: 0 };
}
125 changes: 125 additions & 0 deletions src/__tests__/report-savers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChinaSourcesData } from "../china-sources.ts";

const { callLlmMock } = vi.hoisted(() => ({
callLlmMock: vi.fn(),
}));

vi.mock("../report.ts", () => ({
callLlm: callLlmMock,
saveFile: vi.fn(),
LLM_TOKENS_WEB: 4096,
}));

vi.mock("../github.ts", () => ({
createGitHubIssue: vi.fn(),
}));

import { hasChinaSourcesData } from "../china-sources.ts";
import {
saveArxivReport,
saveChinaTechReport,
saveHfReport,
saveHnReport,
savePhReport,
} from "../report-savers.ts";

function emptyChinaSources(): ChinaSourcesData {
return {
kr36: { articles: [], fetchSuccess: false, status: status("kr36") },
infoqCn: {
articles: [],
fetchSuccess: true,
status: {
id: "infoq-cn",
label: "InfoQ 中国",
state: "empty",
fetchedCount: 0,
acceptedCount: 0,
},
},
gitee: {
projects: [],
fetchSuccess: true,
status: {
id: "gitee",
label: "Gitee",
state: "empty",
fetchedCount: 0,
acceptedCount: 0,
},
},
oschina: { news: [], fetchSuccess: false, status: status("oschina") },
juejin: {
articles: [],
fetchSuccess: true,
status: {
id: "juejin",
label: "掘金",
state: "empty",
fetchedCount: 0,
acceptedCount: 0,
},
},
};
}

beforeEach(() => {
callLlmMock.mockReset();
});

describe("saveArxivReport", () => {
it("skips LLM generation when a successful fetch contains no papers", async () => {
await saveArxivReport(
{
papers: [],
fetchSuccess: true,
status: {
id: "arxiv",
label: "ArXiv",
state: "empty",
fetchedCount: 0,
acceptedCount: 0,
},
},
"2026-06-01 00:00",
"2026-06-01",
"",
"",
);

expect(callLlmMock).not.toHaveBeenCalled();
});
});

describe("empty source reports", () => {
it("skips HN, Product Hunt, and Hugging Face LLM generation when successful fetches are empty", async () => {
await saveHnReport({ stories: [], fetchSuccess: true, status: { ...status("hn") } }, "", "", "", "");
await savePhReport(
{ products: [], fetchSuccess: true, status: { ...status("product-hunt") } },
"",
"",
"",
"",
);
await saveHfReport({ models: [], fetchSuccess: true, status: { ...status("hf") } }, "", "", "", "");

expect(callLlmMock).not.toHaveBeenCalled();
});
});

describe("China sources empty results", () => {
it("does not treat successful empty fetches as China source content", () => {
expect(hasChinaSourcesData(emptyChinaSources())).toBe(false);
});

it("skips China tech LLM generation when all sources contain no entries", async () => {
await saveChinaTechReport(emptyChinaSources(), "2026-06-01 00:00", "2026-06-01", "", "");

expect(callLlmMock).not.toHaveBeenCalled();
});
});

function status(id: string) {
return { id, label: id, state: "empty" as const, fetchedCount: 0, acceptedCount: 0 };
}
Loading
Loading