supercode-cli 0.4.20

supercode — a lightweight, fully-customizable AI coding agent CLI in Rust. Any model via OpenRouter; natively continues Claude Code and Codex sessions.
//! ORCH-12 dev/01 acceptance at the CLI: `supercode memory show|search` read
//! each harness's own memory documents, and `--json` emits the RPC result
//! verbatim.
//!
//! Fully offline. Hermes and OpenClaw are read from the committed fixture
//! homes the way a real install reaches them (`HERMES_HOME`,
//! `OPENCLAW_STATE_DIR`); Claude Code is read from a temp home
//! (`CLAUDE_CONFIG_DIR`) plus a temp working tree, so no real harness home is
//! ever opened or written.

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

fn bin() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}

fn fixtures() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../harness/tests/fixtures")
}

/// A unique temp directory. The counter matters: the clock's resolution is
/// coarse enough that two test threads can stamp the same nanosecond, and a
/// shared scratch directory means one test deletes another's working tree.
fn scratch(tag: &str) -> PathBuf {
    static SEQUENCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let dir = std::env::temp_dir().join(format!(
        "supercode-orch12-{tag}-{}-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos(),
        SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
    ));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

/// Every harness home is pinned at an absent path unless the caller names it,
/// so a read can never fall through to this machine's real installs.
fn run(cwd: &Path, envs: &[(&str, PathBuf)], args: &[&str]) -> Output {
    let home = scratch("home");
    let mut command = Command::new(bin());
    command
        .current_dir(cwd)
        .env("SUPERCODE_HOME", home.join("sessions"))
        .env("HOME", &home)
        .env("CLAUDE_CONFIG_DIR", home.join("absent-claude"))
        .env("CODEX_HOME", home.join("absent-codex"))
        .env("OPENCODE_DB", home.join("absent-opencode"))
        .env("PI_CODING_AGENT_DIR", home.join("absent-pi"))
        .env("HERMES_HOME", home.join("absent-hermes"))
        .env("OPENCLAW_STATE_DIR", home.join("absent-openclaw"))
        .env("NO_COLOR", "1")
        .env_remove("OPENROUTER_API_KEY");
    for (key, value) in envs {
        command.env(key, value);
    }
    let output = command
        .args(args)
        .stdin(std::process::Stdio::null())
        .output()
        .expect("supercode binary runs");
    let _ = std::fs::remove_dir_all(&home);
    output
}

fn stdout(cwd: &Path, envs: &[(&str, PathBuf)], args: &[&str]) -> String {
    let output = run(cwd, envs, args);
    assert!(
        output.status.success(),
        "`supercode {}` failed: {}",
        args.join(" "),
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8(output.stdout).unwrap()
}

fn result(
    cwd: &Path,
    envs: &[(&str, PathBuf)],
    args: &[&str],
    key: &str,
) -> Vec<serde_json::Value> {
    let text = stdout(cwd, envs, args);
    let value: serde_json::Value =
        serde_json::from_str(&text).expect("memory --json is one JSON object");
    assert_eq!(value["schema"], "supercode.memory.v1", "{value:#}");
    value[key].as_array().cloned().unwrap_or_default()
}

fn find<'a>(rows: &'a [serde_json::Value], profile: &str, name: &str) -> &'a serde_json::Value {
    rows.iter()
        .find(|row| row["profile"] == profile && row["name"] == name)
        .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
}

