Files
claude-code fb0c20af25
werkator build succeeded after 00:16
werkator/werkrevisor-statistics build succeeded after 00:40
werkrevisor 0.40.0: rules for raster images (#98)
Co-authored-by: claude-code <claude-code@javagil.de>
2026-10-13 07:42:24 +02:00

124 lines
6.0 KiB
Python

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()