Files
UHD_Media_Tool/packages/domain/test/differential/naming.test.ts
T
rootandClaude Opus 5.5 f7c75ad781 feat(domain): naming template engine (Jinja safe subset, MP variables, path layout)
- naming/engine: lexer/parser/AST/render for {{ }} / if-elif-else / whitelisted filters
  (default pad pad2 upper lower trim replace truncate date join); rejects for/set/attribute/
  subscript/call/~/% with line:col errors and 'did you mean' hints
- naming/variables: variable table aligned with MoviePilot names plus ours (cut, airDate,
  issue, slotName, libraryRoot ...); buildNamingVars from parsed release + entity + units
- naming/layout: split by literal '/', segment roles (category/root/season/sub/file),
  value escaping, reserved names, 255-byte segments, validateNamingTemplate
- naming/defaults: MP default movie/tv + our variety template, v1 syntax upgrade,
  entity > root > global override resolution, builtin preview samples
- path/binding: resolveTargetDirectory keeps bound dirs, only unbound segments render;
  renderMediaFileName = last segment of the full template; DEFAULT_TEMPLATES = MP v2
- differential: 34 cases vs MP (21 identical, 13 documented deviations)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 15:24:32 +08:00

63 lines
2.4 KiB
TypeScript

/**
* 命名模板差分:MP(MetaInfo + TemplateContextBuilder + jinja2.Template)vs 我们的 parseRelease + buildNamingVars + renderNamingPath。
* MP 输出由 gen-naming-mp.py 生成(naming-mp.json)。没有登记 deviation 的样例必须与 MP 逐字一致;
* 登记了的必须确实不同(差异消除后删掉登记)。说明见 docs/runbook/naming.md。
*/
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
buildNamingVars,
MP_DEFAULT_NAMING,
type NamingEntityInput,
type NamingKind,
parseRelease,
renderNamingPath,
validateNamingTemplate,
} from "../../src/index.ts";
interface Case {
id: string;
kind: NamingKind;
fileName: string;
entity: NamingEntityInput;
template?: string;
vars?: Record<string, string>;
expect?: string;
expectError?: string;
deviation?: { kind: "intentional" | "ours" | "representation"; note: string };
}
const dir = new URL(".", import.meta.url);
const { cases } = JSON.parse(readFileSync(new URL("naming-cases.json", dir), "utf8")) as { cases: Case[] };
const { mp } = JSON.parse(readFileSync(new URL("naming-mp.json", dir), "utf8")) as {
mp: Record<string, { path?: string; error?: string }>;
};
function ours(c: Case): { path?: string; error?: string } {
const template = c.template ?? MP_DEFAULT_NAMING[c.kind];
const v = validateNamingTemplate(template, c.kind);
if (!v.ok) return { error: v.errors[0]!.code };
const ext = c.fileName.slice(c.fileName.lastIndexOf("."));
const vars = { ...buildNamingVars(c.entity, parseRelease(c.fileName), { fileExt: ext }), ...c.vars };
const r = renderNamingPath(template, vars);
return r.ok ? { path: r.relativePath } : { error: r.errors[0]!.code };
}
describe("命名模板差分(MP ↔ naming 引擎)", () => {
it("样例 ≥ 25 且每条都有 MP 输出", () => {
expect(cases.length).toBeGreaterThanOrEqual(25);
for (const c of cases) expect(mp[c.id], c.id).toBeDefined();
});
for (const c of cases) {
it(`${c.id} ${c.template ?? `默认 ${c.kind}`} ← ${c.fileName}`, () => {
const got = ours(c);
if (c.expectError) expect(got).toEqual({ error: c.expectError });
else expect(got).toEqual({ path: c.expect });
const m = mp[c.id]!;
if (c.deviation) expect(m.path, `${c.id} 登记为偏离但与 MP 一致,删掉 deviation`).not.toBe(got.path);
else expect(m.path, `${c.id} 与 MP 不一致`).toBe(got.path);
});
}
});