modelshelf 0.1.0

A shared local LLM model registry: discover, deduplicate, download, and update models across desktop apps.
Documentation
//! Dedup engine safety tests: plan/apply/undo/gc against fake ecosystems.
//! These tests use inode identity, so the physical-linking assertions are
//! Unix-only; planning/refusal logic is asserted everywhere.

use std::collections::HashMap;

use modelshelf::dedup::DedupOptions;
use modelshelf::gguf::fixtures::{gguf_bytes, write_gguf};
use modelshelf::scan::fixtures;
use modelshelf::{Ecosystem, ModelId, OpenOptions, ScanEnv, ScanOptions, Shelf};

/// Dedup options tuned for tiny test files: no size floor, no mtime grace.
fn test_options() -> DedupOptions {
    DedupOptions {
        min_size_bytes: 0,
        recent_grace_secs: 0,
        ..Default::default()
    }
}

struct Fx {
    _tmp: tempfile::TempDir,
    shelf: Shelf,
    scan: ScanOptions,
    ollama_root: std::path::PathBuf,
    lmstudio_root: std::path::PathBuf,
    hf_root: std::path::PathBuf,
}

fn fixture() -> Fx {
    let tmp = tempfile::tempdir().unwrap();
    let base = tmp.path();
    let (ollama_root, lmstudio_root, hf_root) =
        (base.join("ollama"), base.join("lmstudio"), base.join("hf"));
    for d in [&ollama_root, &lmstudio_root, &hf_root, &base.join("home")] {
        std::fs::create_dir_all(d).unwrap();
    }
    let shelf =
        Shelf::open(OpenOptions::at(base.join("shelf")).app_id("com.example.test")).unwrap();
    let scan = ScanOptions {
        env: ScanEnv {
            home_dir: Some(base.join("home")),
            roots: HashMap::from([
                (Ecosystem::Ollama, ollama_root.clone()),
                (Ecosystem::Lmstudio, lmstudio_root.clone()),
                (Ecosystem::HfCache, hf_root.clone()),
            ]),
        },
        ..Default::default()
    };
    Fx {
        _tmp: tmp,
        shelf,
        scan,
        ollama_root,
        lmstudio_root,
        hf_root,
    }
}

#[test]
fn plan_never_targets_read_only_ecosystems() {
    let fx = fixture();
    let content = gguf_bytes("Everywhere", "llama", 15, 4096, b"EVERYWHERE");
    // Same content in Ollama, HF cache, and twice in LM Studio.
    fixtures::ollama::add_model(&fx.ollama_root, "everywhere", "latest", &content);
    fixtures::hf_cache::add_model(
        &fx.hf_root,
        "org/Everywhere-GGUF",
        "rev",
        "e.gguf",
        &content,
    );
    write_gguf(
        &fx.lmstudio_root.join("org/Everywhere-GGUF/e1.gguf"),
        "Everywhere",
        "llama",
        15,
        4096,
        b"EVERYWHERE",
    );
    write_gguf(
        &fx.lmstudio_root.join("org/Everywhere-GGUF/e2.gguf"),
        "Everywhere",
        "llama",
        15,
        4096,
        b"EVERYWHERE",
    );
    fx.shelf.scan(&fx.scan).unwrap();

    let plan = fx.shelf.plan_dedup(&test_options()).unwrap();

    // THE safety property: no write target inside Ollama or the HF cache.
    for action in &plan.actions {
        assert!(
            !action.target.starts_with(&fx.ollama_root),
            "plan targets Ollama internals: {}",
            action.target.display()
        );
        assert!(
            !action.target.starts_with(&fx.hf_root),
            "plan targets HF cache internals: {}",
            action.target.display()
        );
        assert!(action.ecosystem.is_writable());
    }
    // Both LM Studio copies are planned.
    assert_eq!(plan.actions.len(), 2);
    // The blob doesn't exist yet, so exactly one adoption is planned.
    assert_eq!(plan.adoptions.len(), 1);
}

