double-exposure / tests /test_restore.py
Eddie Faillace
double-exposure app deploy snapshot 2026-07-21 (WP-24 calibration pass)
7dff04f
Raw
History Blame Contribute Delete
34.8 kB
"""Offline tests for app.restore (physics-anchored generative restoration).
All network is stubbed via the ``restore_fn`` hook; every assert compares two
independently computed quantities or checks a soft-fail contract.
"""
import numpy as np
import pytest
from app.restore import restore_layer, recover, _SEPARATE, _RESTORE, RecoverResult
def _toy_scan(seed: int = 0):
"""A tiny synthetic positive with densitometry, via the real preprocessing path."""
from PIL import Image
from app.preprocessing import preprocess_negative
rng = np.random.default_rng(seed)
# two overlapping soft blobs so a heuristic split has something to bite on
yy, xx = np.mgrid[0:96, 0:96]
a = np.exp(-((xx - 30) ** 2 + (yy - 40) ** 2) / 500.0)
b = np.exp(-((xx - 65) ** 2 + (yy - 55) ** 2) / 700.0)
img = np.clip(0.25 + 0.6 * a + 0.5 * b, 0, 1).astype(np.float32)
rgb = np.stack([img, img * 0.9, img * 0.8], axis=-1)
pil = Image.fromarray((rgb * 255).astype(np.uint8))
return preprocess_negative(pil, stock="Portra 400", scan_type="positive",
scan_calibration="auto_exposed", auto_trim=False)
def test_restore_layer_soft_fails_without_key(monkeypatch):
"""No REPLICATE_API_TOKEN and no hook -> (None, meta) with api_contacted False."""
monkeypatch.delenv("REPLICATE_API_TOKEN", raising=False)
notes: list[str] = []
out, meta = restore_layer(np.zeros((8, 8, 3), np.float32), "x", notes=notes)
assert out is None
assert meta["api_contacted"] is False
assert any("REPLICATE_API_TOKEN" in n for n in notes)
def test_restore_layer_prompt_mode_selects_template():
"""separate vs restore choose different instructions, both naming the scene."""
seen = {}
def hook(rgb, prompt):
seen["prompt"] = prompt
return rgb
_out, _m = restore_layer(np.zeros((4, 4, 3), np.float32), "a red barn",
mode="separate", restore_fn=hook)
assert seen["prompt"].startswith(_SEPARATE.split("{")[0])
assert "a red barn" in seen["prompt"]
_out, _m = restore_layer(np.zeros((4, 4, 3), np.float32), "a red barn",
mode="restore", restore_fn=hook)
assert seen["prompt"].startswith(_RESTORE.split("{")[0])
# The two modes must not produce the same instruction (teeth).
assert _SEPARATE.split("{")[0] != _RESTORE.split("{")[0]
def test_recover_orchestration_with_hook():
"""Hook path returns both layers, honest disclosure, and never contacts the API."""
pre = _toy_scan()
calls = {"n": 0}
def hook(rgb, prompt):
calls["n"] += 1
return np.clip(rgb * 1.2, 0, 1) # a distinct, non-identity 'restore'
res = recover(pre.rgb, pre.h_total, pre.confidence_mask, restore_fn=hook)
assert isinstance(res, RecoverResult)
assert res.dominant is not None and res.second is not None
assert res.api_contacted is False
assert calls["n"] == 2 # exactly one call per layer
# dreamed_frac is a real fraction independently recomputable from the mask subtraction
from app.asymmetric import subtract_layer
_hb, _g, snr = subtract_layer(res.anchor_rgb, pre.h_total, pre.confidence_mask)
assert res.dreamed_frac == pytest.approx(100.0 * float(np.mean(snr)), abs=1e-6)
def test_recover_without_densitometry_skips_second_layer():
"""No h_total -> dominant still attempted, second is None, note explains."""
pre = _toy_scan()
notes: list[str] = []
res = recover(pre.rgb, None, None, restore_fn=lambda rgb, p: rgb, notes=notes)
assert res.dominant is not None
assert res.second is None
assert any("Second layer unavailable" in n for n in notes)
def test_recover_soft_fails_offline(monkeypatch):
"""No key, no hook -> both layers None, nothing sent."""
monkeypatch.delenv("REPLICATE_API_TOKEN", raising=False)
pre = _toy_scan()
res = recover(pre.rgb, pre.h_total, pre.confidence_mask)
assert res.dominant is None and res.second is None
assert res.api_contacted is False
def test_recover_user_scenes_win_over_vlm():
"""User-supplied scene descriptions are used verbatim and the VLM is never called."""
pre = _toy_scan()
prompts: list[str] = []
def hook(rgb, prompt):
prompts.append(prompt)
return rgb
def vlm_spy(pil, q): # must NOT be consulted when the user supplied scenes
raise AssertionError("VLM called despite user-supplied scenes")
res = recover(pre.rgb, pre.h_total, pre.confidence_mask, restore_fn=hook,
vlm=vlm_spy, scene_headline="the woman in the orange top",
scene_other="a stone castle wall")
assert res.dominant is not None and res.second is not None
assert "the woman in the orange top" in prompts[0] # headline = separate call
assert "a stone castle wall" in prompts[1] # residual = restore call
def test_identify_scenes_stub_vlm():
"""identify_scenes returns the two cleaned phrases from the vlm callable."""
from app.restore import identify_scenes
pre = _toy_scan()
answers = iter(["a sunlit garden", "a brick house"])
s1, s2 = identify_scenes(pre.rgb, lambda pil, q: next(answers))
assert (s1, s2) == ("a sunlit garden", "a brick house")
# Teeth: over-long / multi-line junk is rejected to empty (editor falls back)
junk = iter(["x" * 500, "line1\nline2"])
s1, s2 = identify_scenes(pre.rgb, lambda pil, q: next(junk))
assert s1 == "" and s2 == ""
def test_restore_best_scene_handler_scene_choice(monkeypatch):
"""UI handler routes the PICKED scene into the headline prompt (Scene 2 chosen)."""
import app.main as m
from PIL import Image as PILImage
pre = _toy_scan()
bs = m._build_best_state(
pre, type("S", (), {"image_a": pre.rgb, "image_b": pre.rgb})(), "Portra 400", 1.0, 1.0
)
prompts: list[str] = []
def fake_recover(observed, h_total=None, confidence_mask=None, *, scene_headline=None,
scene_other=None, notes=None, **kwargs):
from app.restore import RecoverResult
prompts.append((scene_headline, scene_other))
return RecoverResult(dominant=np.zeros((4, 4, 3), np.float32), notes=notes or [])
import app.restore as r
monkeypatch.setattr(r, "recover", fake_recover)
dom, sec, status, _alts, _alt_state = m.restore_best_scene(bs, "Scene 2", "castle", "person")
assert prompts == [("person", "castle")] # Scene 2 ("person") leads
assert dom is not None
assert "person" in status
def test_scene_prompt_truncated_at_cap():
"""Over-long scene text hits restore_layer's SAFETY NET (MAX_TOTAL_SCENE_CHARS)
at a word boundary, with a note — the per-piece budgets live in recover()."""
from app.restore import restore_layer, MAX_TOTAL_SCENE_CHARS
seen = {}
notes: list[str] = []
long_scene = "word " * 400 # 2000 chars, well over the safety net
restore_layer(np.zeros((4, 4, 3), np.float32), long_scene,
mode="restore", restore_fn=lambda rgb, p: seen.update(p=p) or rgb,
notes=notes)
# The full description does not survive; it is cut at a word boundary with an ellipsis.
assert long_scene.strip() not in seen["p"]
assert "…" in seen["p"]
# Independent bound: not all 400 words survived, and the surviving run fits the net.
assert seen["p"].count("word") < 400
assert seen["p"].count("word") <= MAX_TOTAL_SCENE_CHARS // len("word ") + 1
# Unlike before, the truncation is REPORTED, not silent.
assert any("prompt budget" in n for n in notes)
# A short description passes through untouched (no ellipsis).
seen.clear()
restore_layer(np.zeros((4, 4, 3), np.float32), "a green patio with two women",
mode="restore", restore_fn=lambda rgb, p: seen.update(p=p) or rgb)
assert "a green patio with two women" in seen["p"] and "…" not in seen["p"]
def test_run_with_backoff_retries_throttle_only(monkeypatch):
"""429/throttled errors are retried (no real sleep); other errors raise at once."""
from app.restore import _run_with_backoff
monkeypatch.setattr("time.sleep", lambda s: None)
calls = {"n": 0}
def flaky():
calls["n"] += 1
if calls["n"] < 3:
raise RuntimeError("status: 429 detail: Request was throttled.")
return "ok"
assert _run_with_backoff(flaky) == "ok"
assert calls["n"] == 3
def hard_fail():
raise ValueError("not a throttle")
with pytest.raises(ValueError):
_run_with_backoff(hard_fail)
def always_throttled():
raise RuntimeError("throttled")
with pytest.raises(RuntimeError):
_run_with_backoff(always_throttled, attempts=2)
def test_drift_warning_on_repaint():
"""A hook that returns unrelated noise triggers the drift warning; a faithful
edit (brightened copy) does not."""
pre = _toy_scan()
rng = np.random.default_rng(0)
notes: list[str] = []
recover(pre.rgb, pre.h_total, pre.confidence_mask, notes=notes,
restore_fn=lambda rgb, p: rng.random(rgb.shape).astype(np.float32))
assert any("drifted from your photo" in n for n in notes)
notes2: list[str] = []
recover(pre.rgb, pre.h_total, pre.confidence_mask, notes=notes2,
restore_fn=lambda rgb, p: np.clip(rgb * 1.2, 0, 1))
assert not any("drifted" in n for n in notes2)
def test_best_of_n_picks_most_faithful():
"""With n=3, the noise takes lose to the faithful take; alternates sorted desc."""
pre = _toy_scan()
rng = np.random.default_rng(1)
calls = {"n": 0}
def hook(rgb, prompt):
calls["n"] += 1
if calls["n"] == 2: # only the second take respects the input
return np.clip(rgb * 1.15, 0, 1)
return rng.random(rgb.shape).astype(np.float32)
res = recover(pre.rgb, None, None, restore_fn=hook, n_candidates=3)
assert calls["n"] == 3
assert len(res.alternates) == 3
rs = [r for _img, r in res.alternates]
assert rs == sorted(rs, reverse=True)
# dominant is the faithful take: independently recompute its similarity.
# WP-19: candidates are ranked against the physics ANCHOR, not the mixed
# frame — r vs the mixed rewards failed separations (2026-07-19 bake-off).
from app.restore import structural_similarity_r
assert structural_similarity_r(res.anchor_rgb, res.dominant) == max(rs)
assert any("Best-of-3" in n for n in res.notes)
def test_best_of_default_single_call():
"""n_candidates default (1) keeps exactly one call per layer — no cost surprise."""
pre = _toy_scan()
calls = {"n": 0}
def hook(rgb, prompt):
calls["n"] += 1
return rgb
recover(pre.rgb, pre.h_total, pre.confidence_mask, restore_fn=hook)
assert calls["n"] == 2 # headline + second layer, one each
def test_promote_alternate_returns_clicked_take():
"""Clicking take N in the gallery returns that take's image (pairs contract)."""
import app.main as m
takes = [(np.full((4, 4, 3), v, np.float32), 0.9 - 0.1 * k)
for k, v in enumerate((0.2, 0.5, 0.8))]
ev = type("E", (), {"index": 2})()
img, _status = m.promote_alternate(ev, takes, "")
assert img is not None
assert np.allclose(np.asarray(img, np.float32) / 255.0, 0.8, atol=0.01)
# out-of-range and empty stay no-ops
ev_bad = type("E", (), {"index": 9})()
upd, upd2 = m.promote_alternate(ev_bad, takes, "")
assert not isinstance(upd, type(img))
upd3, upd4 = m.promote_alternate(ev, None, "")
assert not isinstance(upd3, type(img))
def test_budget_composition_keeps_hints_and_context_with_long_description():
"""A max-length scene description must not crowd out tag hints or photo context
(the 'my text got cut off' regression): all three pieces reach the prompt."""
from app.restore import MAX_SCENE_CHARS
pre = _toy_scan()
h, w = pre.rgb.shape[:2]
sa = np.zeros((h, w), bool); sa[10:20, 10:30] = True
sb = np.zeros((h, w), bool); sb[60:70, 60:80] = True
long_desc = ("green table patio detail " * 40).strip() # ~1000 chars, > cap
prompts: list[str] = []
notes: list[str] = []
recover(pre.rgb, pre.h_total, pre.confidence_mask,
restore_fn=lambda rgb, p: prompts.append(p) or rgb,
scene_headline=long_desc, scene_other="museum wall",
context="honeymoon roll, patio over museum",
seeds_headline=sa, seeds_other=sb,
hints_headline="the pool (top left)", hints_other="the frames (top right)",
notes=notes)
assert len(prompts) == 2
# hints AND context both survived the over-long description
assert "the pool (top left)" in prompts[0]
assert "honeymoon roll" in prompts[0]
# the description itself was capped with a user-visible note
assert any("longer than" in n and "description" in n for n in notes)
# and no silent tail-slice fired in restore_layer
assert not any("prompt budget" in n for n in notes)
def test_canvas_from_upload_matches_preprocess_geometry_for_huge_upload(monkeypatch):
"""The marking canvas must share geometry with the working image even when the
intake megapixel guard downscales a huge upload."""
import warnings
from PIL import Image as PILImage
import app.preprocessing as pp
from app.preprocessing import preprocess_negative
# Shrink the guard so the test doesn't need a real >50MP allocation
monkeypatch.setattr(pp, "INTAKE_MAX_MEGAPIXELS", 0.02) # 20k px cap
big = PILImage.fromarray(
(np.random.default_rng(0).random((300, 400, 3)) * 255).astype(np.uint8)
) # 0.12 MP > cap
with warnings.catch_warnings():
warnings.simplefilter("ignore")
pre = preprocess_negative(big, stock="Portra 400", scan_type="positive")
# the canvas/tap/repair surfaces all share this module-level intake
import app.main as m
canvas = m._working_frame_from_upload(big)
assert canvas is not None
assert canvas.shape[:2] == pre.rgb.shape[:2], (
f"canvas {canvas.shape[:2]} != working image {pre.rgb.shape[:2]} — "
f"scribble strokes would be misregistered"
)
def test_vlm_gated_on_replicate_token_p0_privacy(monkeypatch):
"""P0-privacy: with ANTHROPIC available but NO Replicate token, the photo must
NOT be sent to Anthropic (its descriptions could never be consumed)."""
monkeypatch.delenv("REPLICATE_API_TOKEN", raising=False)
pre = _toy_scan()
calls = {"n": 0}
def vlm_spy(pil, q):
calls["n"] += 1
return "a scene"
res = recover(pre.rgb, pre.h_total, pre.confidence_mask, vlm=vlm_spy)
assert calls["n"] == 0, "photo sent to Anthropic despite no Replicate token"
assert res.api_contacted is False
def test_vlm_receives_context_and_fills_only_missing(monkeypatch):
"""VLM fallback passes the user's context through and only fills MISSING scenes."""
monkeypatch.setenv("REPLICATE_API_TOKEN", "tok")
pre = _toy_scan()
seen_questions: list[str] = []
def vlm_spy(pil, q):
seen_questions.append(q)
return "vlm-scene"
prompts: list[str] = []
# restore_fn present would skip the VLM; instead patch the network boundary.
import app.restore as r
monkeypatch.setattr(
r, "_replicate_restore",
lambda rgb, prompt, slug, max_side, meta=None, refs=None: (prompts.append(prompt), rgb)[1],
)
notes: list[str] = []
res = recover(pre.rgb, pre.h_total, pre.confidence_mask, vlm=vlm_spy,
scene_headline="user headline", scene_other=None,
context="honeymoon roll context", notes=notes)
# context reached the VLM call (identify_scenes folds it into the question)
assert any("honeymoon roll context" in q for q in seen_questions)
# supplied description survives; only the missing one came from the VLM
assert any("user headline" in p for p in prompts)
assert any("vlm-scene" in p for p in prompts)
# Anthropic contact is disclosed
assert res.api_contacted is True
assert any("Anthropic" in n for n in notes)
# ---------------------------------------------------------------------------
# WP-18 D2 — status honesty
# ---------------------------------------------------------------------------
def test_promote_alternate_rerenders_drift_line(monkeypatch):
"""D2a: promoting a take rewrites the status for THAT take; a drifted take
gains the warning; list-style gallery indices are normalized."""
import app.main as m
from app.restore import DRIFT_R_THRESHOLD
good = np.full((4, 4, 3), 0.5, np.float32)
bad = np.full((4, 4, 3), 0.9, np.float32)
alts = [(good, 0.85), (bad, DRIFT_R_THRESHOLD - 0.1)]
ev_ok = type("E", (), {"index": 1})()
img, status = m.promote_alternate(ev_ok, alts, "old text\n\n**Showing take 1** (faithfulness r=0.85)")
assert img is not None
assert "Showing take 2" in status and "drifted" in status
assert status.count("**Showing take") == 1 # old line stripped, one current line
# list-style index (row, col)
ev_list = type("E", (), {"index": [0, 0]})()
img2, status2 = m.promote_alternate(ev_list, alts, "x")
assert img2 is not None and "Showing take 1" in status2 and "drifted" not in status2
def test_second_scene_copy_names_densitometry_failure(monkeypatch):
"""D2b: with h_total=None the status blames densitometry, not rate limits."""
import app.main as m
pre = _toy_scan()
bs = {"observed_rgb": pre.rgb, "h_total": None, "confidence_mask": None,
"film_stock": "Portra 400", "trim_bbox_frac": None}
monkeypatch.setenv("REPLICATE_API_TOKEN", "tok")
import app.restore as r
monkeypatch.setattr(r, "_replicate_restore",
lambda rgb, prompt, slug, max_side, meta=None: rgb)
_dom, _sec, status, _g, _s = m.restore_best_scene(bs, "Scene 1", "a", "b")
assert "densitometry" in status
assert "rate-limit" not in status
def test_api_contacted_false_on_prenetwork_failure(monkeypatch):
"""D2c: an exception BEFORE the network call must not claim the photo was sent."""
import app.restore as r
monkeypatch.setenv("REPLICATE_API_TOKEN", "tok")
def boom(rgb, prompt, slug, max_side, meta=None):
raise RuntimeError("pre-network failure (no upload happened)")
monkeypatch.setattr(r, "_replicate_restore", boom)
notes: list[str] = []
out, meta = r.restore_layer(np.zeros((4, 4, 3), np.float32), "x", notes=notes)
assert out is None
assert meta["api_contacted"] is False # nothing left the process
assert any("error" in n.lower() for n in notes)
# ---------------------------------------------------------------------------
# WP-18 D3 — robustness
# ---------------------------------------------------------------------------
def test_backoff_classifies_throttle_by_status_or_word(monkeypatch):
"""D3a: structured 429 retried; 'throttled' text retried; a bare '429'
substring in an id/URL raises immediately (no 26s of sleeps)."""
from app.restore import _run_with_backoff
sleeps: list[float] = []
monkeypatch.setattr("time.sleep", lambda s: sleeps.append(s))
class Structured(Exception):
status = 429
calls = {"n": 0}
def structured_flaky():
calls["n"] += 1
if calls["n"] < 2:
raise Structured("rate limited")
return "ok"
assert _run_with_backoff(structured_flaky) == "ok"
def id_contains_429():
raise RuntimeError("prediction id x4295f failed: invalid input")
sleeps.clear()
with pytest.raises(RuntimeError):
_run_with_backoff(id_contains_429)
assert sleeps == [] # no retry sleeps on a hard failure
def test_slope_bounds_cached_across_fresh_curves():
"""D3b: two fresh curves of the same preset compute the LUT sweep ONCE."""
import densitometry as d
from film_physics import get_film_curve
d._SLOPE_BOUNDS_CACHE.clear()
sweeps = {"n": 0}
orig = np.gradient
def counting_gradient(*a, **k):
sweeps["n"] += 1
return orig(*a, **k)
d.np.gradient, saved = counting_gradient, d.np.gradient
try:
b1 = d._slope_valid_bounds(get_film_curve("Portra 400"))
b2 = d._slope_valid_bounds(get_film_curve("Portra 400")) # fresh object
finally:
d.np.gradient = saved
assert b1 == b2
assert sweeps["n"] == 1, f"LUT sweep ran {sweeps['n']}x for the same preset"
def test_scene2_choice_honored_with_empty_boxes(monkeypatch):
"""D3c: empty boxes + Scene 2 picked => the VLM's SECONDARY scene leads."""
monkeypatch.setenv("REPLICATE_API_TOKEN", "tok")
pre = _toy_scan()
answers = iter(["the dominant castle", "the secondary person"])
prompts: list[str] = []
import app.restore as r
monkeypatch.setattr(r, "_replicate_restore",
lambda rgb, prompt, slug, max_side, meta=None, refs=None: (prompts.append(prompt), rgb)[1])
r.recover(pre.rgb, pre.h_total, pre.confidence_mask,
vlm=lambda pil, q: next(answers), headline_is_secondary=True)
# WP-19 bundle prompts name BOTH scenes; the recover target is the one after
# "Recover this photo:" — assert the pick leads there, not merely appears.
assert "Recover this photo: the secondary person" in prompts[0]
assert "Recover this photo: the dominant castle" in prompts[1]
# ---------------------------------------------------------------------------
# WP-19 — evidence-bundle restore (nano-banana-2 default)
# ---------------------------------------------------------------------------
def test_bundle_prompt_names_enemy_and_numbers_references():
"""The bundle prompt embeds the legend, numbers images to match the refs, and
names the other scene as removable contamination (the 128 failure fix)."""
from app.restore import build_bundle_prompt
p = build_bundle_prompt(
"a gallery wall with four framed prints", "a sunny patio with two women",
has_markup=True, has_layer=True,
legend="red strokes mark 'paintings' — belongs to this photo",
)
assert "Image 2 is image 1 with the user's hand-drawn annotations" in p
assert "red strokes mark 'paintings'" in p
assert "Image 3 is a rough physics-based separation" in p
assert "Recover this photo: a gallery wall with four framed prints" in p
assert "a sunny patio with two women" in p and "contamination" in p
assert "never solidify" in p
# without markup, the layer takes the image-2 slot
p2 = build_bundle_prompt("a", "b", has_markup=False, has_layer=True)
assert "Image 2 is a rough physics-based separation" in p2
assert "Image 3" not in p2
def test_model_inputs_families():
"""kontext = single input_image; google = image_input array + 1K matched; flux-2 = input_images."""
import io as _io
from app.restore import _model_inputs
bufs = [_io.BytesIO(b"a"), _io.BytesIO(b"b")]
k = _model_inputs("black-forest-labs/flux-kontext-pro", bufs, "p")
assert k["input_image"] is bufs[0] and "image_input" not in k
g = _model_inputs("google/nano-banana-2", bufs, "p")
assert g["image_input"] == bufs
assert g["resolution"] == "1K" and g["aspect_ratio"] == "match_input_image"
f = _model_inputs("black-forest-labs/flux-2-pro", bufs, "p")
assert f["input_images"] == bufs and "image_input" not in f
def test_recover_threads_markup_and_layer_refs(monkeypatch):
"""On the live path the headline call carries [markup, layer] references and
the per-scene legend reaches the prompt."""
monkeypatch.setenv("REPLICATE_API_TOKEN", "tok")
pre = _toy_scan()
h, w = pre.rgb.shape[:2]
seeds_h = np.zeros((h, w), bool); seeds_h[2:6, 2:20] = True
seeds_o = np.zeros((h, w), bool); seeds_o[40:44, 40:60] = True
seen: list[dict] = []
import app.restore as r
def spy(rgb, prompt, slug, max_side, meta=None, refs=None):
seen.append({"prompt": prompt, "n_refs": len(refs or [])})
return rgb
monkeypatch.setattr(r, "_replicate_restore", spy)
r.recover(pre.rgb, pre.h_total, pre.confidence_mask,
scene_headline="castle", scene_other="person",
seeds_headline=seeds_h, seeds_other=seeds_o,
markup_rgb=pre.rgb, legend_headline="red = castle (this photo)",
legend_other="red = castle (the other photo)")
assert len(seen) == 2
assert all(c["n_refs"] == 2 for c in seen) # markup + physics layer, both calls
assert "red = castle (this photo)" in seen[0]["prompt"]
assert "red = castle (the other photo)" in seen[1]["prompt"]
def test_referee_ranks_candidates_over_r():
"""WP-19: with a VLM referee, the semantically best take wins even when r
disagrees; the note discloses the referee and the Anthropic contact."""
pre = _toy_scan()
takes = iter([0.5, 0.7, 0.9])
def hook(rgb, prompt):
return np.clip(rgb * next(takes), 0, 1)
scores = iter(["3", "9 out of 10", "5"]) # candidate 2 wins semantically
notes: list[str] = []
res = recover(pre.rgb, pre.h_total, pre.confidence_mask, restore_fn=hook,
vlm=lambda pil, q: next(scores),
scene_headline="castle", scene_other="person",
n_candidates=3, notes=notes)
assert len(res.alternates) == 3
expected = np.clip(pre.rgb * 0.7, 0, 1)
assert np.allclose(res.dominant, expected, atol=1e-6)
assert any("referee" in n and "Anthropic" in n for n in notes)
def test_referee_failure_falls_back_to_r_ranking():
"""Every referee call failing (no digits) -> r-ranking, classic best-of note."""
pre = _toy_scan()
rng = np.random.default_rng(3)
calls = {"n": 0}
def hook(rgb, prompt):
calls["n"] += 1
if calls["n"] == 1:
return np.clip(rgb * 1.1, 0, 1) # the faithful take
return rng.random(rgb.shape).astype(np.float32)
notes: list[str] = []
res = recover(pre.rgb, pre.h_total, pre.confidence_mask, restore_fn=hook,
vlm=lambda pil, q: "no rating possible",
scene_headline="castle", scene_other="person",
n_candidates=3, notes=notes)
rs = [r for _img, r in res.alternates]
assert rs == sorted(rs, reverse=True)
assert any("faithfulness scores" in n for n in notes)
def test_run_with_backoff_retries_transient_ssl_not_hard_errors():
"""WP-19: SSL/transport blips retry (observed live 2026-07-19); hard errors raise at once."""
from app.restore import _run_with_backoff
state = {"n": 0}
def flaky():
state["n"] += 1
if state["n"] < 3:
raise Exception("[SSL: SSLV3_ALERT_BAD_RECORD_MAC] ssl/tls alert bad record mac")
return "ok"
assert _run_with_backoff(flaky, attempts=3, wait_s=0.01) == "ok"
assert state["n"] == 3
def hard():
state["n"] += 1
raise ValueError("model does not exist")
state["n"] = 0
with pytest.raises(ValueError):
_run_with_backoff(hard, attempts=3, wait_s=0.01)
assert state["n"] == 1 # no retries burned on a hard failure
# ---------------------------------------------------------------------------
# WP-20 — identity references, corrective retry, second-scene best-of
# ---------------------------------------------------------------------------
def test_bundle_prompt_identity_role_and_numbering():
from app.restore import build_bundle_prompt
p = build_bundle_prompt("a", "b", has_markup=True, has_layer=True, n_identity=1)
assert "Image 4 is a separate REAL photograph" in p
assert "true appearance" in p and "do NOT copy its pose" in p
p2 = build_bundle_prompt("a", "b", n_identity=1)
assert "Image 2 is a separate REAL photograph" in p2 and "Image 3" not in p2
# WP-21: several reference photos are numbered as a range
p3 = build_bundle_prompt("a", "b", has_markup=True, n_identity=3)
assert "Images 3-5 are separate REAL photographs" in p3
def test_recover_threads_identity_reference(monkeypatch):
"""identity_headline rides as the LAST reference of the headline call only."""
monkeypatch.setenv("REPLICATE_API_TOKEN", "tok")
pre = _toy_scan()
h, w = pre.rgb.shape[:2]
seeds_h = np.zeros((h, w), bool); seeds_h[2:6, 2:20] = True
seeds_o = np.zeros((h, w), bool); seeds_o[40:44, 40:60] = True
ident = np.full((10, 10, 3), 0.5, np.float32)
seen: list[dict] = []
import app.restore as r
def spy(rgb, prompt, slug, max_side, meta=None, refs=None):
seen.append({"prompt": prompt, "n_refs": len(refs or []),
"last_is_ident": bool(refs) and refs[-1].shape == (10, 10, 3)})
return rgb
monkeypatch.setattr(r, "_replicate_restore", spy)
r.recover(pre.rgb, pre.h_total, pre.confidence_mask,
scene_headline="castle", scene_other="person",
seeds_headline=seeds_h, seeds_other=seeds_o,
markup_rgb=pre.rgb, identity_headline=ident)
assert seen[0]["n_refs"] == 3 and seen[0]["last_is_ident"] # markup+layer+identity
assert "true appearance" in seen[0]["prompt"]
assert seen[1]["n_refs"] == 2 and not seen[1]["last_is_ident"] # no identity_other
def test_referee_critique_drives_one_corrective_retry():
"""Best take under REFEREE_RETRY_BELOW with a critique -> exactly one retry whose
prompt carries the critique; the retry's better score wins."""
pre = _toy_scan()
factors = iter([0.5, 0.6, 0.7, 0.9])
prompts: list[str] = []
def hook(rgb, prompt):
prompts.append(prompt)
return np.clip(rgb * next(factors), 0, 1)
scores = iter(["5 | three women are visible but the scene should have two",
"4 | frames from the other photo remain",
"3 | heavy contamination",
"9 | none"])
notes: list[str] = []
res = recover(pre.rgb, None, None, restore_fn=hook, # no h_total: headline only
vlm=lambda pil, q: next(scores),
scene_headline="castle", scene_other="person",
n_candidates=3, notes=notes)
assert len(prompts) == 4 # 3 takes + 1 corrective retry
assert "three women are visible" in prompts[3] and "Do not repeat" in prompts[3]
assert np.allclose(res.dominant, np.clip(pre.rgb * 0.9, 0, 1), atol=1e-6)
assert any("corrective retry" in n for n in notes)
assert len(res.alternates) == 4 # the retry joins the gallery
def test_second_scene_joins_best_of_with_referee():
"""n_candidates=3 -> the second scene runs best-of-2, referee-ranked."""
pre = _toy_scan()
factors = iter([0.5, 0.7, 0.9, 0.6, 0.8])
def hook(rgb, prompt):
return np.clip(rgb * next(factors), 0, 1)
scores = iter(["3 | a", "9 | none", "5 | b", # headline: best 9, no retry
"8 | none", "6 | c"]) # second: first take wins
res = recover(pre.rgb, pre.h_total, pre.confidence_mask, restore_fn=hook,
vlm=lambda pil, q: next(scores),
scene_headline="castle", scene_other="person",
n_candidates=3)
assert len(res.alternates) == 3
assert np.allclose(res.dominant, np.clip(pre.rgb * 0.7, 0, 1), atol=1e-6)
assert res.second is not None
assert np.allclose(res.second, np.clip(pre.rgb * 0.6, 0, 1), atol=1e-6)
# ---------------------------------------------------------------------------
# WP-21 — multi-reference identities, 2K finalize
# ---------------------------------------------------------------------------
def test_identity_list_capped_at_budget(monkeypatch):
monkeypatch.setenv("REPLICATE_API_TOKEN", "tok")
import app.restore as r
pre = _toy_scan()
seen = {}
def spy(rgb, prompt, slug, max_side, meta=None, refs=None):
seen.setdefault("n_refs", len(refs or []))
return rgb
monkeypatch.setattr(r, "_replicate_restore", spy)
notes: list[str] = []
ident = [np.full((8, 8, 3), 0.5, np.float32)] * 6 # over the 4-per-scene budget
r.recover(pre.rgb, None, None, scene_headline="castle", scene_other="person",
identity_headline=ident, notes=notes)
assert seen["n_refs"] == 1 + 4 # anchor layer + capped identities
assert any("reference budget" in n for n in notes)
def test_finalize_take_offline_hook_and_drift_note():
from app.restore import finalize_take
rgb = np.tile(np.linspace(0, 1, 64, dtype=np.float32)[None, :, None], (48, 1, 3))
rgb[10:20, 10:30] = 0.9
out, meta = finalize_take(rgb, restore_fn=lambda im, prompt: im * 0.98)
assert out is not None and meta["api_contacted"] is False
notes: list[str] = []
rng = np.random.default_rng(7)
finalize_take(rgb, restore_fn=lambda im, prompt: rng.random(im.shape, np.float32),
notes=notes)
assert any("drifted" in n for n in notes)
def test_finalize_take_soft_fails_without_token(monkeypatch):
from app.restore import finalize_take
monkeypatch.delenv("REPLICATE_API_TOKEN", raising=False)
notes: list[str] = []
out, meta = finalize_take(np.zeros((8, 8, 3), np.float32), notes=notes)
assert out is None and meta["api_contacted"] is False
assert any("REPLICATE_API_TOKEN" in n for n in notes)
def test_fallback_chain_on_hard_failure(monkeypatch):
"""A rotated/404 primary editor falls through to the backups with a note."""
import app.restore as r
seen = []
def fake_replicate_restore(rgb, prompt, slug, max_side, meta=None, refs=None, resolution="1K"):
seen.append(slug)
if "nano-banana-2" in slug:
raise RuntimeError("404 Not Found: gemini preview retired")
return rgb
monkeypatch.setattr(r, "_replicate_restore", fake_replicate_restore)
notes: list[str] = []
meta: dict = {}
out = r._restore_with_fallback(
np.zeros((8, 8, 3), np.float32), "p", "google/nano-banana-2", 1024,
meta, notes=notes,
)
assert out is not None
assert seen == ["google/nano-banana-2", "google/nano-banana-pro"]
assert meta["model"] == "google/nano-banana-pro"
assert any("backup model" in n for n in notes)
def test_fallback_chain_reraises_throttle(monkeypatch):
"""Throttles are account-wide: no pointless fallback, the 429 surfaces."""
import app.restore as r
def throttled(rgb, prompt, slug, max_side, meta=None, refs=None, resolution="1K"):
raise RuntimeError("Request was throttled (6/min)")
monkeypatch.setattr(r, "_replicate_restore", throttled)
import pytest
with pytest.raises(RuntimeError, match="throttled"):
r._restore_with_fallback(
np.zeros((8, 8, 3), np.float32), "p", "google/nano-banana-2", 1024, {},
)