upskill 0.3.1

Author and distribute AI-assistance content across coding agents
Documentation
use assert_cmd::Command;
use predicates::str::contains;
use std::io::{Read, Write};
use tempfile::tempdir;

fn mock_skills_server(json_body: &'static str) -> std::net::SocketAddr {
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    std::thread::spawn(move || {
        if let Ok((mut stream, _)) = listener.accept() {
            let mut buf = [0u8; 4096];
            let _ = stream.read(&mut buf);
            let response = format!(
                "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
                json_body.len(),
                json_body
            );
            let _ = stream.write_all(response.as_bytes());
        }
    });
    addr
}

const MOCK_RESPONSE: &str = r#"{
  "query": "rust",
  "searchType": "fuzzy",
  "skills": [
    {
      "id": "github/awesome-copilot/rust-mcp-server-generator",
      "skillId": "rust-mcp-server-generator",
      "name": "rust-mcp-server-generator",
      "installs": 7608,
      "source": "github/awesome-copilot"
    },
    {
      "id": "github/anthropics/rust-analyzer",
      "skillId": "rust-analyzer",
      "name": "rust-analyzer",
      "installs": 3200,
      "source": "github/anthropics/skills"
    }
  ],
  "count": 2,
  "duration_ms": 12
}"#;

const EMPTY_RESPONSE: &str = r#"{
  "query": "zzznomatch",
  "searchType": "fuzzy",
  "skills": [],
  "count": 0,
  "duration_ms": 5
}"#;

#[test]
fn search_requires_query() {
    let cwd = tempdir().expect("must create temp dir");
    Command::cargo_bin("upskill")
        .unwrap()
        .current_dir(cwd.path())
        .args(["search"])
        .assert()
        .code(2);
}

#[test]
fn search_help_exits_zero() {
    let cwd = tempdir().expect("must create temp dir");
    Command::cargo_bin("upskill")
        .unwrap()
        .current_dir(cwd.path())
        .args(["search", "--help"])
        .assert()
        .code(0);
}

#[test]
fn search_returns_results() {
    let addr = mock_skills_server(MOCK_RESPONSE);
    let cwd = tempdir().expect("must create temp dir");

    Command::cargo_bin("upskill")
        .unwrap()
        .current_dir(cwd.path())
        .env("UPSKILL_REGISTRY_URL", format!("http://{}", addr))
        .args(["search", "rust"])
        .assert()
        .code(0)
        .stdout(contains("rust-mcp-server-generator"))
        .stdout(contains("awesome-copilot"))
        .stdout(contains("rust-analyzer"));
}

#[test]
fn search_shows_install_command() {
    let addr = mock_skills_server(MOCK_RESPONSE);
    let cwd = tempdir().expect("must create temp dir");

    Command::cargo_bin("upskill")
        .unwrap()
        .current_dir(cwd.path())
        .env("UPSKILL_REGISTRY_URL", format!("http://{}", addr))
        .args(["search", "rust"])
        .assert()
        .code(0)
        .stdout(contains(
            "upskill add awesome-copilot --skill rust-mcp-server-generator",
        ));
}

#[test]
fn search_empty_results() {
    let addr = mock_skills_server(EMPTY_RESPONSE);
    let cwd = tempdir().expect("must create temp dir");

    Command::cargo_bin("upskill")
        .unwrap()
        .current_dir(cwd.path())
        .env("UPSKILL_REGISTRY_URL", format!("http://{}", addr))
        .args(["search", "zzznomatch"])
        .assert()
        .code(0)
        .stdout(contains("no skills found"));
}

#[test]
fn search_unreachable_registry_exits_one() {
    let cwd = tempdir().expect("must create temp dir");

    Command::cargo_bin("upskill")
        .unwrap()
        .current_dir(cwd.path())
        .env("UPSKILL_REGISTRY_URL", "http://127.0.0.1:1")
        .args(["search", "rust"])
        .assert()
        .code(1)
        .stderr(contains("error:"));
}