#[test]
fn memory_show_json_reads_the_hermes_fixture_home() {
    let cwd = scratch("cwd");
    let rows = result(
        &cwd,
        &[("HERMES_HOME", fixtures().join("hermes_home"))],
        &["memory", "show", "--harness", "hermes", "--json"],
        "documents",
    );

    let notes = find(&rows, "default", "MEMORY.md");
    assert_eq!(notes["harness"], "hermes");
    assert_eq!(notes["scope"], "user");
    // The head only: a body crosses the wire only under `--full`.
    assert!(notes.get("content").is_none(), "{notes:#?}");

    let user = find(&rows, "default", "USER.md");
    assert!(user["size"].as_u64().unwrap() > 0);

    find(&rows, "default", "memories/2026-09-01-notes.md");

    let coder = find(&rows, "coder", "MEMORY.md");
    assert_eq!(coder["scope"], "profile");

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

#[test]
fn memory_show_full_returns_the_body_and_the_table_names_every_column() {
    let cwd = scratch("cwd");
    let homes = [("HERMES_HOME", fixtures().join("hermes_home"))];
    let rows = result(
        &cwd,
        &homes,
        &[
            "memory",
            "show",
            "--harness",
            "hermes",
            "--profile",
            "coder",
            "--full",
            "--json",
        ],
        "documents",
    );
    assert!(
        rows.iter().all(|row| row["profile"] == "coder"),
        "{rows:#?}"
    );
    assert!(find(&rows, "coder", "MEMORY.md")["content"]
        .as_str()
        .expect("--full returns the body")
        .contains("anthropic/claude-opus-4-8"));

    let table = stdout(
        &cwd,
        &homes,
        &[
            "memory",
            "show",
            "--harness",
            "hermes",
            "--profile",
            "coder",
        ],
    );
    let header = table
        .lines()
        .find(|line| line.contains("HARNESS"))
        .unwrap_or_else(|| panic!("no header in:\n{table}"));
    for column in ["SCOPE", "PROFILE", "NAME", "BYTES", "UPDATED"] {
        assert!(header.contains(column), "{header}");
    }
    let row = table
        .lines()
        .find(|line| line.contains("MEMORY.md"))
        .unwrap_or_else(|| panic!("no MEMORY.md row in:\n{table}"));
    assert!(row.contains("hermes"), "{row}");
    assert!(row.contains("profile"), "{row}");
    assert!(row.contains("coder"), "{row}");

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

#[test]
fn memory_show_reads_the_openclaw_agent_workspaces() {
    let cwd = scratch("cwd");
    let rows = result(
        &cwd,
        &[("OPENCLAW_STATE_DIR", fixtures().join("openclaw_home"))],
        &["memory", "show", "--harness", "openclaw", "--json"],
        "documents",
    );
    let main = find(&rows, "main", "MEMORY.md");
    assert_eq!(main["scope"], "agent");
    assert!(main["path"]
        .as_str()
        .unwrap()
        .ends_with("openclaw_home/workspace/MEMORY.md"));
    find(&rows, "main", "memory/2026-09-01-standup.md");
    find(&rows, "design", "MEMORY.md");
    let _ = std::fs::remove_dir_all(&cwd);
}

/// Claude Code from a temp home: the auto-memory directory of the project the
/// working tree belongs to, resolved through the enclosing git repository.
#[test]
fn memory_show_reads_a_claude_code_temp_project_directory() {
    let claude_home = scratch("claude");
    let project = scratch("project");
    std::fs::create_dir_all(project.join(".git")).unwrap();
    // The slug is built from the path the process actually runs in, which on
    // macOS is the canonical `/private/var/...` form of a temp dir.
    let resolved = std::fs::canonicalize(&project).unwrap();
    let slug: String = resolved
        .to_string_lossy()
        .chars()
        .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
        .collect();
    let memory = claude_home.join("projects").join(&slug).join("memory");
    std::fs::create_dir_all(&memory).unwrap();
    std::fs::write(
        memory.join("MEMORY.md"),
        "# index\n- [pins](pins.md) — the pinned harnesses\n",
    )
    .unwrap();
    std::fs::write(memory.join("pins.md"), "hermes 0.19.0 is the pin\n").unwrap();

    let homes = [("CLAUDE_CONFIG_DIR", claude_home.clone())];
    let rows = result(
        &project,
        &homes,
        &["memory", "show", "--harness", "claude-code", "--json"],
        "documents",
    );
    let index = find(&rows, &slug, "MEMORY.md");
    assert_eq!(index["harness"], "claude-code");
    assert_eq!(index["scope"], "project");
    find(&rows, &slug, "pins.md");

    let hits = result(
        &project,
        &homes,
        &[
            "memory",
            "search",
            "--harness",
            "claude-code",
            "0.19.0",
            "--json",
        ],
        "matches",
    );
    assert_eq!(hits.len(), 1, "{hits:#?}");
    assert_eq!(hits[0]["name"], "pins.md");
    assert_eq!(hits[0]["line"], 1);

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

#[test]
fn memory_search_reports_a_hit_by_line_and_a_miss_as_no_rows() {
    let cwd = scratch("cwd");
    let homes = [("HERMES_HOME", fixtures().join("hermes_home"))];
    let hits = result(
        &cwd,
        &homes,
        &[
            "memory",
            "search",
            "--harness",
            "hermes",
            "NEOVIM",
            "--json",
        ],
        "matches",
    );
    assert_eq!(hits.len(), 1, "{hits:#?}");
    assert_eq!(hits[0]["name"], "USER.md");
    assert_eq!(hits[0]["line"], 5);

    let human = stdout(
        &cwd,
        &homes,
        &["memory", "search", "--harness", "hermes", "neovim"],
    );
    assert!(human.contains("USER.md:5:"), "{human}");

    let miss = result(
        &cwd,
        &homes,
        &[
            "memory",
            "search",
            "--harness",
            "hermes",
            "no-memory-line-says-this",
            "--json",
        ],
        "matches",
    );
    assert!(miss.is_empty(), "{miss:#?}");

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

/// A harness supercode reads no memory store for is refused by name — the
/// uniform-verb contract, never a silent empty listing.
#[test]
fn memory_refuses_a_harness_without_a_store() {
    let cwd = scratch("cwd");
    for args in [
        vec!["memory", "show", "--harness", "codex"],
        vec!["memory", "search", "--harness", "opencode", "anything"],
    ] {
        let output = run(&cwd, &[], &args);
        assert!(!output.status.success(), "{args:?} must fail");
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(stderr.contains("unsupported action"), "{stderr}");
        assert!(stderr.contains("claude-code, hermes, openclaw"), "{stderr}");
    }
    let _ = std::fs::remove_dir_all(&cwd);
}