/** * RSS 解析差分:MP `RssHelper._parse_impl`(lxml 回退)vs 本项目 `parseFeed`。 * 期望由 scripts/mp-sync/gen-differential.py rss|indexer 生成(rss.json);不一致必须登记在 rss.json `deviations`。 */ import { describe, expect, it } from "vitest"; import { FeedError, type FeedItem, parseFeed } from "../../src/rss/index.ts"; import { diffAgainstRegistry, dumpMismatches, FieldCollector, formatMismatches, loadDoc, readInput, } from "./diff-util.ts"; interface MpPubdate { kind: "absent" | "unparsed" | "aware" | "naive"; text?: string; wall?: string; utc?: string; } interface MpRssItem { title: string; description: string; link: string; enclosure: string; size: number; nickname?: string; pubdate: MpPubdate; } interface RssSample { id: string; input: { file: string; sha256: string }; mp: { status: "ok" | "false_error" | "none_expired"; items: MpRssItem[] }; } const doc = loadDoc("rss.json"); /** MP 字段 → 可与本项目比较的值:空串即缺省;pubdate 统一为 UTC ISO */ function mpPubdate(p: MpPubdate): string | undefined { if (p.kind === "aware") return p.utc; // naive:MP astimezone(None) 以宿主本地时区解释墙上时间;JS 对无偏移的日期时间同样按本地解释 if (p.kind === "naive") return new Date(p.wall!).toISOString(); return undefined; } const blank = (s: string | null | undefined) => (s === "" || s == null ? undefined : s); function runOurs(xml: string): { status: string; items: FeedItem[] } { try { return { status: "ok", items: parseFeed(xml, { fetchedAt: new Date("2026-09-27T00:00:00Z") }).items, }; } catch (e) { if (e instanceof FeedError) return { status: `error:${e.kind}`, items: [] }; throw e; } } function statusEquivalent(mp: RssSample["mp"]["status"], ours: string): boolean { if (mp === "ok") return ours === "ok"; if (mp === "none_expired") return ours === "error:expired"; return ours.startsWith("error:") && ours !== "error:expired"; } describe("RSS 差分(MP RssHelper Python 回退)", () => { const c = new FieldCollector(); for (const s of doc.samples) { const ours = runOurs(readInput(s.input.file)); if (!statusEquivalent(s.mp.status, ours.status)) { c.mismatches.push({ key: `${s.id}.status`, mp: s.mp.status, ours: ours.status, }); } if (s.mp.status !== "ok" || ours.status !== "ok") continue; if (s.mp.items.length !== ours.items.length) { c.mismatches.push({ key: `${s.id}.count`, mp: s.mp.items.length, ours: ours.items.length, }); } const n = Math.min(s.mp.items.length, ours.items.length); for (let i = 0; i < n; i++) { const m = s.mp.items[i]!; const o = ours.items[i]!; const k = (f: string) => `${s.id}#${i}.${f}`; c.check(k("title"), m.title, o.title); c.check(k("description"), blank(m.description), blank(o.description)); c.check(k("link"), blank(m.link), blank(o.link)); c.check(k("enclosure"), blank(m.enclosure), blank(o.enclosureUrl)); c.check(k("size"), m.size, o.size); c.check(k("pubdate"), mpPubdate(m.pubdate), o.publishedAt); c.check(k("nickname"), blank(m.nickname), blank(o.nickname)); } } it("有样本且记录了生成来源", () => { expect(doc.samples.length).toBeGreaterThan(10); expect(doc.generatedFrom.commit).toMatch(/^[0-9a-f]{40}$/); }); it("实际不一致集合 == 登记的 deviations", () => { dumpMismatches("rss", c.mismatches, { compared: c.compared, equal: c.equal, }); const { unregistered, stale } = diffAgainstRegistry(c.mismatches, doc.deviations); expect(formatMismatches(unregistered), "未登记的不一致").toBe(""); expect(stale.join("\n"), "登记了但已不再出现的偏差(请从 rss.json deviations 删除)").toBe(""); }); it("登记项分类合法", () => { for (const [key, d] of Object.entries(doc.deviations)) { expect(["intentional", "missing", "our_bug", "mp_bug"], key).toContain(d.kind); expect(d.note.length, key).toBeGreaterThan(0); } }); });