"""Golden parser fixtures for immich-go reports (US05-03). The reports below are the pinned grammar for each supported uploader version. If a future immich-go changes its wording, the fix is a new adapter and a new golden report — never a looser pattern here, because a loose pattern is how an unread line becomes a false success. """ import pytest from photo_pipeline.integrations import immich_go_report as parser TEXT_REPORT = """ Scanning /lib/rome INFO uploaded /lib/rome/new.jpg INFO server has the same file /lib/rome/dup.jpg INFO server has an older file, upgrading /lib/rome/old.jpg WARN discarded /lib/rome/notes.txt: unsupported file type ERROR error uploading /lib/rome/broken.jpg: connection reset by peer Uploaded 1, upgraded 1, duplicates 1, skipped 1, errors 1 """ JSON_REPORT = """ {"time":"2026-01-01T10:00:00Z","level":"INFO","msg":"uploaded","file":"/lib/rome/new.jpg"} {"level":"INFO","msg":"server has the same file","file":"/lib/rome/dup.jpg"} {"level":"INFO","msg":"server has an older file, upgrading","file":"/lib/rome/old.jpg"} {"level":"WARN","msg":"discarded: unsupported file type","file":"/lib/rome/notes.txt"} {"level":"ERROR","msg":"error uploading","file":"/lib/rome/broken.jpg"} {"level":"INFO","msg":"report","counts":{"uploaded":1,"upgraded":1,"duplicates":1,\ "skipped":1,"errors":1}} """ EXPECTED = { "/lib/rome/new.jpg": parser.UPLOADED, "/lib/rome/dup.jpg": parser.DUPLICATE, "/lib/rome/old.jpg": parser.UPGRADED, "/lib/rome/notes.txt": parser.SKIPPED, "/lib/rome/broken.jpg": parser.FAILED, } EXPECTED_COUNTS = { parser.UPLOADED: 1, parser.UPGRADED: 1, parser.DUPLICATE: 1, parser.SKIPPED: 1, parser.FAILED: 1, } def _outcomes(parsed) -> dict: return {entry["path"]: entry["outcome"] for entry in parsed["entries"]} @pytest.mark.parametrize( ("version", "report", "expected_parser"), [ ("immich-go 0.21.0", TEXT_REPORT, "text-v1"), ("immich-go 0.22.3", TEXT_REPORT, "text-v1"), ("immich-go 0.23.1", JSON_REPORT, "json-v1"), ("immich-go 0.24.0", JSON_REPORT, "json-v1"), ], ) def test_every_supported_version_classifies_every_outcome(version, report, expected_parser): parsed = parser.parse(report, version) assert parsed["parser"] == expected_parser and parsed["supported"] assert parsed["parser_version"] == parser.PARSER_VERSION assert _outcomes(parsed) == EXPECTED assert parsed["counts"] == EXPECTED_COUNTS def test_an_unsupported_version_parses_nothing_at_all(): """A build whose output was never pinned must not be read optimistically.""" parsed = parser.parse(TEXT_REPORT, "immich-go 9.99.0") assert parsed["parser"] is None and parsed["supported"] is False assert parsed["entries"] == [] and parsed["counts"] is None @pytest.mark.parametrize("version", [None, "", "immich-go dev", "unknown"]) def test_a_missing_or_unreadable_version_is_unsupported(version): assert parser.parser_for(version) is None def test_malformed_lines_are_counted_never_classified(): report = ( "{not json at all\n" '{"level":"INFO","msg":"uploaded"}\n' # no file '["not","an","object"]\n' '{"level":"INFO","msg":"uploaded","file":"/lib/rome/new.jpg"}\n' ) parsed = parser.parse(report, "immich-go 0.23.1") assert _outcomes(parsed) == {"/lib/rome/new.jpg": parser.UPLOADED} assert parsed["unparsed"] == 3 def test_a_line_with_a_path_but_no_known_verb_is_not_an_outcome(): parsed = parser.parse("INFO considering /lib/rome/a.jpg\n", "immich-go 0.21.0") assert parsed["entries"] == [] and parsed["unparsed"] == 1 def test_the_album_name_never_classifies_the_line(): """A folder called "errors" must not turn a successful upload into a failure.""" parsed = parser.parse("INFO uploaded /lib/errors/a.jpg\n", "immich-go 0.21.0") assert _outcomes(parsed) == {"/lib/errors/a.jpg": parser.UPLOADED} def test_paths_with_spaces_and_reasons_are_read_whole(): report = "ERROR error uploading /lib/summer holiday/a b.jpg: connection reset\n" parsed = parser.parse(report, "immich-go 0.21.0") assert _outcomes(parsed) == {"/lib/summer holiday/a b.jpg": parser.FAILED} def test_evidence_is_bounded_per_entry(): line = "INFO uploaded /lib/rome/a.jpg " + "x" * 5_000 (entry,) = parser.parse(line, "immich-go 0.21.0")["entries"] assert len(entry["evidence"]) == parser.MAX_EVIDENCE_CHARS def test_a_looping_uploader_cannot_produce_unbounded_entries(monkeypatch): monkeypatch.setattr(parser, "MAX_ENTRIES", 10) report = "".join(f"INFO uploaded /lib/rome/{n}.jpg\n" for n in range(50)) parsed = parser.parse(report, "immich-go 0.21.0") assert len(parsed["entries"]) == 10 and parsed["entries_truncated"] is True def test_a_report_without_a_summary_reports_no_counts(): parsed = parser.parse("INFO uploaded /lib/rome/a.jpg\n", "immich-go 0.21.0") assert parsed["counts"] is None, "counts are read, never invented"