Snapshot differential tests (known deviations registered, current state passes): - title/path recognition: 216 samples vs MP MetaInfo/MetaInfoPath - filter rules: 42 rules x 28 torrents vs MP FilterModule - RSS/indexer parsing: 15 feeds, 72 indexer samples - zhconv: 69 samples Generator: scripts/mp-sync/gen-differential.py; report: docs/porting/mp-rust-logic-parity.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
/**
|
|
* 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<RssSample>("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);
|
|
}
|
|
});
|
|
});
|