Fix what the verification pass found in the knob removal

An adversarial review of 52b2c11 (five independent audits, each finding put to
two skeptics, plus a completeness critic) found four defects that commit
introduced and several stale claims it should have caught.

The install hint no longer installs -- again. Folding five hints into one
INVISIBLE_EXTRA constant dropped the shell quoting the originals had, so the
printed remediation was `pip install remove-ai-watermarks[qwen-zimage]`. Bare
brackets are a glob in zsh, the macOS default shell: it dies with "no matches
found" before pip runs. That is the exact failure 52b2c11 existed to stop
producing, reintroduced in a different form by a bulk replace. The constant is
quoted now, and a test asserts the quotes rather than the bare substring -- the
old assertions passed either way, which is why nothing caught it.

Three tests were not guarding what they claimed:

- The commit's headline behaviour change, per-profile polish resolution inside
  the engine, had no test at all. Rebinding resolve_adaptive_polish to the
  pre-commit `bool(value)` left the full suite green. Now covered by a test that
  drives the real engine and observes whether humanizer.adaptive_polish ran;
  that mutation now fails it.
- TestAvailability still asserted the pre-commit (torch, diffusers) contract, so
  in a diffusion-only environment it was simply wrong, and comparing each gate to
  a tuple copied from itself could never catch the two gates disagreeing -- the
  drift the shared REMOVAL_MODULES was introduced to prevent. Replaced with a
  test that simulates each module's absence and requires BOTH gates to close.
- Both CUDA-refusal guards skipped in every environment, including CI: they were
  gated on the diffusion stack, which no CI job installs. The refusal fires
  before any torch attribute is read, so they now run everywhere; only the dtype
  assertion keeps its skip.

Also: the retired-knob test covered `invisible` but not `all` or `batch`, though
all three declared those options separately; and smoke_matrix.py still called
remove_watermark(region=...), a parameter 52b2c11 deleted, with the resulting
TypeError swallowed into a skip by a broad except.

Stale documentation the previous sweep missed: known-limitations still described
an MPS out-of-memory fallback and a lighter-pipeline escape that no code can
produce; module-internals declared Canny thresholds of 100/200 as a compatibility
contract while the code uses 13/64, attributed enable_model_cpu_offload to
deleted profiles, and still warned that the engine and CLI defaults differ (this
commit's predecessor made them identical); cli.md gated `all` on the `diffusion`
extra; python-api claimed "cuda" was the only accepted explicit device when
"auto" is too. The claim that `device` is not a parameter was wrong in both
module-internals and .claude/rules/development.md -- it is one, deliberately, and
now says so. `--cpu-offload` help and the pipeline's CUDA guard both still
pointed at MPS.

Not fixed here, reported instead -- both are outside this repo:
- ComfyUI-remove-ai-watermarks nodes.py:332 passes num_inference_steps and
  guidance_scale (plus min_resolution/upscaler from bf4bfc1). distribute.yml's
  comfyui job runs on every release and fails the release if the node sync fails,
  so 0.25.0 needs that node updated first.
- raiw-app modal_app.py:422-425 forwards the same two kwargs into
  remove_watermark. Latent: it is pinned to 1a77e24 and nothing supplies a value
  today, so it fires on the next pin bump.

pre-commit: 1) maintain.sh - exit 0 (1093 tests, Pyright 0 errors, no
vulnerabilities); 2) /simplify - not re-run, this commit is the applied output of
a five-dimension adversarial review; 3) docs sync - grepped MPS/mps, the extras
names and every symbol touched across README, docs/, scripts/, .claude/; updated
6 docs; 4) CLAUDE.md - corrected the device claim in .claude/rules/development.md
and added the shell-quoting rule

