testsAndMisc/python_pkg/artgate/tests/test_cli.py
Krzysztof kuhy Rudnicki e6e0ba5ab3
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
artgate: diffusion + 3D routes, full vector coverage, and the margin gate
Closes the remaining bake-off cells. The phase's real result was not a route
score but a MISSING GATE.

All twelve SDXL icons measured 100% opaque coverage -- square tiles with no
sprite silhouette -- and passed every gate in the harness. Gate-pass was
measuring pipeline hygiene, not usability. MARGIN_TOO_SMALL inverts the table
from a meaningless 12/12/12 tie to:

    diffusion   0/12      (every output full-bleed)
    vector     10/12
    procedural 11/12

The two non-diffusion failures were true positives in MY shapes, not gate
error: the procedural sword pommel ran off the bottom edge and the vector meat
bone clipped the top. Both fixed; both routes back to 12/12.

Diffusion recovers to 12/12 with flood-fill background removal, but visual
inspection shows the fill eats into the art (bone reduced to a fragment, sword
nearly erased). Reported as inadequate rather than shipped -- proper matting is
needed before route (b) is a usable pipeline.

Blender exits 0 even when its embedded script raises: BLENDER_EEVEE_NEXT does
not exist in 5.2, and the failure produced a clean exit and no file. render()
now asserts the output exists, or the route would report phantom successes.

Model gating verified at execution time, never recalled: SDXL base and
nerijs/pixel-art-xl are ungated, FLUX.1-dev is gated=auto, and
runwayml/stable-diffusion-v1-5 now 307-redirects.

The finishing stage now generalises across three continuous-tone sources
(SVG raster, Blender render, SDXL): each is 0/N raw and N/N finished.

Verified: 114 tests, 100% branch coverage on 13 modules, zero suppressions,
all hooks pass. Note: a local pytest run masked a missing branch that the
pre-commit hook caught -- the hook's run is authoritative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
2026-08-15 22:51:31 +02:00

182 lines
6.0 KiB
Python

