# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """custom.json: labels the album learned from the chat, and what refuses one.""" from __future__ import annotations import json from typing import TYPE_CHECKING import pytest from signal_bot.groups import ConfigError from signal_bot.pack_custom import CUSTOM_FILE, CustomTags, load, write if TYPE_CHECKING: from pathlib import Path NO_TRIGGERS: frozenset[str] = frozenset() def _write(directory: Path, document: object) -> Path: directory.mkdir(parents=True, exist_ok=True) (directory / CUSTOM_FILE).write_text(json.dumps(document), encoding="utf-8") return directory def test_an_album_with_no_custom_file_has_no_custom_tags(tmp_path: Path) -> None: """The normal state of an album nobody has run !add_photo on.""" tags = load(tmp_path, NO_TRIGGERS) assert tags.keys == frozenset() assert tags.resolve("anything") is None def test_labels_resolve_to_their_key(tmp_path: Path) -> None: tags = load(_write(tmp_path, {"t1": ["wakacje"], "t2": ["gory", "gorki"]}), NO_TRIGGERS) assert tags.resolve("wakacje") == "t1" assert tags.resolve("gorki") == "t2" assert tags.keys == {"t1", "t2"} def test_a_label_resolves_through_folding(tmp_path: Path) -> None: """The same folding a trigger gets, so a Cyrillic "o" still resolves.""" tags = load(_write(tmp_path, {"t1": ["Wakacje"]}), NO_TRIGGERS) assert tags.resolve("WAKACJE") == "t1" assert tags.resolve("wakacje") == "t1" def test_the_next_key_is_one_past_the_highest_in_use() -> None: """Not the count: deleting t2 by hand must not make a mint reuse it. Reusing it would silently inherit every photo the old t2 was tagged with. """ tags = CustomTags(by_key={"t1": ("a",), "t5": ("b",)}, by_label={"a": "t1", "b": "t5"}) assert tags.next_key() == "t6" assert CustomTags().next_key() == "t1" def test_a_key_that_is_not_a_pseudonym_fails_the_load(tmp_path: Path) -> None: """The key reaches the log and tags.json, so it may not look like a name.""" with pytest.raises(ConfigError, match="must be 't' followed by a number"): load(_write(tmp_path, {"wakacje": ["x"]}), NO_TRIGGERS) @pytest.mark.parametrize("document", [{"t1": []}, {"t1": "wakacje"}, {"t1": None}]) def test_an_entry_without_labels_fails_the_load(tmp_path: Path, document: object) -> None: with pytest.raises(ConfigError, match="non-empty list of labels"): load(_write(tmp_path, document), NO_TRIGGERS) def test_a_label_that_folds_away_to_nothing_fails_the_load(tmp_path: Path) -> None: with pytest.raises(ConfigError, match="folds away to nothing"): load(_write(tmp_path, {"t1": ["!!!"]}), NO_TRIGGERS) def test_a_label_that_shadows_a_person_fails_the_load(tmp_path: Path) -> None: """Otherwise the label quietly hides the command that person is reached by.""" with pytest.raises(ConfigError, match="already a person's trigger"): load(_write(tmp_path, {"t1": ["alfa"]}), frozenset({"!alfa"})) def test_two_keys_claiming_one_label_fails_the_load(tmp_path: Path) -> None: """!random would otherwise pick between them arbitrarily.""" with pytest.raises(ConfigError, match="is claimed by"): load(_write(tmp_path, {"t1": ["wakacje"], "t2": ["wakacje"]}), NO_TRIGGERS) def test_malformed_json_fails_the_load(tmp_path: Path) -> None: (tmp_path / CUSTOM_FILE).write_text("{not json", encoding="utf-8") with pytest.raises(ConfigError, match="not valid JSON"): load(tmp_path, NO_TRIGGERS) def test_a_document_that_is_not_an_object_fails_the_load(tmp_path: Path) -> None: with pytest.raises(ConfigError, match="must be an object"): load(_write(tmp_path, ["t1"]), NO_TRIGGERS) def test_an_unreadable_file_fails_the_load(tmp_path: Path) -> None: """A directory where the manifest should be: read_text raises OSError.""" (tmp_path / CUSTOM_FILE).mkdir(parents=True) with pytest.raises(ConfigError, match="cannot be read"): load(tmp_path, NO_TRIGGERS) def test_writing_then_loading_round_trips(tmp_path: Path) -> None: tags = CustomTags().with_label("t1", "wakacje").with_label("t1", "wakacje2019") write(tmp_path, tags) again = load(tmp_path, NO_TRIGGERS) assert again.by_key == {"t1": ("wakacje", "wakacje2019")} assert again.resolve("wakacje2019") == "t1" def test_the_written_file_is_not_world_readable(tmp_path: Path) -> None: write(tmp_path, CustomTags().with_label("t1", "wakacje")) assert (tmp_path / CUSTOM_FILE).stat().st_mode & 0o077 == 0 def test_with_label_does_not_mutate_the_original() -> None: """The pending mint must be discardable, which needs a value type.""" original = CustomTags() extended = original.with_label("t1", "wakacje") assert original.keys == frozenset() assert extended.keys == {"t1"}