a3s 0.10.0

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::path::PathBuf;
use std::process::Command;

#[cfg(unix)]
mod support;

fn a3s_binary() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_a3s"))
}

#[test]
fn knowledge_and_memory_commands_use_typed_json_and_the_effective_directory() {
    let directory = tempfile::tempdir().expect("temp directory");
    let launch_directory = directory.path().join("launch");
    let workspace = directory.path().join("workspace");
    let memory = workspace.join("relative-memory");
    std::fs::create_dir_all(&launch_directory).expect("launch directory");
    std::fs::create_dir_all(memory.join("items")).expect("memory directory");
    std::fs::write(
        memory.join("index.json"),
        r#"[{"id":"mem-1","content_lower":"needle decision","tags":["architecture"],"importance":0.8,"timestamp":"2026-07-15T00:00:00Z","memory_type":"semantic"}]"#,
    )
    .expect("memory index");
    std::fs::write(
        memory.join("items/mem-1.json"),
        r#"{"content":"Needle decision from the effective workspace","metadata":{"source":"test"}}"#,
    )
    .expect("memory item");
    let canonical_workspace = workspace.canonicalize().expect("canonical workspace");

    let run = |args: &[&str]| {
        Command::new(a3s_binary())
            .current_dir(&launch_directory)
            .env("HOME", directory.path().join("home"))
            .env("A3S_MEMORY_DIR", "relative-memory")
            .env_remove("A3S_CONFIG_FILE")
            .arg("-C")
            .arg(&workspace)
            .args(["--output", "json"])
            .args(args)
            .output()
            .expect("run typed Code knowledge command")
    };

    let kb_path = run(&["code", "kb", "path"]);
    assert!(
        kb_path.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&kb_path.stderr)
    );
    let kb_path: serde_json::Value = serde_json::from_slice(&kb_path.stdout).expect("KB JSON");
    assert_eq!(kb_path["command"], "code.kb.path");
    assert_eq!(
        kb_path["data"]["path"],
        canonical_workspace.join(".a3s/kb").display().to_string()
    );

    let add = run(&["code", "kb", "add", "typed knowledge note"]);
    assert!(
        add.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&add.stderr)
    );
    let add: serde_json::Value = serde_json::from_slice(&add.stdout).expect("KB add JSON");
    assert_eq!(add["command"], "code.kb.add");
    assert_eq!(add["data"]["created"], true);

    let search = run(&["code", "kb", "search", "typed knowledge"]);
    assert!(search.status.success());
    let search: serde_json::Value = serde_json::from_slice(&search.stdout).expect("KB search JSON");
    assert_eq!(search["command"], "code.kb.search");
    assert_eq!(search["data"]["hits"].as_array().unwrap().len(), 1);

    let memories = run(&["code", "memory", "list", "needle"]);
    assert!(
        memories.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&memories.stderr)
    );
    let memories: serde_json::Value =
        serde_json::from_slice(&memories.stdout).expect("memory JSON");
    assert_eq!(memories["command"], "code.memory.list");
    assert_eq!(
        memories["data"]["path"],
        canonical_workspace
            .join("relative-memory")
            .display()
            .to_string()
    );
    assert_eq!(memories["data"]["entries"][0]["id"], "mem-1");
    assert_eq!(
        memories["data"]["entries"][0]["content"],
        "Needle decision from the effective workspace"
    );
}

#[cfg(unix)]
#[test]
fn context_history_uses_typed_json_and_the_effective_child_directory() {
    let directory = tempfile::tempdir().expect("temp directory");
    let launch_directory = directory.path().join("launch");
    let workspace = directory.path().join("workspace");
    let bin = directory.path().join("bin");
    let cwd_log = directory.path().join("ctx-cwd.log");
    std::fs::create_dir_all(&launch_directory).expect("launch directory");
    std::fs::create_dir_all(&workspace).expect("workspace directory");
    support::make_executable(
        &bin.join("ctx"),
        &format!(
            "#!/bin/sh\nprintf '%s\\n' \"$PWD\" > {}\nif [ \"$1\" = \"search\" ]; then printf '%s\\n' '{{\"results\":[{{\"ctx_event_id\":\"ev-1\",\"ctx_session_id\":\"ses-1\",\"provider\":\"codex\",\"timestamp\":\"2026-07-15T01:02:03Z\",\"title\":\"Migration decision\",\"snippet\":\"Use explicit context\"}}]}}'; exit 0; fi\nprintf 'history for %s\\n' \"$*\"\n",
            support::sh_quote(&cwd_log)
        ),
    );
    let canonical_workspace = workspace.canonicalize().expect("canonical workspace");

    let output = Command::new(a3s_binary())
        .current_dir(&launch_directory)
        .env("HOME", directory.path().join("home"))
        .env("PATH", &bin)
        .arg("-C")
        .arg(&workspace)
        .args([
            "--output",
            "json",
            "code",
            "context",
            "search",
            "invocation context",
        ])
        .output()
        .expect("search context history");

    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let value: serde_json::Value = serde_json::from_slice(&output.stdout).expect("context JSON");
    assert_eq!(value["command"], "code.context.search");
    assert_eq!(value["data"]["hits"][0]["eventId"], "ev-1");
    assert_eq!(
        std::fs::read_to_string(cwd_log).unwrap().trim(),
        canonical_workspace.display().to_string()
    );
}