Verified by execution, not assertion: smoke_matrix --quick 51 pass / 0 fail,
_knob_rows driven directly 10 pass / 0 fail / 7 skip (no CUDA), the install hint
rendered and round-tripped through zsh, and each new test confirmed to fail
under the mutation it is meant to catch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-08-03 16:31:39 -07:00
co-authored by Claude Opus 5
parent 52b2c115e8
commit 2f72257996
13 changed files with 213 additions and 130 deletions
+60 -26
View File
@@ -14,6 +14,7 @@ import pytest
from remove_ai_watermarks._internal.utils import get_image_format, is_supported_format
from remove_ai_watermarks._internal.watermark_profiles import (
PROFILE_CHOICES,
REMOVAL_MODULES,
SDXL_ZIMAGE_GEMINI_STRENGTH,
SDXL_ZIMAGE_OPENAI_STRENGTH,
SDXL_ZIMAGE_UNKNOWN_STRENGTH,
@@ -67,20 +68,19 @@ class TestDeviceDetection:
here only defers a guaranteed failure to model-load time - several layers down,
after the dependency check and the pipeline import, under a message naming
whichever profile the internal pipeline happens to be.
Deliberately NOT gated on the diffusion stack. It used to be, and since no CI
job installs diffusers or diffsynth (the dev extra pulls torch only, via
invisible-watermark) the guard skipped in every environment it ran in --
including the maintainer's. The refusal fires before any torch attribute is
touched, so faking the dependency probe is enough to reach it.
"""
if not is_watermark_removal_available():
pytest.skip("torch/diffusers not installed")
import torch
from remove_ai_watermarks._internal import watermark_remover as module
from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover
for device in ("cpu", "mps", "xpu"):
with pytest.raises(ValueError, match="CUDA-only"):
WatermarkRemover(device=device)
remover = WatermarkRemover(device="cuda")
assert remover.device == "cuda"
assert remover.torch_dtype == torch.bfloat16
with patch.object(module, "is_watermark_removal_available", return_value=True):
for device in ("cpu", "mps", "xpu"):
with pytest.raises(ValueError, match="CUDA-only"):
module.WatermarkRemover(device=device)
def test_the_refusal_names_the_resolved_device_not_a_bare_none(self):
"""``device=None`` on a CUDA-less host must report "cpu", not "None".
@@ -88,16 +88,28 @@ class TestDeviceDetection:
The message used to interpolate the raw argument, so the common auto-detect
path told the user that ``'None'`` cannot run the removal.
"""
if not is_watermark_removal_available():
pytest.skip("torch/diffusers not installed")
from remove_ai_watermarks._internal import watermark_remover as module
with (
patch.object(module, "is_watermark_removal_available", return_value=True),
patch.object(module, "get_device", return_value="cpu"),
pytest.raises(ValueError, match="'cpu' cannot run it"),
):
module.WatermarkRemover(device=None)
def test_a_cuda_remover_picks_the_profile_dtype(self):
"""The dtype half still needs real torch, so it keeps its skip."""
if not is_watermark_removal_available():
pytest.skip("the qwen-zimage extra is not installed")
import torch
from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover
remover = WatermarkRemover(device="cuda")
assert remover.device == "cuda"
assert remover.torch_dtype == torch.bfloat16
assert WatermarkRemover(device="cuda", pipeline="sdxl-zimage").torch_dtype == torch.float16
class TestModelProfiles:
"""Only the two CUDA-only two-stage profiles remain."""
@@ -155,7 +167,7 @@ class TestNoReembeddedWatermark:
def test_sdxl_global_stage_disables_watermarker(self, monkeypatch: pytest.MonkeyPatch):
if not is_watermark_removal_available():
pytest.skip("torch/diffusers not installed")
pytest.skip("the qwen-zimage extra is not installed")
import diffusers
from remove_ai_watermarks._internal.sdxl_zimage_pipeline import SdxlZImagePipeline
@@ -315,25 +327,47 @@ class TestFormatUtils:
class TestAvailability:
"""Tests for dependency availability checks."""
"""The CLI gate and the remover precondition must answer from the same module list.
def test_watermark_removal_available(self):
# Reflects the actual environment: True iff torch + diffusers (the gpu
# extra) are importable. The default+dev CI env has no diffusers, so this
# must not assume the full stack is present.
import importlib.util
Both used to hardcode (torch, diffusers) while the code moved to REMOVAL_MODULES,
which includes diffsynth. In a torch+diffusers-only environment the assertions were
then simply wrong -- and, worse, comparing each gate against a tuple copied from
itself can never catch the two disagreeing, which is the drift that let the CLI pass
an environment the run then died in.
"""
expected = all(importlib.util.find_spec(m) is not None for m in ("torch", "diffusers"))
assert is_watermark_removal_available() is expected
def test_invisible_is_available(self):
def test_both_gates_agree_and_read_the_shared_module_list(self):
import importlib.util
from remove_ai_watermarks.invisible_engine import is_available
expected = all(importlib.util.find_spec(m) is not None for m in ("torch", "diffusers"))
assert "diffsynth" in REMOVAL_MODULES
expected = all(importlib.util.find_spec(m) is not None for m in REMOVAL_MODULES)
assert is_watermark_removal_available() is expected
assert is_available() is expected
def test_a_missing_module_closes_both_gates(self, monkeypatch: pytest.MonkeyPatch):
"""Discriminating, not vacuous: it must FAIL if either gate stops requiring one.
Comparing the live answer to a tuple derived from the same constant passes on
any host -- with the full stack (True == True) and with none of it
(False == False). Simulate each module's absence instead.
"""
import remove_ai_watermarks.invisible_engine as engine_module
from remove_ai_watermarks._internal import watermark_remover as remover_module
for missing in REMOVAL_MODULES:
present = {name: name != missing for name in REMOVAL_MODULES}
monkeypatch.setattr(
"remove_ai_watermarks.optional_deps.module_available",
lambda *names, _p=present: all(_p.get(n, True) for n in names),
)
# The remover probes at import time, so drive its cached flags directly.
monkeypatch.setattr(remover_module, "_HAS_TORCH", missing != "torch")
monkeypatch.setattr(remover_module, "_HAS_REMOVAL_MODULES", missing == "torch")
assert engine_module.is_available() is False, f"engine gate ignores a missing {missing}"
assert remover_module.is_watermark_removal_available() is False, f"remover gate ignores a missing {missing}"
# ── Platform-specific path handling ─────────────────────────────────