import unittest from tests.fakes import FakeGitea, FakeJev, FakeModel, config, diff_for, gitea_file from tests.test_config_layers import host_data, repo from tests.test_flow import run from werkrevisor.config import ConfigError, ImagesConfig, build_config from werkrevisor.images import check, is_image from werkrevisor.rules import ChangedFile from werkrevisor.verdict import Verdict DOC = "docs/prs/2026-10-13-PR#7-readme.md" SCREENSHOT = "doc/images/dashboard.png" PNG = b"\x89PNG\r\n\x1a\n" def png(size): return PNG + b"\0" * (size - len(PNG)) def image_gitea(images): gitea = FakeGitea(files=[gitea_file(DOC)] + [gitea_file(path, status="added", additions=0) for path in images], diff=diff_for(DOC, added=["Text."], new=True)) gitea.raw_files.update(images) return gitea def codes(outcome): return {r.code: r for r in outcome.result.reasons} class ImageFlowTest(unittest.TestCase): def test_a_small_image_gives_only_a_hint(self): outcome, _ = run(image_gitea({SCREENSHOT: png(176871)}), FakeModel(prompt_files=[DOC])) self.assertIs(outcome.result.verdict, Verdict.GREEN) found = codes(outcome) self.assertEqual(found["images"].text, f"images checked by size and signature only: {SCREENSHOT} (177 KB)") self.assertNotIn("unreviewable_files", found) self.assertNotIn("category", found) def test_a_small_image_needs_no_judgment_by_jev(self): cfg = config(engine={"mode": "combined", "model_role": "advises"}, gate={"yellow_gate": "block"}, path_judge={"judge": "jev", "scope": "all"}) model = FakeModel(prompt_files=[DOC]) outcome, _ = run(image_gitea({SCREENSHOT: png(176871)}), model, cfg=cfg, jev=FakeJev()) self.assertIs(outcome.result.verdict, Verdict.GREEN) self.assertNotIn("judged_paths_not_judged", codes(outcome)) self.assertEqual(model.excluded, [(SCREENSHOT,)]) def test_an_image_above_max_bytes_is_yellow(self): gitea = image_gitea({SCREENSHOT: png(512001)}) outcome, _ = run(gitea, FakeModel(prompt_files=[DOC])) self.assertIs(outcome.result.verdict, Verdict.YELLOW) self.assertEqual(codes(outcome)["image_too_large"].text, f"images above [images] max_bytes = 512 KB: {SCREENSHOT}; reduce them, e.g. with pngquant") def test_images_above_max_total_bytes_are_yellow(self): images = {f"doc/images/{n}.png": png(450000) for n in range(5)} outcome, _ = run(image_gitea(images), FakeModel(prompt_files=[DOC])) self.assertIs(outcome.result.verdict, Verdict.YELLOW) self.assertEqual(codes(outcome)["images_too_large"].text, "images of 2250 KB in all exceed [images] max_total_bytes = 2000 KB") def test_another_content_with_an_image_extension_is_yellow(self): outcome, _ = run(image_gitea({"doc/logo.png": b"PK\x03\x04archive"}), FakeModel(prompt_files=[DOC])) self.assertIs(outcome.result.verdict, Verdict.YELLOW) self.assertEqual(codes(outcome)["image_signature"].text, "files whose content is no image of their extension: doc/logo.png") def test_a_pr_of_images_only_needs_no_model(self): gitea = FakeGitea(files=[gitea_file(SCREENSHOT, status="added", additions=0)], diff="") gitea.raw_files[SCREENSHOT] = png(1000) model = FakeModel(prompt_files=[]) outcome, _ = run(gitea, model) self.assertIs(outcome.result.verdict, Verdict.GREEN) self.assertEqual(model.calls, []) self.assertEqual(outcome.result.llm_status, "skipped (images and mechanical changes only)") class ImageCheckTest(unittest.TestCase): def test_signatures(self): for path, content, matches in (("a.jpg", b"\xff\xd8\xff\xe0", True), ("a.JPEG", b"\xff\xd8\xff\xe1", True), ("a.gif", b"GIF89a", True), ("a.ico", b"\0\0\1\0", True), ("a.webp", b"RIFF\0\0\0\0WEBPVP8 ", True), ("a.webp", b"RIFF\0\0\0\0WAVE", False), ("a.png", b"\xff\xd8\xff", False)): with self.subTest(path=path, content=content): reasons = check([ChangedFile(path, "added")], lambda p, limit: content, ImagesConfig()) self.assertEqual([r.code for r in reasons], ["images" if matches else "image_signature"]) def test_reads_at_most_one_byte_above_max_bytes(self): limits = [] check([ChangedFile("a.png", "added")], lambda p, limit: limits.append(limit) or PNG, ImagesConfig()) self.assertEqual(limits, [512001]) def test_a_deleted_image_is_not_read(self): def fetch(path, limit): raise AssertionError(path) self.assertEqual(check([ChangedFile("a.png", "removed")], fetch, ImagesConfig()), []) def test_only_known_formats_within_the_globs_are_images(self): self.assertTrue(is_image("doc/a.png", ImagesConfig())) self.assertFalse(is_image("doc/a.svg", ImagesConfig())) self.assertFalse(is_image("doc/a.bmp", ImagesConfig(globs=("**/*",)))) self.assertFalse(is_image("doc/a.png", ImagesConfig(globs=("src/**",)))) class ImageConfigTest(unittest.TestCase): def test_a_repository_may_lower_the_limits(self): cfg = build_config(host_data(), repo("[images]\nmax_bytes = 100000\nmax_total_bytes = 500000\n")) self.assertEqual((cfg.images.max_bytes, cfg.images.max_total_bytes), (100000, 500000)) def test_a_repository_must_not_raise_the_limits(self): for key, value in (("max_bytes", 512001), ("max_total_bytes", 2000001)): with self.subTest(key=key), self.assertRaisesRegex(ConfigError, f"may only lower it"): build_config(host_data(), repo(f"[images]\n{key} = {value}\n")) def test_limits_must_be_positive(self): with self.assertRaisesRegex(ConfigError, r"\[images\] max_bytes must be a positive integer"): config(images={"max_bytes": 0}) if __name__ == "__main__": unittest.main()