qq-cli 0.1.0

A modular Rust QQ task interface for coding agents
use qq_cli::{napcat::install::acquire, storage::references::References, storage::state::Store};
use serde_json::Value;
use std::time::{Duration, Instant};
mod common;
fn deadline() -> Instant {
    Instant::now() + Duration::from_secs(5)
}

#[test]
fn local_source_is_verified_and_cached_import_does_not_reopen_it() {
    let temp = tempfile::tempdir().unwrap();
    let store = Store::new(temp.path().join("state"));
    let refs = References::with_key(store.root().join("references"), [53; 32]);
    let (bytes, release) = common::synthetic_release();
    let path = temp.path().join("original.zip");
    std::fs::write(&path, bytes).unwrap();
    acquire::install(&release, Some(&path), &store, &refs, deadline()).unwrap();
    std::fs::remove_file(&path).unwrap();
    let cached: Value = serde_json::to_value(
        acquire::install(&release, Some(&path), &store, &refs, deadline()).unwrap(),
    )
    .unwrap();
    assert_eq!(cached["data"]["cached"], true);
    assert_eq!(store.read().unwrap().installations.len(), 1);
}

#[test]
fn rejected_sources_create_no_installation_and_do_not_echo_paths() {
    let temp = tempfile::tempdir().unwrap();
    let store = Store::new(temp.path().join("state"));
    let refs = References::with_key(store.root().join("references"), [54; 32]);
    let (_, release) = common::synthetic_release();
    let path = temp.path().join("synthetic-private-input.zip");
    let error = acquire::install(&release, Some(&path), &store, &refs, deadline())
        .err()
        .unwrap();
    assert_eq!(error.code, "ARTIFACT_SOURCE_UNAVAILABLE");
    assert!(
        !serde_json::to_string(&error)
            .unwrap()
            .contains("synthetic-private-input")
    );
    assert!(store.read().unwrap().installations.is_empty());
    std::fs::write(&path, b"original wrong size").unwrap();
    assert_eq!(
        acquire::install(&release, Some(&path), &store, &refs, deadline())
            .err()
            .unwrap()
            .code,
        "ARTIFACT_SOURCE_UNAVAILABLE"
    );
    let (mut bytes, _) = common::synthetic_release();
    bytes[0] ^= 1;
    std::fs::write(&path, bytes).unwrap();
    assert_eq!(
        acquire::install(&release, Some(&path), &store, &refs, deadline())
            .err()
            .unwrap()
            .code,
        "ARTIFACT_MISMATCH"
    );
    assert!(store.read().unwrap().installations.is_empty());
}

#[test]
fn public_install_checks_catalog_then_reports_missing_local_source() {
    use std::process::Command;
    let temp = tempfile::tempdir().unwrap();
    let state = temp.path().join("state");
    let config = temp.path().join("config.json");
    std::fs::write(
        &config,
        serde_json::to_vec(&qq_cli::storage::config::Config {
            state_dir: Some(state.clone()),
            ..Default::default()
        })
        .unwrap(),
    )
    .unwrap();
    let missing = temp.path().join("synthetic-private-input.zip");
    for (version, code) in [
        ("latest", "VERSION_UNSUPPORTED"),
        ("v4.18.38", "ARTIFACT_SOURCE_UNAVAILABLE"),
    ] {
        let output = Command::new(env!("CARGO_BIN_EXE_qq-cli"))
            .args(["napcat", "install", "--version", version, "--from"])
            .arg(&missing)
            .arg("--config")
            .arg(&config)
            .output()
            .unwrap();
        assert_eq!(
            output.status.code(),
            Some(if version == "latest" { 4 } else { 6 })
        );
        let value: Value = serde_json::from_slice(&output.stdout).unwrap();
        assert_eq!(value["error"]["code"], code);
        assert!(!String::from_utf8_lossy(&output.stdout).contains("synthetic-private-input"));
        assert!(output.stderr.is_empty());
        if version == "latest" {
            assert!(!state.exists());
        }
    }
}