mirror of
https://github.com/guillaumemeyer/watermarks-remover.git
synced 2026-08-22 13:11:57 +02:00
CI gating for AI provenance marks already exists (audit_dir.py's -j concurrency + SARIF export from #101), but that only runs after a marked file has already been committed and pushed. Catch it at commit time instead, using git's own hook point. Adds two hooks via .pre-commit-hooks.yaml: - watermarks-remover-check: fails the commit and lists findings when staged files carry AI/C2PA marks. Wraps audit_lib.scan_file() / is_actionable() -- the exact per-file logic audit_dir.py already uses for CI, so the pre-commit gate and the CI gate agree on what counts as actionable. - watermarks-remover-clean (opt-in): rewrites staged files in place by shelling out to clean_file.py --in-place per file (no duplicated cleaning logic), then exits 1 so the developer reviews the diff and re-stages -- the same convention as auto-fixing hooks like ruff --fix. .pre-commit-hooks.yaml needed an explicit allow-rule in the deny-by- default .gitignore, same as every other root-level config file already listed there. Closes #135 Co-authored-by: Guillaume Meyer (The Opinionated Man) <1385518+guillaumemeyer@users.noreply.github.com>
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""Tests for the pre-commit hook wrappers (check_staged.py / clean_staged.py)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPTS = ROOT / "service" / "scripts"
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
|
|
import check_staged
|
|
import clean_staged
|
|
|
|
|
|
def _watermarked_text() -> str:
|
|
return "Hello" + chr(0x200B) + "World!"
|
|
|
|
|
|
def test_check_staged_clean_file_exits_0(tmp_path, monkeypatch, capsys):
|
|
f = tmp_path / "clean.txt"
|
|
f.write_text("Nothing to see here.", encoding="utf-8")
|
|
monkeypatch.setattr(sys, "argv", ["check_staged.py", str(f)])
|
|
assert check_staged.main() == 0
|
|
|
|
|
|
def test_check_staged_marked_file_exits_1(tmp_path, monkeypatch, capsys):
|
|
f = tmp_path / "marked.txt"
|
|
f.write_text(_watermarked_text(), encoding="utf-8")
|
|
monkeypatch.setattr(sys, "argv", ["check_staged.py", str(f)])
|
|
assert check_staged.main() == 1
|
|
err = capsys.readouterr().err
|
|
assert str(f) in err
|
|
assert "layer-a" in err
|
|
|
|
|
|
def test_check_staged_multiple_files_one_marked(tmp_path, monkeypatch, capsys):
|
|
clean = tmp_path / "clean.txt"
|
|
clean.write_text("plain text", encoding="utf-8")
|
|
marked = tmp_path / "marked.txt"
|
|
marked.write_text(_watermarked_text(), encoding="utf-8")
|
|
monkeypatch.setattr(sys, "argv", ["check_staged.py", str(clean), str(marked)])
|
|
assert check_staged.main() == 1
|
|
err = capsys.readouterr().err
|
|
assert str(marked) in err
|
|
assert str(clean) not in err
|
|
|
|
|
|
def test_check_staged_unknown_format_skipped(tmp_path, monkeypatch):
|
|
f = tmp_path / "data.bin"
|
|
f.write_bytes(b"\x00\x01\x02\xff\xfe no known magic bytes here")
|
|
monkeypatch.setattr(sys, "argv", ["check_staged.py", str(f)])
|
|
assert check_staged.main() == 0
|
|
|
|
|
|
def test_check_staged_missing_path_exits_2(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(sys, "argv", ["check_staged.py", str(tmp_path / "nope.txt")])
|
|
assert check_staged.main() == 2
|
|
|
|
|
|
def test_clean_staged_marked_file_cleans_and_exits_1(tmp_path, monkeypatch, capsys):
|
|
f = tmp_path / "marked.txt"
|
|
f.write_text(_watermarked_text(), encoding="utf-8")
|
|
monkeypatch.setattr(sys, "argv", ["clean_staged.py", str(f)])
|
|
assert clean_staged.main() == 1
|
|
assert f.read_text(encoding="utf-8") == "HelloWorld!"
|
|
err = capsys.readouterr().err
|
|
assert str(f) in err
|
|
|
|
|
|
def test_clean_staged_already_clean_file_exits_0_unchanged(tmp_path, monkeypatch):
|
|
f = tmp_path / "clean.txt"
|
|
original = "Nothing to see here."
|
|
f.write_text(original, encoding="utf-8")
|
|
monkeypatch.setattr(sys, "argv", ["clean_staged.py", str(f)])
|
|
assert clean_staged.main() == 0
|
|
assert f.read_text(encoding="utf-8") == original
|
|
|
|
|
|
def test_clean_staged_unknown_format_skipped(tmp_path, monkeypatch):
|
|
f = tmp_path / "data.bin"
|
|
original = b"\x00\x01\x02\xff\xfe no known magic bytes here"
|
|
f.write_bytes(original)
|
|
monkeypatch.setattr(sys, "argv", ["clean_staged.py", str(f)])
|
|
assert clean_staged.main() == 0
|
|
assert f.read_bytes() == original
|
|
|
|
|
|
def test_pre_commit_hooks_manifest_defines_both_hooks():
|
|
# No PyYAML in this project's stdlib-only test deps (requirements-dev.txt) —
|
|
# check the manifest's shape textually rather than adding a parser dependency.
|
|
text = (ROOT / ".pre-commit-hooks.yaml").read_text(encoding="utf-8")
|
|
assert "id: watermarks-remover-check" in text
|
|
assert "id: watermarks-remover-clean" in text
|
|
assert text.count("entry: python3 service/scripts/") == 2
|
|
assert text.count("language: system") == 2
|