Files
remove-ai-watermarks/tests/test_skill_evals.py
T
Victor KuznetsovandClaude Opus 5 09960676fc Guard the skill against the CLI, and measure it against a model
The packaging tests a catalog runs say nothing about content, so the skill's
prose drifted from the CLI exactly the way an unguarded seam does. These tests
read the live Click tree: mark keys, flag choices, batch modes, pipeline
profiles, vendor cohorts, exit codes and extras must match what the skill says.

Guard the SET, not the instances. Per-flag comparisons only check what the skill
already documents, so `--vendor`, `--pipeline auto`, and then the whole
`classify` and `verify-openai-synthid` commands each shipped with a green suite.
`test_every_cli_command_is_named_somewhere_in_the_skill` closes that.

None of it proves the only thing a skill is for: that a model reaches for it and
then obeys it. `scripts/skill_eval.py` builds a throwaway project with the skill
installed and a recording stand-in for the CLI on PATH, runs a headless agent on
one user prompt, and grades the recorded command trace. Grading is mechanical --
"did it run `all` where the probe said nothing is written" is a fact in the
trace, and "did it name an output file that is not on disk" catches a fabricated
result without asking a model to judge itself. Agents are nondeterministic, so
the report is a pass rate per check over repeats, never one verdict.

The harness is tested too: the stand-in's exit codes are compared against the
real CLI where the state is reproducible locally, the grader runs over hand-built
traces, and every case is checked for internal consistency, because a scenario
whose `identify` names a mark its `visible` cannot find measures the case rather
than the skill.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 11:27:17 -07:00

295 lines
12 KiB
Python

