- 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]>
63 lines
2.4 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
});
|