#[cfg(unix)]
#[test]
fn apply_links_duplicates_to_store_and_undo_restores_them() {
    use std::os::unix::fs::MetadataExt;

    let fx = fixture();
    let payload = b"DEDUP-ME-PLEASE";
    let a = fx.lmstudio_root.join("org/Model-GGUF/copy-a.gguf");
    let b = fx.lmstudio_root.join("org/Model-GGUF/copy-b.gguf");
    write_gguf(&a, "Dedup Me", "llama", 15, 4096, payload);
    write_gguf(&b, "Dedup Me", "llama", 15, 4096, payload);
    fx.shelf.scan(&fx.scan).unwrap();

    let before_a = std::fs::metadata(&a).unwrap().ino();
    let before_b = std::fs::metadata(&b).unwrap().ino();
    assert_ne!(std::fs::read(&a).unwrap().len(), 0);
    assert_ne!(before_a, before_b, "test setup: independent files");

    let plan = fx.shelf.plan_dedup(&test_options()).unwrap();
    assert_eq!(plan.actions.len(), 2);
    let content_before = std::fs::read(&a).unwrap();

    let report = fx.shelf.apply_dedup(&plan).unwrap();
    assert_eq!(report.applied, 2);
    assert!(report.skipped.is_empty(), "{:?}", report.skipped);

    // Both copies now share the store blob's inode; content is unchanged.
    let id: &ModelId = &fx.shelf.list().unwrap()[0].id;
    let blob = fx.shelf.blob_path(id).unwrap();
    let blob_ino = std::fs::metadata(&blob).unwrap().ino();
    assert_eq!(std::fs::metadata(&a).unwrap().ino(), blob_ino);
    assert_eq!(std::fs::metadata(&b).unwrap().ino(), blob_ino);
    assert_eq!(std::fs::read(&a).unwrap(), content_before);

    // Registry reflects the new roles.
    let entry = &fx.shelf.list().unwrap()[0];
    let hardlinks = entry
        .locations
        .iter()
        .filter(|l| l.role == modelshelf::LocationRole::Hardlink)
        .count();
    assert_eq!(hardlinks, 2);

    // Undo restores independent inodes with identical content.
    let undo = fx.shelf.undo(&report.op_id).unwrap();
    assert_eq!(undo.restored, 2);
    let after_a = std::fs::metadata(&a).unwrap();
    assert_ne!(after_a.ino(), blob_ino, "undo must break the link");
    assert_eq!(std::fs::read(&a).unwrap(), content_before);
    assert_eq!(std::fs::read(&b).unwrap(), content_before);
}

#[cfg(unix)]
#[test]
fn apply_skips_files_changed_after_planning() {
    let fx = fixture();
    let payload = b"WILL-CHANGE";
    let a = fx.lmstudio_root.join("org/M-GGUF/a.gguf");
    let b = fx.lmstudio_root.join("org/M-GGUF/b.gguf");
    write_gguf(&a, "M", "llama", 15, 4096, payload);
    write_gguf(&b, "M", "llama", 15, 4096, payload);
    fx.shelf.scan(&fx.scan).unwrap();

    let plan = fx.shelf.plan_dedup(&test_options()).unwrap();
    assert_eq!(plan.actions.len(), 2);

    // A third party rewrites one file between plan and apply.
    std::fs::write(&a, b"user overwrote this with something precious").unwrap();
    let precious = std::fs::read(&a).unwrap();

    let report = fx.shelf.apply_dedup(&plan).unwrap();
    assert_eq!(report.applied, 1, "unchanged copy still deduped");
    assert_eq!(report.skipped.len(), 1);
    assert_eq!(report.skipped[0].path, a);
    assert_eq!(
        std::fs::read(&a).unwrap(),
        precious,
        "changed file must remain untouched"
    );
}

#[test]
fn gc_deletes_only_orphan_blobs() {
    let fx = fixture();
    // A registered model with a store copy but no refs.
    let registered = gguf_bytes("Registered", "llama", 15, 4096, b"REGISTERED");
    write_gguf(
        &fx.lmstudio_root.join("o/R-GGUF/r1.gguf"),
        "Registered",
        "llama",
        15,
        4096,
        b"REGISTERED",
    );
    write_gguf(
        &fx.lmstudio_root.join("o/R-GGUF/r2.gguf"),
        "Registered",
        "llama",
        15,
        4096,
        b"REGISTERED",
    );
    fx.shelf.scan(&fx.scan).unwrap();
    let plan = fx.shelf.plan_dedup(&test_options()).unwrap();
    fx.shelf.apply_dedup(&plan).unwrap();
    let _ = registered;

    // An orphan blob: correct name shape, but no registry entry.
    let orphan_hex = "f".repeat(64);
    let orphan = fx.shelf.paths().blob_file(&orphan_hex);
    std::fs::write(&orphan, b"ORPHANED BYTES").unwrap();

    // Dry-run reports, deletes nothing.
    let report = fx.shelf.gc(false).unwrap();
    assert_eq!(report.orphan_blobs, vec![orphan.clone()]);
    assert_eq!(report.deleted, 0);
    assert!(orphan.exists());
    assert_eq!(report.unreferenced.len(), 1, "ref-less model is reported");

    // Apply deletes the orphan but never the registered blob.
    let report = fx.shelf.gc(true).unwrap();
    assert_eq!(report.deleted, 1);
    assert!(!orphan.exists());
    let entry = &fx.shelf.list().unwrap()[0];
    assert!(
        fx.shelf.blob_path(&entry.id).is_ok(),
        "registered blob survives gc"
    );
}

#[test]
fn cloud_synced_paths_are_refused() {
    let fx = fixture();
    // Two copies under a simulated OneDrive folder inside LM Studio's tree.
    let payload = b"CLOUDY";
    write_gguf(
        &fx.lmstudio_root.join("OneDrive/M-GGUF/a.gguf"),
        "M",
        "llama",
        15,
        4096,
        payload,
    );
    write_gguf(
        &fx.lmstudio_root.join("OneDrive/M-GGUF/b.gguf"),
        "M",
        "llama",
        15,
        4096,
        payload,
    );
    fx.shelf.scan(&fx.scan).unwrap();

    let plan = fx.shelf.plan_dedup(&test_options()).unwrap();
    assert!(plan.actions.is_empty());
    assert_eq!(plan.skipped.len(), 2);
    assert!(plan.skipped[0].reason.contains("cloud-synced"));
}