# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Importing a folder: the two-stage dedup, and the arithmetic that proves it.""" from __future__ import annotations from typing import TYPE_CHECKING from signal_bot import pack_ledger from signal_bot.media_sniff import MediaKind from signal_bot.pack import MEDIA_DIR from signal_bot.pack_custom import FILE_MODE from signal_bot.pack_import import plan_import, run_import from signal_bot.pack_report import SKIPPED_DIR, Outcome from tests.pack_fixture import CLIP, PIXEL if TYPE_CHECKING: from pathlib import Path # A second distinct PNG, so a test can have two files that are genuinely # different rather than two names for one. OTHER = PIXEL.replace(b"IDAT", b"IDAT", 1) + b"\x00" class FakeStripper: """Strips by mapping bytes, so no test ever shells out to exiftool.""" def __init__(self, mapping: dict[bytes, bytes | None] | None = None) -> None: self.mapping = mapping or {} def strip(self, payload: bytes, kind: MediaKind) -> bytes | None: """The mapped output, or the payload unchanged.""" assert isinstance(kind, MediaKind) return self.mapping.get(payload, payload) def _album(tmp_path: Path, files: dict[str, bytes] | None = None) -> Path: directory = tmp_path / "pack" media = directory / MEDIA_DIR media.mkdir(parents=True) for name, payload in (files or {}).items(): (media / name).write_bytes(payload) return directory def _source(tmp_path: Path, files: dict[str, bytes]) -> Path: source = tmp_path / "batch" / "Folder" source.mkdir(parents=True) for name, payload in files.items(): (source / name).write_bytes(payload) return source def test_every_file_lands_in_exactly_one_bucket(tmp_path: Path) -> None: # The done condition, mechanised: the buckets must sum to what was seen. directory = _album(tmp_path, {"have.png": PIXEL}) source = _source( tmp_path, {"new.png": OTHER, "same.png": PIXEL, "copy.png": OTHER, "notes.txt": b"hello"}, ) report = plan_import(source, directory, FakeStripper()) assert sum(report.counts.values()) == len(report.decisions) == 4 assert report.counts == { Outcome.NOT_MEDIA: 1, Outcome.UNSTRIPPABLE: 0, Outcome.ALREADY_IN_PACK: 1, Outcome.EXACT_DUP: 1, Outcome.IMPORTED: 1, } assert report.before == 1 assert report.after == 2 def test_the_report_prints_every_bucket_including_the_empty_ones(tmp_path: Path) -> None: # A suppressed zero is a silently missing category; "0 could not be # stripped" is how the operator sees the video fix still holding. report = plan_import(_source(tmp_path, {}), _album(tmp_path), FakeStripper()) body = "\n".join(report.lines()) for outcome in Outcome: assert str(outcome) in body or outcome is Outcome.IMPORTED assert "could not be stripped" in body assert "accounted for" in body def test_a_file_already_in_the_album_is_not_imported_twice(tmp_path: Path) -> None: directory = _album(tmp_path, {"have.png": PIXEL}) source = _source(tmp_path, {"renamed.png": PIXEL}) report = plan_import(source, directory, FakeStripper()) assert report.decisions[0].outcome is Outcome.ALREADY_IN_PACK def test_two_sources_that_strip_to_one_photo_collapse(tmp_path: Path) -> None: # The measured case that a source-hash-only ledger would import twice: # the same photo re-encoded, identical once the metadata is gone. directory = _album(tmp_path) source = _source(tmp_path, {"a.png": PIXEL, "b.png": OTHER}) stripper = FakeStripper({PIXEL: PIXEL, OTHER: PIXEL}) report = plan_import(source, directory, stripper) outcomes = [d.outcome for d in report.decisions] assert outcomes == [Outcome.IMPORTED, Outcome.EXACT_DUP] def test_a_chat_upload_and_an_exported_copy_collapse(tmp_path: Path) -> None: # Same as above but across runs: the album already holds the stripped # bytes, and the export folder holds the unstripped original. directory = _album(tmp_path, {"hash.png": PIXEL}) source = _source(tmp_path, {"original.png": OTHER}) report = plan_import(source, directory, FakeStripper({OTHER: PIXEL})) assert report.decisions[0].outcome is Outcome.ALREADY_IN_PACK def test_a_file_the_stripper_will_not_vouch_for_is_refused(tmp_path: Path) -> None: report = plan_import( _source(tmp_path, {"a.png": PIXEL}), _album(tmp_path), FakeStripper({PIXEL: None}) ) assert report.decisions[0].outcome is Outcome.UNSTRIPPABLE def test_format_is_decided_by_bytes_not_by_suffix(tmp_path: Path) -> None: directory = _album(tmp_path) source = _source(tmp_path, {"photo.txt": PIXEL, "fake.jpg": b"not an image at all"}) report = plan_import(source, directory, FakeStripper()) by_name = {d.source.name: d for d in report.decisions} assert by_name["fake.jpg"].outcome is Outcome.NOT_MEDIA imported = by_name["photo.txt"] assert imported.outcome is Outcome.IMPORTED # The suffix is corrected to match the bytes: pack.media_type looks the # stored suffix up in a closed table, and a miss fails the *next* load. assert imported.stored is not None assert imported.stored.name == "photo.png" def test_a_suffix_that_agrees_with_the_bytes_is_kept(tmp_path: Path) -> None: # .jpeg and .jpg are both image/jpeg, so a .jpeg keeps its name. jpeg = b"\xff\xd8\xff" + b"\x00" * 40 source = _source(tmp_path, {"photo.jpeg": jpeg}) report = plan_import(source, _album(tmp_path), FakeStripper()) assert report.decisions[0].stored is not None assert report.decisions[0].stored.name == "photo.jpeg" def test_a_name_collision_disambiguates_rather_than_dropping_a_photo(tmp_path: Path) -> None: # The old bug: two exports both saying IMG_0001.jpg silently lost one. directory = _album(tmp_path, {"IMG_1.png": PIXEL}) source = _source(tmp_path, {"IMG_1.png": OTHER}) report = plan_import(source, directory, FakeStripper()) assert report.decisions[0].stored is not None assert report.decisions[0].stored.name == "IMG_1-2.png" def test_video_is_imported_like_anything_else(tmp_path: Path) -> None: report = plan_import(_source(tmp_path, {"c.mp4": CLIP}), _album(tmp_path), FakeStripper()) assert report.decisions[0].outcome is Outcome.IMPORTED def test_a_subdirectory_in_the_source_is_ignored(tmp_path: Path) -> None: source = _source(tmp_path, {"a.png": PIXEL}) (source / "nested").mkdir() report = plan_import(source, _album(tmp_path), FakeStripper()) assert len(report.decisions) == 1 def test_running_the_import_stores_the_new_file_and_the_ledger(tmp_path: Path) -> None: directory = _album(tmp_path) source = _source(tmp_path, {"a.png": PIXEL}) run_import(source, directory, FakeStripper()) stored = directory / MEDIA_DIR / "a.png" assert stored.read_bytes() == PIXEL assert stored.stat().st_mode & 0o777 == FILE_MODE assert pack_ledger.read(directory)["a.png"].source == pack_ledger.digest(PIXEL) def test_a_rejected_file_is_moved_and_never_deleted(tmp_path: Path) -> None: directory = _album(tmp_path, {"have.png": PIXEL}) source = _source(tmp_path, {"same.png": PIXEL, "notes.txt": b"hello"}) run_import(source, directory, FakeStripper()) parent = source.parent assert (parent / SKIPPED_DIR / "already-in-pack" / "same.png").read_bytes() == PIXEL assert (parent / SKIPPED_DIR / "not-media" / "notes.txt").read_bytes() == b"hello" assert not (source / "same.png").exists() def test_the_skipped_folder_sits_beside_the_source_not_under_home(tmp_path: Path) -> None: # Derived from the source argument so a tmp_path source keeps every write # inside tmp_path -- conftest redirects XDG_DATA_HOME, not $HOME. source = _source(tmp_path, {"notes.txt": b"hello"}) run_import(source, _album(tmp_path), FakeStripper()) assert (source.parent / SKIPPED_DIR).is_dir() def test_importing_the_same_folder_twice_is_a_no_op(tmp_path: Path) -> None: # The property the ledger exists for. directory = _album(tmp_path) source = _source(tmp_path, {"a.png": PIXEL}) run_import(source, directory, FakeStripper()) again = _source(tmp_path / "second", {"a.png": PIXEL}) report = run_import(again, directory, FakeStripper()) assert report.counts[Outcome.IMPORTED] == 0 assert report.counts[Outcome.ALREADY_IN_PACK] == 1 def test_an_import_creates_the_album_directory_when_there_is_none(tmp_path: Path) -> None: directory = tmp_path / "fresh" directory.mkdir() report = run_import(_source(tmp_path, {"a.png": PIXEL}), directory, FakeStripper()) assert (directory / MEDIA_DIR / "a.png").exists() assert report.before == 0 def test_the_import_does_not_write_tags(tmp_path: Path) -> None: # An imported file is simply absent from tags.json, which is what # --untagged keys off and what pack_load already tolerates. directory = _album(tmp_path) run_import(_source(tmp_path, {"a.png": PIXEL}), directory, FakeStripper()) assert not (directory / "tags.json").exists() def test_a_photo_that_arrives_mid_run_is_not_stored_twice(tmp_path: Path) -> None: # Planning strips every candidate, which takes minutes on a real batch. # A photo uploaded from the chat during that window is in the album by # the time the apply pass runs, and storing it anyway puts it there # twice: once hash-named by !add_photo, once under its original name. # Measured: that happened to 6 photos during the first live import. directory = _album(tmp_path) source = _source(tmp_path, {"a.png": PIXEL}) class Interloper(FakeStripper): """Drops the photo into the album the moment planning reads it.""" def strip(self, payload: bytes, kind: MediaKind) -> bytes | None: (directory / MEDIA_DIR / "from-the-chat.png").write_bytes(payload) return super().strip(payload, kind) report = run_import(source, directory, Interloper()) assert report.counts[Outcome.IMPORTED] == 0 assert report.counts[Outcome.ALREADY_IN_PACK] == 1 assert sorted(p.name for p in (directory / MEDIA_DIR).iterdir()) == ["from-the-chat.png"] # And it is moved aside like any other repeat, never left behind. assert (source.parent / SKIPPED_DIR / "already-in-pack" / "a.png").exists()