use modde_core::manifest::wabbajack::{
ArchiveEntry, ArchiveState, BSAFileState, RawDirective, WabbajackManifest,
};
use modde_sources::wabbajack::validator::validate_install;
use xxhash_rust::xxh3::xxh3_64;
fn empty_manifest() -> WabbajackManifest {
WabbajackManifest {
name: "test".to_string(),
author: "test".to_string(),
description: "test".to_string(),
game: "skyrimse".to_string(),
version: "1.0".to_string(),
archives: vec![],
directives: vec![],
}
}
#[tokio::test]
async fn test_validate_from_archive_checks_presence_only() {
let staging = tempfile::tempdir().unwrap();
let content = b"archive file content";
tokio::fs::write(staging.path().join("output.esp"), content)
.await
.unwrap();
let manifest = WabbajackManifest {
archives: vec![ArchiveEntry {
hash: 123456,
name: "test.zip".to_string(),
size: 100,
state: None,
}],
directives: vec![RawDirective::FromArchive {
archive_hash_path: vec![
serde_json::Value::Number(serde_json::Number::from(123456)),
serde_json::Value::String("inner.esp".to_string()),
],
to: "output.esp".to_string(),
size: 0,
}],
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.total_files, 1);
assert_eq!(report.verified, 1);
assert!(report.mismatches.is_empty());
}
#[tokio::test]
async fn test_validate_from_archive_missing_output() {
let staging = tempfile::tempdir().unwrap();
let manifest = WabbajackManifest {
archives: vec![],
directives: vec![RawDirective::FromArchive {
archive_hash_path: vec![
serde_json::Value::Number(99999.into()),
serde_json::Value::String("file.txt".to_string()),
],
to: "output.txt".to_string(),
size: 0,
}],
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.total_files, 1);
assert_eq!(report.verified, 0);
assert_eq!(report.missing, vec!["output.txt"]);
}
#[tokio::test]
async fn test_validate_mixed_from_archive_and_patched() {
let staging = tempfile::tempdir().unwrap();
let correct_content = b"correct archive output";
let correct_hash = xxh3_64(correct_content);
tokio::fs::write(staging.path().join("from_archive.esp"), correct_content)
.await
.unwrap();
let patched_content = b"patched output data";
let patched_hash = xxh3_64(patched_content);
tokio::fs::write(staging.path().join("patched.esp"), patched_content)
.await
.unwrap();
let manifest = WabbajackManifest {
archives: vec![ArchiveEntry {
hash: correct_hash,
name: "source.zip".to_string(),
size: 200,
state: Some(ArchiveState::NexusDownloader {
game_name: "SkyrimSE".to_string(),
mod_id: 42.into(),
file_id: 99.into(),
}),
}],
directives: vec![
RawDirective::FromArchive {
archive_hash_path: vec![
serde_json::Value::Number(serde_json::Number::from(correct_hash)),
serde_json::Value::String("data.esp".to_string()),
],
to: "from_archive.esp".to_string(),
size: 0,
},
RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: "patched.esp".to_string(),
hash: patched_hash,
},
],
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.total_files, 2);
assert_eq!(report.verified, 2);
assert!(report.missing.is_empty());
assert!(report.mismatches.is_empty());
}
#[tokio::test]
async fn test_validate_all_directive_types_mixed() {
let staging = tempfile::tempdir().unwrap();
let manifest = WabbajackManifest {
archives: vec![],
directives: vec![
RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: "missing.txt".to_string(),
hash: 12345,
},
RawDirective::CreateBSA {
temp_id: "bsa1".to_string(),
to: "output.bsa".to_string(),
file_states: vec![BSAFileState {
path: "mesh.nif".to_string(),
hash: 0,
size: 100,
}],
},
RawDirective::Unknown,
],
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.total_files, 1, "only PatchedFromArchive counts");
assert_eq!(report.missing.len(), 1);
assert_eq!(report.missing[0], "missing.txt");
}
#[tokio::test]
async fn test_validate_deeply_nested_file() {
let staging = tempfile::tempdir().unwrap();
let deep = staging.path().join("textures/landscape/mountains");
tokio::fs::create_dir_all(&deep).await.unwrap();
let content = b"deep texture data";
let hash = xxh3_64(content);
tokio::fs::write(deep.join("peak.dds"), content)
.await
.unwrap();
let manifest = WabbajackManifest {
directives: vec![RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: "textures/landscape/mountains/peak.dds".to_string(),
hash,
}],
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.verified, 1);
}
#[tokio::test]
async fn test_validate_report_counts_are_consistent() {
let staging = tempfile::tempdir().unwrap();
let correct_content = b"correct";
let correct_hash = xxh3_64(correct_content);
tokio::fs::write(staging.path().join("correct.txt"), correct_content)
.await
.unwrap();
tokio::fs::write(staging.path().join("wrong.txt"), b"wrong data")
.await
.unwrap();
let manifest = WabbajackManifest {
directives: vec![
RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: "correct.txt".to_string(),
hash: correct_hash,
},
RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: "wrong.txt".to_string(),
hash: 99999,
},
RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: "missing.txt".to_string(),
hash: 88888,
},
],
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.total_files, 3);
assert_eq!(report.verified, 1);
assert_eq!(report.missing.len(), 1);
assert_eq!(report.mismatches.len(), 1);
assert_eq!(
report.total_files,
report.verified + report.missing.len() + report.mismatches.len()
);
}
#[tokio::test]
async fn test_validate_mismatch_contains_both_hashes() {
let staging = tempfile::tempdir().unwrap();
let content = b"actual content";
let actual_hash = xxh3_64(content);
tokio::fs::write(staging.path().join("file.txt"), content)
.await
.unwrap();
let manifest = WabbajackManifest {
directives: vec![RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: "file.txt".to_string(),
hash: 0, }],
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.mismatches.len(), 1);
assert_eq!(report.mismatches[0].expected_hash, 0);
assert_eq!(report.mismatches[0].actual_hash, actual_hash);
assert_eq!(report.mismatches[0].path, "file.txt");
}
#[tokio::test]
async fn test_validate_many_files() {
let staging = tempfile::tempdir().unwrap();
let mut directives = Vec::new();
for i in 0..50 {
let content = format!("content_{i}");
let hash = xxh3_64(content.as_bytes());
let filename = format!("file_{i}.txt");
tokio::fs::write(staging.path().join(&filename), content.as_bytes())
.await
.unwrap();
directives.push(RawDirective::PatchedFromArchive {
archive_hash_path: vec![serde_json::Value::Number(0.into())],
patch_id: String::new(),
size: 0,
to: filename,
hash,
});
}
let manifest = WabbajackManifest {
directives,
..empty_manifest()
};
let report = validate_install(&manifest, staging.path()).await.unwrap();
assert_eq!(report.total_files, 50);
assert_eq!(report.verified, 50);
assert!(report.missing.is_empty());
assert!(report.mismatches.is_empty());
}