Files
alfred/tests/domain/release/test_parser_v2_scaffolding.py
T
francwa a2c917618f feat(release): scaffold v2 parser package (annotate-based pipeline)
New package alfred/domain/release/parser/ lays the foundation for the
release parser refactor (specs in memory). Exposes:

- Token: frozen VO carrying text + stream index + TokenRole + extra dict.
  with_role() returns a new instance (no mutation).
- TokenRole: str-backed enum split into structural (TITLE/YEAR/SEASON_EP/
  GROUP), technical (RESOLUTION/SOURCE/CODEC/AUDIO_*/BIT_DEPTH/HDR/
  EDITION/LANGUAGE), and meta (SITE_TAG/UNKNOWN) families.
- pipeline.strip_site_tag(): pulls a [site.tag] prefix or suffix.
- pipeline.tokenize(): release name -> list[Token] (all UNKNOWN),
  string-ops split on kb.separators (no regex, per CLAUDE.md).
- pipeline.annotate(): documented stub. Walk order recorded in docstring
  (group right-to-left, then season/episode, year, tech, title).

Legacy parse_release in release.services remains the live implementation
until the annotate step lands. Scaffolding tests verify Token API,
site-tag stripping (prefix/suffix), and tokenize output shape.

Refs: project_release_parser_v2_specs (memory)
2026-05-20 00:12:33 +02:00

80 lines
3.0 KiB
Python

"""Scaffolding tests for the v2 parser package.
These tests lock the **shape** of the new pipeline (token VOs, tokenize
output, site-tag stripping) before the annotate step is wired in. They
do not check parsed-release output yet — that comes once :func:`annotate`
is implemented and the fixtures-based suite switches over.
"""
from __future__ import annotations
from alfred.domain.release.parser import Token, TokenRole
from alfred.domain.release.parser.pipeline import strip_site_tag, tokenize
from alfred.infrastructure.knowledge.release_kb import YamlReleaseKnowledge
_KB = YamlReleaseKnowledge()
class TestToken:
def test_default_role_is_unknown(self) -> None:
t = Token(text="1080p", index=3)
assert t.role is TokenRole.UNKNOWN
assert not t.is_annotated
def test_with_role_returns_new_instance(self) -> None:
t = Token(text="1080p", index=3)
promoted = t.with_role(TokenRole.RESOLUTION)
assert promoted is not t
assert promoted.role is TokenRole.RESOLUTION
assert t.role is TokenRole.UNKNOWN # original unchanged (frozen)
def test_with_role_merges_extra(self) -> None:
t = Token(text="x265-KONTRAST", index=5)
promoted = t.with_role(TokenRole.CODEC, group="KONTRAST")
assert promoted.role is TokenRole.CODEC
assert promoted.extra == {"group": "KONTRAST"}
class TestStripSiteTag:
def test_no_tag(self) -> None:
clean, tag = strip_site_tag("The.Movie.2020.1080p-GRP")
assert tag is None
assert clean == "The.Movie.2020.1080p-GRP"
def test_suffix_tag(self) -> None:
clean, tag = strip_site_tag("Sinners.2025.1080p-[YTS.MX]")
assert tag == "YTS.MX"
assert clean == "Sinners.2025.1080p-"
def test_prefix_tag(self) -> None:
clean, tag = strip_site_tag("[ OxTorrent.vc ] The.Title.S01E01")
assert tag == "OxTorrent.vc"
assert clean == "The.Title.S01E01"
class TestTokenize:
def test_simple_release(self) -> None:
tokens, tag = tokenize("Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST", _KB)
assert tag is None
texts = [t.text for t in tokens]
# Dash is not a separator, so x265-KONTRAST stays glued.
assert texts == [
"Back", "in", "Action", "2025", "1080p", "WEBRip", "x265-KONTRAST",
]
def test_all_tokens_start_unknown(self) -> None:
tokens, _ = tokenize("Back.in.Action.2025.1080p.WEBRip.x265-KONTRAST", _KB)
assert all(t.role is TokenRole.UNKNOWN for t in tokens)
def test_indexes_are_contiguous(self) -> None:
tokens, _ = tokenize("A.B.C.D", _KB)
assert [t.index for t in tokens] == [0, 1, 2, 3]
def test_strips_site_tag_before_tokenize(self) -> None:
tokens, tag = tokenize(
"Sinners.2025.1080p.WEBRip.x265.10bit.AAC5.1-[YTS.MX]", _KB
)
assert tag == "YTS.MX"
# Site tag substring must not appear among tokens.
assert not any("YTS" in t.text for t in tokens)