diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..17329eb --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +tests/fixtures/*.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff69bef..fe9971c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,19 @@ env: CARGO_TERM_COLOR: always jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt -- --check + - run: cargo clippy -- -D warnings + test: + needs: lint strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -19,11 +31,42 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + + - name: Validate LFS fixtures + run: | + for f in tests/fixtures/*.mp4; do + [ ! -f "$f" ] && continue + size=$(wc -c < "$f") + if [ "$size" -lt 1000 ]; then + echo "ERROR: $f appears to be an LFS pointer (${size} bytes)" + exit 1 + fi + done + - uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 - - run: cargo fmt -- --check - - run: cargo clippy -- -D warnings + + - name: Install ffmpeg (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y ffmpeg + + - name: Install ffmpeg (macOS) + if: runner.os == 'macOS' + run: brew install ffmpeg + - run: cargo test - run: cargo build --release + + test-deep: + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Install ffmpeg + run: sudo apt-get update && sudo apt-get install -y ffmpeg + - name: Run deep detection tests + run: cargo test -- --ignored diff --git a/tests/audio_detection.rs b/tests/audio_detection.rs new file mode 100644 index 0000000..0799110 --- /dev/null +++ b/tests/audio_detection.rs @@ -0,0 +1,75 @@ +use assert_cmd::cargo_bin_cmd; +use predicates::prelude::*; + +// --- Suno 2 (ID3 + Filename, MEDIUM) --- + +#[test] +fn suno_2_detected_via_id3() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_suno_2.mp3"]) + .assert() + .success() + .stdout(predicate::str::contains("ID3")) + .stdout(predicate::str::contains("suno")); +} + +#[test] +fn suno_2_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_suno_2.mp3", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")) + .stdout(predicate::str::contains("suno")); +} + +#[test] +fn suno_2_info_shows_id3_tags() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_suno_2.mp3"]) + .assert() + .success() + .stdout(predicate::str::contains("ID3")); +} + +// --- ElevenLabs (C2PA, HIGH) --- + +#[test] +fn elevenlabs_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_elevenlabs.mp3"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("elevenlabs")); +} + +#[test] +fn elevenlabs_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_elevenlabs.mp3", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +#[test] +fn elevenlabs_info_shows_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_elevenlabs.mp3"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")); +} diff --git a/tests/fixtures/ai_elevenlabs.mp3 b/tests/fixtures/ai_elevenlabs.mp3 new file mode 100644 index 0000000..ddf6154 Binary files /dev/null and b/tests/fixtures/ai_elevenlabs.mp3 differ diff --git a/tests/fixtures/ai_flux_max.jpeg b/tests/fixtures/ai_flux_max.jpeg new file mode 100644 index 0000000..4d30a65 Binary files /dev/null and b/tests/fixtures/ai_flux_max.jpeg differ diff --git a/tests/fixtures/ai_flux_pro.jpeg b/tests/fixtures/ai_flux_pro.jpeg new file mode 100644 index 0000000..8b3bcf2 Binary files /dev/null and b/tests/fixtures/ai_flux_pro.jpeg differ diff --git a/tests/fixtures/ai_gptimage.png b/tests/fixtures/ai_gptimage.png new file mode 100644 index 0000000..79b30dc Binary files /dev/null and b/tests/fixtures/ai_gptimage.png differ diff --git a/tests/fixtures/ai_gptimage_1_5.png b/tests/fixtures/ai_gptimage_1_5.png new file mode 100644 index 0000000..d94acfa Binary files /dev/null and b/tests/fixtures/ai_gptimage_1_5.png differ diff --git a/tests/fixtures/ai_ideogram.png b/tests/fixtures/ai_ideogram.png new file mode 100644 index 0000000..d4a7d19 Binary files /dev/null and b/tests/fixtures/ai_ideogram.png differ diff --git a/tests/fixtures/ai_kling.mp4 b/tests/fixtures/ai_kling.mp4 new file mode 100644 index 0000000..8bffdf1 --- /dev/null +++ b/tests/fixtures/ai_kling.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42fa4fecb05ddb9d01013c51b7ccbc5da8c0959a6fed4d8eab9ca7f88815566b +size 9534441 diff --git a/tests/fixtures/ai_kling_omni.mp4 b/tests/fixtures/ai_kling_omni.mp4 new file mode 100644 index 0000000..342522c --- /dev/null +++ b/tests/fixtures/ai_kling_omni.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd7501e48858168816d050d3a52d23d0c4260f55efe794b19edb8baf2462ad45 +size 6322740 diff --git a/tests/fixtures/ai_kling_v3.mp4 b/tests/fixtures/ai_kling_v3.mp4 new file mode 100644 index 0000000..b5f273d --- /dev/null +++ b/tests/fixtures/ai_kling_v3.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7240a62b516d2c115644226363b6f08e7fa1f07bb4f8e6223c630087a1ad686e +size 13000017 diff --git a/tests/fixtures/ai_ltx.mp4 b/tests/fixtures/ai_ltx.mp4 new file mode 100644 index 0000000..7c1270c --- /dev/null +++ b/tests/fixtures/ai_ltx.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b22b7e9d104d37db9db399af8c483f311f871df695879e44375a6116805cbf32 +size 6814737 diff --git a/tests/fixtures/ai_midjourney_1.png b/tests/fixtures/ai_midjourney_1.png new file mode 100644 index 0000000..3cdcac8 Binary files /dev/null and b/tests/fixtures/ai_midjourney_1.png differ diff --git a/tests/fixtures/ai_midjourney_2.png b/tests/fixtures/ai_midjourney_2.png new file mode 100644 index 0000000..9460014 Binary files /dev/null and b/tests/fixtures/ai_midjourney_2.png differ diff --git a/tests/fixtures/ai_midjourney_3.png b/tests/fixtures/ai_midjourney_3.png new file mode 100644 index 0000000..4ea5bcd Binary files /dev/null and b/tests/fixtures/ai_midjourney_3.png differ diff --git a/tests/fixtures/ai_midjourney_4.png b/tests/fixtures/ai_midjourney_4.png new file mode 100644 index 0000000..4b7623f Binary files /dev/null and b/tests/fixtures/ai_midjourney_4.png differ diff --git a/tests/fixtures/ai_nano_pro.png b/tests/fixtures/ai_nano_pro.png new file mode 100644 index 0000000..6bad26f Binary files /dev/null and b/tests/fixtures/ai_nano_pro.png differ diff --git a/tests/fixtures/ai_seedance.mp4 b/tests/fixtures/ai_seedance.mp4 new file mode 100644 index 0000000..bb2b7a0 --- /dev/null +++ b/tests/fixtures/ai_seedance.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:422bb90107d2beedd4425a83938df2c595ab48a60223dd32c219e426e29a26ae +size 7569468 diff --git a/tests/fixtures/ai_seedream.jpeg b/tests/fixtures/ai_seedream.jpeg new file mode 100644 index 0000000..fca30ab Binary files /dev/null and b/tests/fixtures/ai_seedream.jpeg differ diff --git a/tests/fixtures/ai_seedream_4_5.jpeg b/tests/fixtures/ai_seedream_4_5.jpeg new file mode 100644 index 0000000..e0f0ea2 Binary files /dev/null and b/tests/fixtures/ai_seedream_4_5.jpeg differ diff --git a/tests/fixtures/ai_seedream_v4.jpeg b/tests/fixtures/ai_seedream_v4.jpeg new file mode 100644 index 0000000..3a01fb9 Binary files /dev/null and b/tests/fixtures/ai_seedream_v4.jpeg differ diff --git a/tests/fixtures/ai_seedream_v5.jpeg b/tests/fixtures/ai_seedream_v5.jpeg new file mode 100644 index 0000000..aec33f0 Binary files /dev/null and b/tests/fixtures/ai_seedream_v5.jpeg differ diff --git a/tests/fixtures/ai_sora.mp4 b/tests/fixtures/ai_sora.mp4 new file mode 100644 index 0000000..611df1a --- /dev/null +++ b/tests/fixtures/ai_sora.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7912b0b8fa2e0bb7d18c48fb5c921c955c5b83f7f21827ee3c05bc0fb55b7ff6 +size 4119015 diff --git a/tests/fixtures/ai_sora_pro.mp4 b/tests/fixtures/ai_sora_pro.mp4 new file mode 100644 index 0000000..c533065 --- /dev/null +++ b/tests/fixtures/ai_sora_pro.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10de3ffcab4682d9b04a47fd31bae43f2d70e6a03d36952222faedabd81d5e55 +size 4308898 diff --git a/tests/fixtures/ai_suno_2.mp3 b/tests/fixtures/ai_suno_2.mp3 new file mode 100644 index 0000000..4e2cd77 Binary files /dev/null and b/tests/fixtures/ai_suno_2.mp3 differ diff --git a/tests/fixtures/ai_vidu.mp4 b/tests/fixtures/ai_vidu.mp4 new file mode 100644 index 0000000..a429ccf --- /dev/null +++ b/tests/fixtures/ai_vidu.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8e9e523d398b994d32222bfa14a1a75360541d3daf9a6b7d5a21ec30c047055 +size 5218390 diff --git a/tests/fixtures/ai_wan.mp4 b/tests/fixtures/ai_wan.mp4 new file mode 100644 index 0000000..f282816 --- /dev/null +++ b/tests/fixtures/ai_wan.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:571f6372b43329d2e68f8c1f3f85621d2e27c2c0c7d21215767c17834bde094a +size 7030129 diff --git a/tests/image_detection.rs b/tests/image_detection.rs new file mode 100644 index 0000000..81da32f --- /dev/null +++ b/tests/image_detection.rs @@ -0,0 +1,352 @@ +use assert_cmd::cargo_bin_cmd; +use predicates::prelude::*; + +// --- GPT Image (C2PA, HIGH) --- + +#[test] +fn gptimage_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_gptimage.png"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("trainedAlgorithmicMedia")) + .stdout(predicate::str::contains("gpt-4o")); +} + +#[test] +fn gptimage_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_gptimage.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +#[test] +fn gptimage_1_5_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_gptimage_1_5.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("gpt-4o")); +} + +// --- Midjourney (XMP + Filename, MEDIUM) --- + +#[test] +fn midjourney_1_detected_via_xmp() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_midjourney_1.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("XMP")) + .stdout(predicate::str::contains("trainedAlgorithmicMedia")); +} + +#[test] +fn midjourney_1_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_midjourney_1.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +#[test] +fn midjourney_2_detected_via_xmp() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_midjourney_2.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("XMP")); +} + +#[test] +fn midjourney_3_detected_via_xmp() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_midjourney_3.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("XMP")); +} + +#[test] +fn midjourney_4_detected_via_xmp() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_midjourney_4.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("XMP")); +} + +#[test] +fn midjourney_filename_pattern_detected() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_midjourney_1.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("midjourney")); +} + +// --- Ideogram (EXIF, LOW) --- + +#[test] +fn ideogram_detected_via_exif() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_ideogram.png"]) + .assert() + .success() + .stdout(predicate::str::contains("EXIF")) + .stdout(predicate::str::contains("ideogram")); +} + +#[test] +fn ideogram_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_ideogram.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- Flux (C2PA, HIGH) --- + +#[test] +fn flux_pro_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_flux_pro.jpeg"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("flux")); +} + +#[test] +fn flux_max_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_flux_max.jpeg"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("flux")); +} + +#[test] +fn flux_pro_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_flux_pro.jpeg", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- SeedReam (EXIF/Watermark, LOW-MEDIUM) --- + +#[test] +fn seedream_detected_via_exif() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_seedream.jpeg"]) + .assert() + .success() + .stdout(predicate::str::contains("EXIF")); +} + +#[test] +fn seedream_v4_detected_via_watermark() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_seedream_v4.jpeg", + ]) + .assert() + .success() + .stdout(predicate::str::contains("WATERMARK")); +} + +#[test] +fn seedream_4_5_detected_via_watermark() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_seedream_4_5.jpeg", + ]) + .assert() + .success() + .stdout(predicate::str::contains("WATERMARK")); +} + +#[test] +fn seedream_v5_detected_via_visible_watermark() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "tests/fixtures/ai_seedream_v5.jpeg", + ]) + .assert() + .success() + .stdout(predicate::str::contains("Visible text overlay")); +} + +#[test] +fn seedream_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_seedream.jpeg", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- Nano Pro (C2PA + XMP, HIGH) --- + +#[test] +fn nano_pro_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_nano_pro.png"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("trainedAlgorithmicMedia")); +} + +#[test] +fn nano_pro_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_nano_pro.png", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- Deep mode tests (slow, require pixel analysis) --- + +#[test] +#[ignore] // slow: deep watermark analysis +fn gptimage_deep_shows_watermark() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "--deep", + "tests/fixtures/ai_gptimage.png", + ]) + .assert() + .success(); +} + +#[test] +#[ignore] // slow: deep watermark analysis +fn seedream_v4_deep_invisible_watermark() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "--deep", + "tests/fixtures/ai_seedream_v4.jpeg", + ]) + .assert() + .success() + .stdout(predicate::str::contains("Invisible watermark")); +} + +// --- Info command tests --- + +#[test] +fn gptimage_info_shows_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_gptimage.png"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")); +} + +#[test] +fn midjourney_info_shows_xmp() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_midjourney_1.png"]) + .assert() + .success(); +} + +#[test] +fn ideogram_info_shows_exif() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_ideogram.png"]) + .assert() + .success(); +} diff --git a/tests/video_detection.rs b/tests/video_detection.rs new file mode 100644 index 0000000..5a5b2b4 --- /dev/null +++ b/tests/video_detection.rs @@ -0,0 +1,249 @@ +use assert_cmd::cargo_bin_cmd; +use predicates::prelude::*; + +// --- Sora (C2PA, HIGH) --- + +#[test] +fn sora_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_sora.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("sora")); +} + +#[test] +fn sora_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_sora.mp4", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +#[test] +fn sora_pro_detected_via_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_sora_pro.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")) + .stdout(predicate::str::contains("sora")); +} + +// --- Kling (MP4 SEI marker, MEDIUM) --- + +#[test] +fn kling_detected_via_sei_marker() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_kling.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("SEI")) + .stdout(predicate::str::contains("kling")); +} + +#[test] +fn kling_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_kling.mp4", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +#[test] +fn kling_v3_detected_via_sei_marker() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_kling_v3.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("kling")); +} + +#[test] +fn kling_omni_detected_via_sei_marker() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_kling_omni.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("kling")); +} + +// --- Vidu (MP4 AIGC label, MEDIUM) --- + +#[test] +fn vidu_detected_via_aigc_label() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_vidu.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("AIGC")); +} + +#[test] +fn vidu_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_vidu.mp4", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- Wan (MP4 AIGC label, MEDIUM) --- + +#[test] +fn wan_detected_via_aigc_label() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_wan.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("AIGC")); +} + +#[test] +fn wan_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_wan.mp4", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- SeedAnce (Video frame watermark, MEDIUM, requires ffmpeg) --- + +#[test] +fn seedance_detected_via_video_watermark() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_seedance.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("WATERMARK")); +} + +#[test] +fn seedance_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_seedance.mp4", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- LTX (Video frame watermark, MEDIUM, requires ffmpeg) --- + +#[test] +fn ltx_detected_via_video_watermark() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "check", "tests/fixtures/ai_ltx.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("WATERMARK")); +} + +#[test] +fn ltx_json_output() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "--json", + "check", + "tests/fixtures/ai_ltx.mp4", + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"ai_generated\": true")); +} + +// --- Info command tests --- + +#[test] +fn sora_info_shows_c2pa() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_sora.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("C2PA")); +} + +#[test] +fn kling_info_shows_sei_marker() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_kling.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("SEI")); +} + +#[test] +fn vidu_info_shows_aigc() { + cargo_bin_cmd!("aic") + .args(["--lang", "en", "info", "tests/fixtures/ai_vidu.mp4"]) + .assert() + .success() + .stdout(predicate::str::contains("AIGC")); +} + +// --- Deep mode tests (slow, require ffmpeg + frame extraction) --- + +#[test] +#[ignore] // slow: video frame watermark analysis +fn sora_deep_detection() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "--deep", + "tests/fixtures/ai_sora.mp4", + ]) + .assert() + .success(); +} + +#[test] +#[ignore] // slow: video frame watermark analysis +fn kling_deep_detection() { + cargo_bin_cmd!("aic") + .args([ + "--lang", + "en", + "check", + "--deep", + "tests/fixtures/ai_kling.mp4", + ]) + .assert() + .success(); +}