"""CLI tests, covering all three exit codes.
The three-valued exit code is the contract the whole bake-off rests on, so
each value is asserted explicitly: a harness fault must never be recorded as
an art rejection.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from PIL import Image
import pytest
from python_pkg.artgate.cli import evaluate, main
from python_pkg.artgate.codes import (
EXIT_GATE_FAILURE,
EXIT_HARNESS_ERROR,
EXIT_PASS,
Code,
)
from python_pkg.artgate.spec import TargetSpec
if TYPE_CHECKING:
from pathlib import Path
SPEC = """
name = "t"
canvas = [4, 4]
max_colors = 4
alpha_binary = true
palette = ["#0a141e", "#28323c"]
gates = [
"ALPHA_NOT_BINARY", "TOO_MANY_COLORS", "OFF_PALETTE",
"CANVAS_SIZE", "SCALE_NOT_INVARIANT",
]
"""
def _write_png(path: Path, rgb: tuple[int, int, int], size: int = 4) -> Path:
"""Write a uniform opaque PNG.
Args:
path: Destination file.
rgb: The fill colour.
size: Square edge length.
Returns:
The written path.
"""
arr = np.zeros((size, size, 4), dtype=np.uint8)
arr[:, :, :3] = rgb
arr[:, :, 3] = 255
Image.fromarray(arr, "RGBA").save(path)
return path
@pytest.fixture(name="spec_file")
def fixture_spec_file(tmp_path: Path) -> Path:
"""Write the shared test spec.
Args:
tmp_path: Pytest temporary directory.
Returns:
Path to the spec.
"""
path = tmp_path / "t.toml"
path.write_text(SPEC, encoding="utf-8")
return path
class TestExitCodes:
"""Each exit code is reachable and means exactly one thing."""
def test_pass(
self, tmp_path: Path, spec_file: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Conforming art exits 0."""
img = _write_png(tmp_path / "ok.png", (10, 20, 30))
assert main([str(img), "--spec", str(spec_file)]) == EXIT_PASS
assert "PASS" in capsys.readouterr().out
def test_gate_failure(
self, tmp_path: Path, spec_file: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Off-palette art exits 1 and names the code."""
img = _write_png(tmp_path / "bad.png", (200, 10, 10))
assert main([str(img), "--spec", str(spec_file)]) == EXIT_GATE_FAILURE
assert "OFF_PALETTE" in capsys.readouterr().out
def test_missing_spec_is_harness_error(
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A missing spec exits 2, not 1."""
img = _write_png(tmp_path / "x.png", (10, 20, 30))
code = main([str(img), "--spec", str(tmp_path / "absent.toml")])
assert code == EXIT_HARNESS_ERROR
assert "artgate:" in capsys.readouterr().err
def test_unreadable_image_is_harness_error(
self, tmp_path: Path, spec_file: Path
) -> None:
"""A non-image file exits 2, not 1."""
junk = tmp_path / "junk.png"
junk.write_text("not an image", encoding="utf-8")
code = main([str(junk), "--spec", str(spec_file)])
assert code == EXIT_HARNESS_ERROR
def test_json_output(
self, tmp_path: Path, spec_file: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""JSON mode emits the machine-readable verdict."""
img = _write_png(tmp_path / "ok.png", (10, 20, 30))
main([str(img), "--spec", str(spec_file), "--json"])
out = capsys.readouterr().out
assert '"exit": 0' in out
assert '"style": "t"' in out
class TestEvaluate:
"""Gate selection is driven by the spec, not by hardcoded branching."""
def test_canvas_mismatch_reported(self, tmp_path: Path) -> None:
"""A wrong-sized canvas is caught."""
arr = np.zeros((8, 8, 4), dtype=np.uint8)
arr[:, :, 3] = 255
spec = TargetSpec(name="s", canvas=(4, 4), gates=frozenset({Code.CANVAS_SIZE}))
assert evaluate(arr, spec) == [Code.CANVAS_SIZE]
def test_gates_not_declared_are_not_run(self) -> None:
"""An off-palette image passes when the palette gate is not declared."""
arr = np.zeros((2, 2, 4), dtype=np.uint8)
arr[:, :, :3] = (99, 99, 99)
arr[:, :, 3] = 255
spec = TargetSpec(
name="s",
palette=frozenset({(1, 2, 3)}),
gates=frozenset({Code.CANVAS_SIZE}),
)
assert evaluate(arr, spec) == []
def test_codes_are_deduplicated(self) -> None:
"""A code raised by two gates is reported once."""
arr = np.zeros((2, 2, 4), dtype=np.uint8)
spec = TargetSpec(
name="s",
max_colors=4,
palette=frozenset({(1, 2, 3)}),
gates=frozenset({Code.TOO_MANY_COLORS, Code.OFF_PALETTE}),
)
assert evaluate(arr, spec) == [Code.NO_OPAQUE_PIXELS]
def test_margin_gate_runs_when_declared(self) -> None:
"""A full-bleed asset fails once the margin gate is declared.
Regression: twelve diffusion icons were 100% opaque -- square tiles,
not sprites -- and passed every other gate, because no gate asked
whether the asset had a silhouette at all.
"""
arr = np.zeros((8, 8, 4), dtype=np.uint8)
arr[:, :, 3] = 255
spec = TargetSpec(
name="s", min_margin=1, gates=frozenset({Code.MARGIN_TOO_SMALL})
)
assert evaluate(arr, spec) == [Code.MARGIN_TOO_SMALL]
def test_silhouette_gate_runs_when_declared(self) -> None:
"""A near-white asset fails the silhouette gate.
Regression: ``SILHOUETTE_LOW_CONTRAST`` was declarable in a spec but
had no implementation, so it parsed cleanly, ran nothing and reported
a pass. ``load_spec`` now rejects undeclarable gates, and this asserts
the gate genuinely executes once declared.
"""
arr = np.zeros((8, 8, 4), dtype=np.uint8)
arr[:, :, :3] = 250
arr[:, :, 3] = 255
spec = TargetSpec(name="s", gates=frozenset({Code.SILHOUETTE_LOW_CONTRAST}))
assert evaluate(arr, spec) == [Code.SILHOUETTE_LOW_CONTRAST]