AkurAI Build
Menu

AkurAI-Build

public

Latest change 18b39dea5ae62e0776a868d194eb39d528555ca9 - cli: propagate manifest-read errors so doctor exits 1, not 3 (closes #9) by Olafur Bui

//! End-to-end CLI wiring checks against the built binary.
//!
//! Architecture review 8 (P2): the clap argument plumbing for keygen,
//! migrate, vector put/search, backup, restore acknowledgement, and the
//! doctor exit-code contract had no subprocess coverage; only the
//! underlying library functions were tested.

#![cfg(unix)]

use std::{fs, process::Command};

use anyhow::{Context, Result, ensure};

fn bunfork() -> Command {
    Command::new(env!("CARGO_BIN_EXE_bunfork"))
}

fn run(command: &mut Command) -> Result<Vec<u8>> {
    let output = command.output().context("spawn bunfork")?;
    ensure!(
        output.status.success(),
        "command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    Ok(output.stdout)
}

#[test]
fn cli_wires_keygen_migrate_vector_backup_restore_and_doctor() -> Result<()> {
    let directory = tempfile::tempdir()?;
    let database = directory.path().join("data/bunfork.db");
    let key_file = directory.path().join("db.key");
    let content = directory.path().join("note.txt");
    fs::write(&content, "hello vectors")?;

    run(bunfork().arg("keygen").arg("--out").arg(&key_file))?;

    run(bunfork()
        .arg("--database")
        .arg(&database)
        .arg("--key-file")
        .arg(&key_file)
        .arg("migrate"))?;

    run(bunfork()
        .arg("--database")
        .arg(&database)
        .arg("--key-file")
        .arg(&key_file)
        .args(["vector", "put", "note-1", "--content-file"])
        .arg(&content)
        .args(["--embedding", "1,0"]))?;

    let stdout = run(bunfork()
        .arg("--database")
        .arg(&database)
        .arg("--key-file")
        .arg(&key_file)
        .args(["vector", "search", "--embedding", "1,0", "--limit", "1"]))?;
    let matches: serde_json::Value = serde_json::from_slice(&stdout)?;
    ensure!(matches[0]["id"] == "note-1", "unexpected search result");

    let backup = directory.path().join("backup.db");
    let stdout = run(bunfork()
        .arg("--database")
        .arg(&database)
        .arg("--key-file")
        .arg(&key_file)
        .args(["backup", "--json", "--out"])
        .arg(&backup))?;
    let report: serde_json::Value = serde_json::from_slice(&stdout)?;
    ensure!(report["ok"] == true, "backup report not ok");
    ensure!(backup.is_file(), "backup file missing");

    // Omitting the required --yes acknowledgement is clap misuse: exit 2.
    let output = bunfork()
        .arg("--database")
        .arg(&database)
        .arg("--key-file")
        .arg(&key_file)
        .arg("restore")
        .arg(&backup)
        .output()?;
    ensure!(
        output.status.code() == Some(2),
        "restore without --yes must be a usage error (exit 2)"
    );

    // Verification failure has its own exit code (3) and names the file.
    let output = bunfork()
        .args([
            "doctor",
            "--static",
            "tests/fixtures/static/mpa/site",
            "--manifest",
            "tests/fixtures/static/spa/manifest.json",
            "--json",
        ])
        .output()?;
    ensure!(
        output.status.code() == Some(3),
        "doctor mismatch must exit 3"
    );
    let report: serde_json::Value = serde_json::from_slice(&output.stdout)?;
    ensure!(report["ok"] == false, "doctor report must fail");
    ensure!(
        report["checks"][0]["message"]
            .as_str()
            .is_some_and(|message| message.contains("missing file")),
        "doctor report must name the divergence"
    );
    Ok(())
}

/// Doctor's exit-code contract distinguishes operational failures (exit 1,
/// anyhow's default: 'doctor could not run') from a verified-but-diverging
/// artifact (exit 3: 'artifact is bad'). An unreadable manifest is an
/// operational failure, not a verification failure.
#[test]
fn doctor_exit_code_distinguishes_operational_failure_from_verification_failure() -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    let directory = tempfile::tempdir()?;
    let manifest = directory.path().join("manifest.json");
    fs::copy("tests/fixtures/static/spa/manifest.json", &manifest)?;
    fs::set_permissions(&manifest, fs::Permissions::from_mode(0o000))?;

    let output = bunfork()
        .args([
            "doctor",
            "--static",
            "tests/fixtures/static/mpa/site",
            "--manifest",
        ])
        .arg(&manifest)
        .output()?;
    fs::set_permissions(&manifest, fs::Permissions::from_mode(0o644))?;
    ensure!(
        output.status.code() == Some(1),
        "unreadable manifest must exit 1 (doctor could not run), got {:?}: {}",
        output.status.code(),
        String::from_utf8_lossy(&output.stderr)
    );

    // A byte-divergent artifact is still a verification failure (exit 3).
    let output = bunfork()
        .args([
            "doctor",
            "--static",
            "tests/fixtures/static/mpa/site",
            "--manifest",
            "tests/fixtures/static/spa/manifest.json",
        ])
        .output()?;
    ensure!(
        output.status.code() == Some(3),
        "byte-divergent artifact must still exit 3"
    );
    Ok(())
}