From cc74bec9b05bdaf24653e7772cc58a28a8505b68 Mon Sep 17 00:00:00 2001 From: NimBold Date: Sat, 22 Aug 2026 00:32:22 +0330 Subject: [PATCH] fix(ci): cover Windows headless production contracts - execute queue, retry, normalization, and credential-boundary contracts without the broken Tauri mock runtime\n- refresh target-specific FFmpeg source locks after provider URL and digest drift\n- document the remaining compile-only Windows GUI test limitation --- .github/workflows/ci.yml | 4 + engine-sources.lock.json | 8 +- src-tauri/tests/README.md | 5 +- src-tauri/tests/production_contract.rs | 118 +++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 src-tauri/tests/production_contract.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc41997..301dde6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,10 @@ jobs: if: runner.os == 'Windows' working-directory: src-tauri run: cargo test --lib --no-run --target ${{ matrix.target }} + - name: Run Windows headless production contracts + if: runner.os == 'Windows' + working-directory: src-tauri + run: cargo test --test production_contract --target ${{ matrix.target }} -- --nocapture - name: Verify Windows atomic Torrent storage if: runner.os == 'Windows' working-directory: src-tauri diff --git a/engine-sources.lock.json b/engine-sources.lock.json index a8b06ef..1d9a5bc 100644 --- a/engine-sources.lock.json +++ b/engine-sources.lock.json @@ -14,8 +14,8 @@ }, "ffmpeg": { "version": "8.1.2-44-g7c533d0f86", - "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-20-13-45/ffmpeg-n8.1.2-44-g7c533d0f86-win64-gpl-8.1.zip", - "sha256": "410c82fc0a7d713fd83412138271b8559faa8cf8a74a75eaf541dfca75ea4590" + "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-21-13-40/ffmpeg-n8.1.2-44-g7c533d0f86-win64-gpl-8.1.zip", + "sha256": "827762aba79aa27b397a00607b5e2afe717d4d83264aa753085fb7335ea932f0" }, "aria2c": { "version": "1.37.0", @@ -36,8 +36,8 @@ }, "ffmpeg": { "version": "8.1.2-44-g7c533d0f86", - "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-20-13-45/ffmpeg-n8.1.2-44-g7c533d0f86-linux64-gpl-8.1.tar.xz", - "sha256": "1135ba005de82cecce7ceba9d653c47374af8a657a0eae93571214c6a5169850" + "url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-21-13-40/ffmpeg-n8.1.2-44-g7c533d0f86-linux64-gpl-8.1.tar.xz", + "sha256": "2b5156f14340e2abf83888a925b9fd8bd17270368680b7a5107d8eb4c7169c57" }, "aria2c": { "version": "1.37.0", diff --git a/src-tauri/tests/README.md b/src-tauri/tests/README.md index 4039b8d..a7e841c 100644 --- a/src-tauri/tests/README.md +++ b/src-tauri/tests/README.md @@ -54,4 +54,7 @@ Native CI runs this failure-path smoke after staging the target-specific bundled engines on macOS, Windows, and Linux. Windows executes the Torrent RPC, atomic-storage, canonical-cache, and web-seed normalization targets, while compiling (but not executing) the queue-manager and library test binaries; -the Tauri mock harness exits before running on the Windows runner. +the Tauri mock harness exits before running on the Windows runner. The +headless `production_contract` target still executes queue admission, +normalization, credential-boundary, and retry contracts on Windows without +constructing a Tauri mock application. diff --git a/src-tauri/tests/production_contract.rs b/src-tauri/tests/production_contract.rs new file mode 100644 index 0000000..6aecf61 --- /dev/null +++ b/src-tauri/tests/production_contract.rs @@ -0,0 +1,118 @@ +use firelink_lib::ipc::{TorrentFile, TorrentWebSeed}; +use firelink_lib::queue::{ + clamp_download_connections, normalize_aria2_disk_cache, normalize_torrent_bind_address, + normalize_torrent_dht_message_timeout, normalize_torrent_file_allocation, + normalize_torrent_web_seeds, EnqueueItem, QueueManager, SpawnPayload, +}; +use firelink_lib::retry::{backoff_for, is_permanent_network_error, network_error_class}; +use std::time::Duration; + +#[test] +fn headless_queue_contracts_reject_unsafe_inputs_and_preserve_bounds() { + assert_eq!(clamp_download_connections(0), 1); + assert_eq!(clamp_download_connections(99), 16); + assert_eq!(normalize_aria2_disk_cache(Some(" 32m ")).unwrap(), "32M"); + assert!(normalize_aria2_disk_cache(Some("0K")).is_err()); + assert_eq!( + normalize_torrent_bind_address(Some(" 2001:db8::1 ")).unwrap(), + Some("2001:db8::1".to_string()) + ); + assert!(normalize_torrent_bind_address(Some("not-an-ip")).is_err()); + assert_eq!(normalize_torrent_dht_message_timeout(60).unwrap(), 60); + assert!(normalize_torrent_dht_message_timeout(61).is_err()); + assert_eq!(normalize_torrent_file_allocation(None).unwrap(), "prealloc"); + assert!(normalize_torrent_file_allocation(Some("sparse")).is_err()); +} + +#[test] +fn headless_torrent_web_seed_contract_deduplicates_and_expands_per_file() { + let files = vec![ + TorrentFile { + index: 1, + path: "folder/one.bin".to_string(), + length: 10, + }, + TorrentFile { + index: 2, + path: "two.bin".to_string(), + length: 20, + }, + ]; + let seeds = vec![ + TorrentWebSeed { + file_index: 1, + uri: "https://mirror.example/base".to_string(), + }, + TorrentWebSeed { + file_index: 1, + uri: "https://mirror.example/base".to_string(), + }, + ]; + let normalized = normalize_torrent_web_seeds(Some(&seeds), &files).unwrap(); + assert_eq!(normalized.len(), 1); + let expanded = firelink_lib::queue::expand_torrent_web_seeds(&normalized, &files).unwrap(); + assert_eq!( + expanded, + vec![(1, "https://mirror.example/base/folder/one.bin".to_string())] + ); + + let credentialed = [TorrentWebSeed { + file_index: 1, + uri: "https://user:pass@mirror.example/file".to_string(), + }]; + assert!(normalize_torrent_web_seeds(Some(&credentialed), &files).is_err()); + let unknown_file = [TorrentWebSeed { + file_index: 3, + uri: "https://mirror.example/file".to_string(), + }]; + assert!(normalize_torrent_web_seeds(Some(&unknown_file), &files).is_err()); +} + +#[test] +fn headless_enqueue_contract_strips_torrent_credentials_before_task_creation() { + let item = EnqueueItem { + id: "torrent".to_string(), + queue_id: "main".to_string(), + url: "https://example.test/file.torrent".to_string(), + destination: "/tmp".to_string(), + filename: "file.torrent".to_string(), + username: Some("user".to_string()), + password: Some("secret".to_string()), + headers: Some("Cookie: session=secret".to_string()), + cookies: Some("session=secret".to_string()), + is_torrent: Some(true), + ..EnqueueItem::default() + }; + let task = item.into_task(); + assert!(task.payload.username.is_none()); + assert!(task.payload.password.is_none()); + assert!(task.payload.headers.is_none()); + assert!(task.payload.cookies.is_none()); +} + +#[test] +fn headless_queue_lifecycle_eligibility_and_retry_contracts_hold() { + assert!(QueueManager::::aria2_allocation_phase_eligible( + &SpawnPayload::default() + )); + assert!( + !QueueManager::::aria2_allocation_phase_eligible(&SpawnPayload { + is_media: true, + ..SpawnPayload::default() + }) + ); + assert!( + !QueueManager::::aria2_allocation_phase_eligible(&SpawnPayload { + is_torrent: true, + torrent_file_allocation: Some("none".to_string()), + ..SpawnPayload::default() + }) + ); + assert_eq!(backoff_for(0), Duration::from_secs(2)); + assert_eq!(backoff_for(usize::MAX), Duration::from_secs(10)); + assert_eq!( + network_error_class("HTTP/1.1 503 Service Unavailable"), + "http" + ); + assert!(is_permanent_network_error("HTTP 403 Forbidden")); +}