afterautism-cli 2.0.0

Headless command-line interface for the AfterAutism engine
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 afterautism project contributors

//! End-to-end test of the CLI binary: create → ingest → query → export →
//! backup → restore, exercising the full engine headlessly.

use std::path::PathBuf;
use std::process::Command;

fn bin() -> PathBuf {
    // cargo sets CARGO_BIN_EXE_<name> for integration tests.
    PathBuf::from(env!("CARGO_BIN_EXE_afterautism"))
}

fn unique_dir(tag: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "aa-cli-e2e-{tag}-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ))
}

fn run(args: &[&str]) -> String {
    let out = Command::new(bin())
        .args(args)
        .output()
        .expect("run afterautism");
    assert!(
        out.status.success(),
        "command failed: {args:?}\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr),
    );
    String::from_utf8_lossy(&out.stdout).to_string()
}

#[test]
fn full_engine_flow_end_to_end() {
    let dir = unique_dir("flow");
    std::fs::create_dir_all(&dir).expect("mkdir");
    let corpus = dir.join("corpus.db");
    let csv = dir.join("data.csv");
    let md = dir.join("note.md");
    let backup = dir.join("corpus.backup");

    std::fs::write(&csv, "name,age\nalice,30\nbob,25\n").expect("csv");
    std::fs::write(&md, "# Home\nSee [[Projects]].\n## Projects\nThings.\n").expect("md");

    // create
    run(&["create", corpus.to_str().unwrap()]);

    // ingest both files (different adapters, id namespaces)
    run(&[
        "ingest",
        "--corpus",
        corpus.to_str().unwrap(),
        csv.to_str().unwrap(),
        md.to_str().unwrap(),
    ]);

    // export: 3 csv nodes (header + 2) + 2 md nodes = 5
    let exported = run(&["export", "--corpus", corpus.to_str().unwrap()]);
    let lines: Vec<&str> = exported.lines().collect();
    assert_eq!(lines.len(), 5, "5 nodes exported: {exported}");

    // query: text search finds Projects
    let out = run(&["query", "--corpus", corpus.to_str().unwrap(), "Projects"]);
    assert!(out.contains("Projects"), "query finds Projects: {out}");

    // traversal query: links from Home reach Projects
    let out = run(&[
        "query",
        "--corpus",
        corpus.to_str().unwrap(),
        "--",
        "->link:(Home)",
    ]);
    assert!(
        out.contains("Projects"),
        "traversal reaches Projects: {out}"
    );

    // backup + restore round-trip keeps the corpus intact
    run(&[
        "backup",
        "--corpus",
        corpus.to_str().unwrap(),
        backup.to_str().unwrap(),
    ]);
    run(&[
        "restore",
        "--corpus",
        corpus.to_str().unwrap(),
        backup.to_str().unwrap(),
    ]);
    let after = run(&["export", "--corpus", corpus.to_str().unwrap()]);
    assert_eq!(after.lines().count(), 5, "restore keeps all nodes");

    let _ = std::fs::remove_dir_all(&dir);
}