skillnet 0.6.0

Manage canonical AI skill stores, derived views, and calibration data for multi-phase-plan.
Documentation
use std::{fs, os::unix::fs as unix_fs, path::Path};

use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::{tempdir, TempDir};

struct Fixture {
    tmp: TempDir,
}

impl Fixture {
    fn new() -> Self {
        Self {
            tmp: tempdir().unwrap(),
        }
    }

    fn root(&self) -> &Path {
        self.tmp.path()
    }

    fn path(&self, rel: &str) -> std::path::PathBuf {
        self.root().join(rel)
    }

    fn write_config(&self, body: impl AsRef<str>) -> std::path::PathBuf {
        let path = self.path("skillnet.toml");
        fs::write(&path, body.as_ref()).unwrap();
        path
    }

    fn command(&self, config: &Path) -> Command {
        let mut command = Command::cargo_bin("skillnet").unwrap();
        command.args([
            "--config",
            config.to_str().unwrap(),
            "--mirror-root",
            self.root().join("mirror").to_str().unwrap(),
        ]);
        command
    }
}

fn write_skill(root: &Path, name: &str) {
    let dir = root.join(name);
    fs::create_dir_all(&dir).unwrap();
    fs::write(dir.join("SKILL.md"), name).unwrap();
}

#[test]
fn doctor_checks_project_views_and_aggregator() {
    let fixture = Fixture::new();
    fs::create_dir_all(fixture.path("mirror/global")).unwrap();
    let project = fixture.path("repos/demo");
    let canonical = project.join(".skills");
    write_skill(&canonical, "alpha");
    write_skill(&canonical, "beta");

    let view = project.join(".claude/skills");
    fs::create_dir_all(&view).unwrap();
    unix_fs::symlink("../../.skills/alpha", view.join("alpha")).unwrap();
    unix_fs::symlink("../../.skills/beta", view.join("beta")).unwrap();

    let aggregator = fixture.path("mirror/projects/demo");
    fs::create_dir_all(aggregator.parent().unwrap()).unwrap();
    unix_fs::symlink(&canonical, &aggregator).unwrap();

    let config = fixture.write_config(format!(
        r#"
[global]
views = []

[[projects]]
name = "demo"
path = "{}"
canonical_rel = ".skills"
views = [{{ rel = ".claude/skills", label = "claude" }}]
"#,
        project.display()
    ));

    fixture
        .command(&config)
        .arg("doctor")
        .assert()
        .success()
        .stdout(predicate::str::contains("doctor: no issues"));

    fs::remove_file(view.join("beta")).unwrap();
    unix_fs::symlink("../../.skills/missing", view.join("beta")).unwrap();

    fixture
        .command(&config)
        .arg("doctor")
        .assert()
        .code(1)
        .stderr(predicate::str::contains("beta"))
        .stderr(predicate::str::contains("expected ../../.skills/beta"));
}