# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The shape of a pack: where it lives, and what a typed name can mean.""" from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING import pytest from signal_bot.pack import ( MAX_NAMES, candidate_keysets, default_dirs, media_type, options_for, ) from signal_bot.pack_load import load from tests import pack_fixture if TYPE_CHECKING: from signal_bot.pack import Entry, Pack def _entry(pack: Pack, key: str) -> Entry: return next(entry for entry in pack.entries if entry.key == key) @pytest.fixture def pack(tmp_path: Path) -> Pack: return load(pack_fixture.build(tmp_path / "pack")) def test_a_media_type_comes_from_the_suffix_case_insensitively() -> None: assert media_type("a.JPG") == "image/jpeg" assert media_type("clip.mp4") == "video/mp4" def test_matching_wants_every_key_not_any_of_them(pack: Pack) -> None: assert pack.matching(frozenset({"p1"})) == ("alfa.png", "both.png", "clip.mp4") assert pack.matching(frozenset({"p1", "p2"})) == ("both.png",) assert pack.matching(frozenset({"p2", "p3"})) == () def test_media_path_is_inside_the_album(pack: Pack) -> None: assert pack.media_path("alfa.png") == pack.directory / "media" / "alfa.png" def test_an_explicit_directory_is_taken_at_its_word(monkeypatch: pytest.MonkeyPatch) -> None: """And is reported as explicit, which is what makes its absence an error.""" monkeypatch.setenv("PHOTO_PACK_DIR", "/srv/album") assert default_dirs() == ((Path("/srv/album"), True),) def test_several_albums_can_be_named_at_once(monkeypatch: pytest.MonkeyPatch) -> None: """One bot, several albums, each carrying its own group scope. Comma-separated, the same spelling GROUP_IDS and TRUSTED_INVITERS use. """ monkeypatch.setenv("PHOTO_PACK_DIR", "/srv/album, /srv/testalbum ") assert default_dirs() == ((Path("/srv/album"), True), (Path("/srv/testalbum"), True)) def test_blank_entries_in_the_list_are_dropped(monkeypatch: pytest.MonkeyPatch) -> None: """A trailing comma is a typo, not a request to load the current directory.""" monkeypatch.setenv("PHOTO_PACK_DIR", "/srv/album,,") assert default_dirs() == ((Path("/srv/album"), True),) def test_systemd_beats_anything_under_home(monkeypatch: pytest.MonkeyPatch) -> None: """ProtectHome=read-only is why: StateDirectory is exposed regardless.""" monkeypatch.setenv("STATE_DIRECTORY", "/var/lib/signal-bot") assert default_dirs() == ((Path("/var/lib/signal-bot/pack"), False),) def test_the_xdg_fallback_needs_no_environment(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("XDG_DATA_HOME", raising=False) monkeypatch.setattr(Path, "home", lambda: Path("/home/somebody")) assert default_dirs() == ((Path("/home/somebody/.local/share/signal-bot/pack"), False),) def test_a_name_resolves_with_or_without_the_bang(pack: Pack) -> None: """ "!alfa beta" is how people write it; the second half has no "!".""" assert options_for(pack, "beta") == (frozenset({"p2"}),) assert options_for(pack, "!beta") == (frozenset({"p2"}),) def test_a_name_resolves_through_the_same_folding_as_a_command(pack: Pack) -> None: """The diacritic spelling is a separate alias and folds onto the same key.""" assert options_for(pack, "gammÄ…") == (frozenset({"p3"}),) def test_a_word_that_is_not_a_name_resolves_to_nothing(pack: Pack) -> None: assert options_for(pack, "kto") is None def test_one_trigger_alone_asks_for_that_person(pack: Pack) -> None: assert candidate_keysets(pack, _entry(pack, "p1"), ()) == (frozenset({"p1"}),) def test_a_tail_of_names_is_intersected(pack: Pack) -> None: """Which is the whole feature: "!alfa beta" means a photo with both.""" assert candidate_keysets(pack, _entry(pack, "p1"), ["beta"]) == (frozenset({"p1", "p2"}),) def test_an_all_of_combo_is_one_set_of_several_people(pack: Pack) -> None: assert candidate_keysets(pack, _entry(pack, "c1"), ()) == (frozenset({"p1", "p2"}),) def test_an_any_of_combo_offers_each_reading_separately(pack: Pack) -> None: """So the handler can prefer one that actually has a photo.""" assert candidate_keysets(pack, _entry(pack, "c2"), ()) == ( frozenset({"p2"}), frozenset({"p3"}), ) def test_a_choice_multiplies_out_with_the_rest_of_the_line(pack: Pack) -> None: assert candidate_keysets(pack, _entry(pack, "c2"), ["alfa"]) == ( frozenset({"p2", "p1"}), frozenset({"p3", "p1"}), ) def test_an_unknown_word_in_the_tail_is_not_silently_dropped(pack: Pack) -> None: """Dropping it would answer a different question and look like bad tags.""" assert candidate_keysets(pack, _entry(pack, "p1"), ["beta", "nikt"]) is None def test_too_many_names_is_refused_rather_than_multiplied_out(pack: Pack) -> None: assert candidate_keysets(pack, _entry(pack, "p1"), ["beta"] * MAX_NAMES) is None