Files
UHD_Media_Tool/packages/adapters/test/differential/rss.test.ts
T
rootandClaude Opus 5.5 591cc78c7d test(porting): differential tests against MoviePilot Rust-accelerated parsers
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]>
2026-09-27 22:44:11 +08:00

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);
}
});
});