mockforge-intelligence 0.3.240

AI-powered behavior, response generation, and behavioral cloning for MockForge
Documentation
use super::*;
use serde_json::json;

struct Fixture(PathBuf);

impl Fixture {
    fn new() -> Self {
        Self(std::env::temp_dir().join(format!("mockforge-freeze-{}", uuid::Uuid::new_v4())))
    }
    fn freezer(&self) -> ArtifactFreezer {
        ArtifactFreezer::with_base_dir(&self.0)
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

fn request(format: &str) -> FreezeRequest {
    FreezeRequest {
        artifact_type: "mock".into(),
        content: json!({"name":"fixed fixture","value":42}),
        format: format.into(),
        path: None,
        metadata: Some(FreezeMetadata {
            llm_provider: None,
            llm_model: None,
            llm_version: None,
            prompt_hash: Some("prompt-identity".into()),
            output_hash: None,
            original_prompt: None,
        }),
    }
}

#[tokio::test]
async fn first_use_of_missing_directory_is_a_cache_miss() {
    let fixture = Fixture::new();
    assert!(fixture
        .freezer()
        .load_frozen("mock", Some("new-prompt"))
        .await
        .unwrap()
        .is_none());
    assert!(!fixture.0.exists(), "a cache read must not create directories");
}

#[tokio::test]
async fn fresh_artifact_hash_verifies_with_embedded_metadata() {
    let fixture = Fixture::new();
    let freezer = fixture.freezer();
    let frozen = freezer.freeze(&request("json")).await.unwrap();
    assert!(freezer.verify_frozen_artifact(&frozen).await.unwrap());
}

#[tokio::test]
async fn prompt_identity_round_trips_in_json_and_yaml() {
    for format in ["json", "yaml"] {
        let fixture = Fixture::new();
        let freezer = fixture.freezer();
        let frozen = freezer.freeze(&request(format)).await.unwrap();
        let loaded = freezer.load_frozen("mock", Some("prompt-identity")).await.unwrap().unwrap();
        assert_eq!(loaded.content, frozen.content);
        assert_eq!(loaded.output_hash, frozen.output_hash);
        assert!(freezer.load_frozen("mock", Some("different-prompt")).await.unwrap().is_none());
        assert!(freezer.load_frozen("persona", Some("prompt-identity")).await.unwrap().is_none());
    }
}

#[tokio::test]
async fn loading_rejects_modified_payload() {
    let fixture = Fixture::new();
    let freezer = fixture.freezer();
    let frozen = freezer.freeze(&request("json")).await.unwrap();
    let mut modified = frozen.content;
    modified["value"] = json!(99);
    fs::write(&frozen.path, serde_json::to_vec(&modified).unwrap()).await.unwrap();
    assert!(freezer.load_frozen("mock", None).await.is_err());
}

#[tokio::test]
async fn identified_snapshot_cannot_disable_integrity_by_removing_hash() {
    let fixture = Fixture::new();
    let freezer = fixture.freezer();
    let frozen = freezer.freeze(&request("json")).await.unwrap();
    let mut modified = frozen.content;
    modified["_frozen_metadata"].as_object_mut().unwrap().remove("output_hash");
    modified["value"] = json!(99);
    fs::write(&frozen.path, serde_json::to_vec(&modified).unwrap()).await.unwrap();
    assert!(freezer.load_frozen("mock", Some("prompt-identity")).await.is_err());
}

#[tokio::test]
async fn non_object_payloads_retain_identity_and_hash_without_wrapping() {
    for format in ["json", "yaml", "yml"] {
        for value in [
            json!([1, null, {"nested":true}]),
            json!("fixture"),
            json!(3.25),
            json!(false),
            Value::Null,
        ] {
            let fixture = Fixture::new();
            let freezer = fixture.freezer();
            let mut input = request(format);
            input.content = value.clone();
            let frozen = freezer.freeze(&input).await.unwrap();
            let raw = fs::read(&frozen.path).await.unwrap();
            let decoded: Value = if format == "json" {
                serde_json::from_slice(&raw).unwrap()
            } else {
                serde_yaml::from_slice(&raw).unwrap()
            };
            assert_eq!(decoded, value);
            let loaded = fixture
                .freezer()
                .load_frozen("mock", Some("prompt-identity"))
                .await
                .unwrap()
                .expect("saved identity must survive restart");
            assert_eq!(loaded.content, value);
            assert_eq!(loaded.output_hash, frozen.output_hash);
            assert!(freezer.verify_frozen_artifact(&loaded).await.unwrap());
            assert!(freezer.load_frozen("mock", Some("other-prompt")).await.unwrap().is_none());
        }
    }
}

#[tokio::test]
async fn non_object_payload_changes_are_rejected_without_identifier() {
    let fixture = Fixture::new();
    let freezer = fixture.freezer();
    let mut input = request("json");
    input.content = json!([1, 2]);
    let frozen = freezer.freeze(&input).await.unwrap();
    fs::write(&frozen.path, b"[9,9]").await.unwrap();
    assert!(freezer.load_frozen("mock", None).await.is_err());
}

#[tokio::test]
async fn missing_integrity_hash_is_not_verified() {
    let fixture = Fixture::new();
    let freezer = fixture.freezer();
    let mut frozen = freezer.freeze(&request("json")).await.unwrap();
    frozen.output_hash = None;
    assert!(!freezer.verify_frozen_artifact(&frozen).await.unwrap());
}

#[tokio::test]
async fn non_object_snapshot_publication_returns_one_complete_winner() {
    let fixture = Fixture::new();
    let freezer = fixture.freezer();
    let identifier = "a".repeat(64);
    let mut first = request("json");
    first.content = json!(["first"]);
    first.metadata.as_mut().unwrap().prompt_hash = Some(identifier.clone());
    let mut second = first.clone();
    second.content = Value::Null;
    let (a, b) = tokio::join!(
        freezer.freeze_once(&first, &identifier),
        freezer.freeze_once(&second, &identifier)
    );
    let a = a.unwrap();
    let b = b.unwrap();
    assert_eq!(a.content, b.content);
    assert_eq!(a.path, b.path);
    let raw: Value = serde_json::from_slice(&fs::read(&a.path).await.unwrap()).unwrap();
    assert_eq!(raw, a.content);
    let loaded = fixture
        .freezer()
        .load_snapshot("mock", &identifier, "json")
        .await
        .unwrap()
        .unwrap();
    assert_eq!(loaded.content, a.content);
    let discovered =
        fixture.freezer().load_frozen("mock", Some(&identifier)).await.unwrap().unwrap();
    assert_eq!(discovered.content, a.content);
}

#[tokio::test]
async fn mixed_object_and_scalar_publishers_share_one_winner() {
    for _ in 0..12 {
        let fixture = Fixture::new();
        let freezer = fixture.freezer();
        let identifier = "b".repeat(64);
        let mut first = request("yaml");
        first.metadata.as_mut().unwrap().prompt_hash = Some(identifier.clone());
        let mut second = first.clone();
        second.content = Value::Null;
        let (a, b) = tokio::join!(
            freezer.freeze_once(&first, &identifier),
            freezer.freeze_once(&second, &identifier)
        );
        let a = a.unwrap();
        let b = b.unwrap();
        assert_eq!(a.content, b.content);
        assert_eq!(a.path, b.path);
    }
}

#[tokio::test]
async fn missing_or_unhashed_scalar_provenance_fails_closed() {
    for modification in ["missing", "hash", "type", "oversize"] {
        let fixture = Fixture::new();
        let freezer = fixture.freezer();
        let mut input = request("json");
        input.content = json!(false);
        let frozen = freezer.freeze(&input).await.unwrap();
        let sidecar = provenance::path(Path::new(&frozen.path));
        let mut meta: Value = serde_json::from_slice(&fs::read(&sidecar).await.unwrap()).unwrap();
        match modification {
            "missing" => fs::remove_file(&sidecar).await.unwrap(),
            "oversize" => fs::write(&sidecar, vec![b' '; 64 * 1024 + 1]).await.unwrap(),
            field => {
                let key = if field == "hash" {
                    "output_hash"
                } else {
                    "artifact_type"
                };
                meta.as_object_mut().unwrap().remove(key);
                fs::write(&sidecar, serde_json::to_vec(&meta).unwrap()).await.unwrap();
            }
        }
        assert!(freezer.load_frozen("mock", None).await.is_err(), "{modification}");
    }
}

#[tokio::test]
async fn direct_paths_and_latest_scalar_discovery_are_preserved() {
    let fixture = Fixture::new();
    let freezer = fixture.freezer();
    let mut input = request("json");
    input.content = json!("first");
    input.path = Some(fixture.0.join("mock_first.json").to_string_lossy().into_owned());
    let first = freezer.freeze(&input).await.unwrap();
    assert_eq!(Some(&first.path), input.path.as_ref());
    tokio::time::sleep(std::time::Duration::from_millis(2)).await;
    input.content = json!("latest");
    input.path = None;
    let second = freezer.freeze(&input).await.unwrap();
    assert_eq!(freezer.load_frozen("mock", None).await.unwrap().unwrap().path, second.path);
}

#[cfg(unix)]
#[tokio::test]
async fn scalar_provenance_symlink_and_directory_are_rejected() {
    for kind in ["symlink", "directory"] {
        let fixture = Fixture::new();
        let freezer = fixture.freezer();
        let mut input = request("json");
        input.content = Value::Null;
        let frozen = freezer.freeze(&input).await.unwrap();
        let sidecar = provenance::path(Path::new(&frozen.path));
        let saved = fs::read(&sidecar).await.unwrap();
        fs::remove_file(&sidecar).await.unwrap();
        if kind == "symlink" {
            let other = fixture.0.join("target");
            fs::write(&other, saved).await.unwrap();
            std::os::unix::fs::symlink(other, &sidecar).unwrap();
        } else {
            fs::create_dir(&sidecar).await.unwrap();
        }
        assert!(freezer.load_frozen("mock", None).await.is_err());
    }
}