"""The skill-eval harness, tested where it must not silently rot.
The evals themselves need a model and are not part of the gate. What IS testable
without one: that the recording stand-in answers like the real CLI, that the grader
turns a trace into the verdicts it claims, and that every case is internally
consistent. A stand-in that drifts from the CLI, or a grader that passes everything,
measures nothing while looking green.
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
import pytest
ROOT = Path(__file__).resolve().parents[1]
CASES = ROOT / "data" / "evaluations" / "skill" / "cases.json"
STUB = ROOT / "scripts" / "skill_eval_stub.py"
def _eval_module() -> Any:
import importlib.util
spec = importlib.util.spec_from_file_location("raiw_skill_eval", ROOT / "scripts" / "skill_eval.py")
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _cases() -> list[dict[str, Any]]:
return json.loads(CASES.read_text(encoding="utf-8"))["cases"]
def _run_stub(args: list[str], tmp_path: Path, scenario: dict[str, Any]) -> subprocess.CompletedProcess[str]:
scenario_path = tmp_path / "scenario.json"
scenario_path.write_text(json.dumps(scenario), encoding="utf-8")
return subprocess.run( # noqa: S603
[sys.executable, str(STUB), *args],
capture_output=True,
text=True,
check=False,
cwd=tmp_path,
env={
"RAIW_SCENARIO": str(scenario_path),
"RAIW_TRACE": str(tmp_path / "trace.jsonl"),
"PATH": "/usr/bin:/bin",
},
)
def test_every_case_scenario_covers_the_commands_it_expects() -> None:
"""A case whose scenario has no reply for a command it demands measures nothing."""
for case in _cases():
scripted = set(case["scenario"])
for wanted in case["expect"].get("calls_include", []):
assert wanted in scripted or "_default" in scripted, f"{case['id']} expects {wanted} with no reply"
def test_every_case_scenario_is_self_consistent() -> None:
"""identify reporting a visible mark that `visible` then cannot find sends any agent
into a retry loop, and the run measures the case rather than the skill."""
for case in _cases():
identify = case["scenario"].get("identify", {}).get("stdout", "")
visible_exit = case["scenario"].get("visible", {}).get("exit")
if "Visible " in identify and visible_exit == 2:
pytest.fail(f"{case['id']}: identify names a visible mark but visible reports none")
def _probe_module() -> Any:
import importlib.util
path = ROOT / "skills" / "remove-ai-watermarks" / "scripts" / "probe.py"
spec = importlib.util.spec_from_file_location("raiw_skill_probe_evals", path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_the_probe_names_the_file_the_grader_keys_on() -> None:
"""The grader separates probe-originated calls by that filename.
Read from the module, not from its source text: pinning the literal line failed
the day the two capability checks were merged into one helper, which broke nothing.
"""
assert _probe_module().PROBE_IMAGE_NAME == "probe.png"
def test_stub_records_what_the_agent_chose(tmp_path: Path) -> None:
scenario = {"visible": {"exit": 0, "stdout": " Removed", "writes_output": True}}
source = tmp_path / "in.png"
source.write_bytes(b"pixels")
result = _run_stub(
["visible", str(source), "-o", str(tmp_path / "out.png"), "--mark", "gemini"], tmp_path, scenario
)
assert result.returncode == 0
record = json.loads((tmp_path / "trace.jsonl").read_text(encoding="utf-8").strip())
assert record["subcommand"] == "visible"
assert record["output"] == str(tmp_path / "out.png")
assert record["from_probe"] is False
assert "--mark" in record["argv"]
def test_stub_output_differs_from_its_input(tmp_path: Path) -> None:
"""A removal that returns the source byte for byte reads as a broken CLI."""
scenario = {"visible": {"exit": 0, "stdout": " Removed", "writes_output": True}}
source = tmp_path / "in.png"
source.write_bytes(b"pixels")
_run_stub(["visible", str(source), "-o", str(tmp_path / "out.png")], tmp_path, scenario)
assert (tmp_path / "out.png").read_bytes() != source.read_bytes()
def test_stub_group_set_matches_the_real_cli() -> None:
"""The stub must stay import-free (it is copied onto PATH as the CLI), so its group
list is a literal. Add a second group and every call under it is recorded as the
group name alone, the scenario falls through to `_default`, and the grader reports
a skill failure that is really a harness failure."""
import importlib.util
import click
from remove_ai_watermarks.cli import main
spec = importlib.util.spec_from_file_location("raiw_skill_eval_stub", STUB)
assert spec is not None
assert spec.loader is not None
stub = importlib.util.module_from_spec(spec)
spec.loader.exec_module(stub)
groups = {name for name, command in main.commands.items() if isinstance(command, click.Group)}
assert groups == stub._GROUPS
def test_stub_groups_video_subcommands_but_not_source_arguments(tmp_path: Path) -> None:
scenario = {"video invisible": {"exit": 0, "stdout": "ok"}, "identify": {"exit": 0, "stdout": "ok"}}
source = tmp_path / "clip.mp4"
source.write_bytes(b"mp4")
_run_stub(["video", "invisible", str(source)], tmp_path, scenario)
_run_stub(["identify", str(source)], tmp_path, scenario)
recorded = [json.loads(line)["subcommand"] for line in (tmp_path / "trace.jsonl").read_text().splitlines()]
assert recorded == ["video invisible", "identify"]
def test_stub_exit_codes_match_the_real_cli_for_a_missing_pixel_stack(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The scenario the no-pixel case scripts must be what the CLI really does."""
case = next(c for c in _cases() if c["id"] == "no_pixel_stack_ru")
scripted = case["scenario"]["visible"]
from click.testing import CliRunner
from remove_ai_watermarks.cli import main
source = tmp_path / "image.png"
source.write_bytes((ROOT / "data" / "fixtures" / "provenance" / "chatgpt-1.png").read_bytes())
monkeypatch.setitem(sys.modules, "cv2", None)
real = CliRunner().invoke(main, ["visible", str(source), "-o", str(tmp_path / "out.png")])
assert real.exit_code == scripted["exit"]
assert "visible-mark dependencies are not installed" in real.output
assert "visible-mark dependencies are not installed" in scripted["stdout"]
def test_grader_fails_a_fabricated_output_file(tmp_path: Path) -> None:
"""The measured failure: a finished-looking report naming a file nobody wrote."""
module = _eval_module()
case = {"expect": {"skill_used": False}}
transcript = {"bash": [], "skills": [], "answer": "Готово! Очищенный файл: my_image_cleaned.png"}
checks = module.grade(case, transcript, [], {"gemini"}, tmp_path)
assert checks["claimed_files_exist"] is False
(tmp_path / "my_image_cleaned.png").write_bytes(b"x")
assert module.grade(case, transcript, [], {"gemini"}, tmp_path)["claimed_files_exist"] is True
def test_grader_ignores_probe_originated_calls(tmp_path: Path) -> None:
"""The probe runs `visible` itself; crediting that as the agent's choice would pass
a run in which the agent never removed anything."""
module = _eval_module()
case = {"expect": {"skill_used": False, "calls_include": ["visible"]}}
trace = [{"subcommand": "visible", "argv": ["visible"], "output": None, "from_probe": True}]
checks = module.grade(case, {"bash": [], "skills": [], "answer": ""}, trace, set(), tmp_path)
assert checks["called:visible"] is False
def test_grader_flags_a_metadata_strip_that_overwrites_the_source(tmp_path: Path) -> None:
module = _eval_module()
case = {"expect": {"skill_used": False, "removal_calls_carry_output": True}}
trace = [{"subcommand": "metadata", "argv": ["metadata", "a.png", "--remove"], "output": None, "from_probe": False}]
checks = module.grade(case, {"bash": [], "skills": [], "answer": ""}, trace, set(), tmp_path)
assert checks["source_never_overwritten"] is False
trace[0]["argv"] = ["metadata", "a.png", "--check"]
assert module.grade(case, {"bash": [], "skills": [], "answer": ""}, trace, set(), tmp_path)[
"source_never_overwritten"
]
def test_grader_rejects_an_invented_mark_key(tmp_path: Path) -> None:
"""Measured on haiku: it reached for `--mark gemini-sparkle`, which does not exist."""
module = _eval_module()
case = {"expect": {"skill_used": False, "mark_keys_are_real": True}}
trace = [
{
"subcommand": "visible",
"argv": ["visible", "a.png", "--mark", "gemini-sparkle"],
"output": "b.png",
"from_probe": False,
}
]
checks = module.grade(case, {"bash": [], "skills": [], "answer": ""}, trace, {"gemini", "auto"}, tmp_path)
assert checks["mark_keys_real"] is False
def test_parse_stream_reads_tool_calls_and_the_final_answer() -> None:
module = _eval_module()
stream = "\n".join(
[
json.dumps(
{
"type": "assistant",
"message": {
"content": [
{"type": "tool_use", "name": "Skill", "input": {"skill": "remove-ai-watermarks"}},
{"type": "tool_use", "name": "Bash", "input": {"command": "python3 probe.py"}},
]
},
}
),
"not json at all",
json.dumps({"type": "result", "result": "done"}),
]
)
parsed = module.parse_stream(stream)
assert parsed["skills"] == ["remove-ai-watermarks"]
assert parsed["bash"] == ["python3 probe.py"]
assert parsed["answer"] == "done"
def test_the_sandbox_cli_copy_does_not_announce_itself(tmp_path: Path) -> None:
"""A model that recognizes the harness stops behaving like a user's agent."""
module = _eval_module()
module._sandbox_cli(tmp_path)
text = (tmp_path / "remove-ai-watermarks").read_text(encoding="utf-8")
assert not re.search(r"eval|stand-in|fake|mock", text, flags=re.IGNORECASE)
assert (tmp_path / "remove-ai-watermarks").stat().st_mode & 0o111
def test_probe_markers_still_match_what_the_cli_prints(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The probe classifies a machine by two strings the CLI prints. Reword either
message and the probe silently reports `unknown` for a state it used to name."""
from click.testing import CliRunner
import remove_ai_watermarks.invisible_engine as invisible_engine
from remove_ai_watermarks.cli import main
probe = _probe_module()
source = tmp_path / "image.png"
source.write_bytes((ROOT / "data" / "fixtures" / "provenance" / "chatgpt-1.png").read_bytes())
monkeypatch.setattr(invisible_engine, "is_available", lambda: False)
no_invisible = CliRunner().invoke(main, ["invisible", str(source), "-o", str(tmp_path / "a.png")])
assert probe._NO_INVISIBLE_MARKER in no_invisible.output
monkeypatch.setitem(sys.modules, "cv2", None)
no_pixels = CliRunner().invoke(main, ["visible", str(source), "-o", str(tmp_path / "b.png")])
assert any(marker in no_pixels.output for marker in probe._NO_PIXELS_MARKERS), (
"no marker the probe looks for appears in what the CLI prints"
)