memlay 0.1.5

Repo-native, conflict-resistant shared memory and codebase navigation layer for AI coding agents
//! `memlay init` must leave the repository queryable. The index build was once
//! a no-op behind an `if index {}` block, so `--no-index` did nothing and a
//! freshly initialized repo reported `files 0, symbols 0` until some later
//! command happened to trigger the lazy build — indistinguishable, to someone
//! onboarding, from a broken install.

use serde_json::Value;
use std::path::Path;
use std::process::Command;

fn memlay_bin() -> &'static str {
    env!("CARGO_BIN_EXE_memlay")
}

fn git(dir: &Path, args: &[&str]) {
    let out = Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "git {args:?}: {}",
        String::from_utf8_lossy(&out.stderr)
    );
}

/// A git repo holding one source file with a symbol the indexer will find.
fn fixture_repo() -> tempfile::TempDir {
    let tmp = tempfile::tempdir().unwrap();
    let dir = tmp.path();
    git(dir, &["init", "-b", "main"]);
    git(dir, &["config", "user.email", "test@example.com"]);
    git(dir, &["config", "user.name", "Test"]);
    git(dir, &["config", "commit.gpgsign", "false"]);
    std::fs::write(
        dir.join("app.ts"),
        "export function startGameLoop(fps: number): void {}\n",
    )
    .unwrap();
    git(dir, &["add", "-A"]);
    git(dir, &["commit", "-m", "base"]);
    tmp
}

fn run(dir: &Path, args: &[&str]) -> String {
    let out = Command::new(memlay_bin())
        .args(["--repo", dir.to_str().unwrap()])
        .args(args)
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "memlay {args:?}: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8_lossy(&out.stdout).into_owned()
}

fn stats(dir: &Path) -> Value {
    serde_json::from_str(&run(dir, &["--json", "index", "stats"])).unwrap()
}

#[test]
fn init_indexes_by_default() {
    let tmp = fixture_repo();
    let dir = tmp.path();
    let out: Value = serde_json::from_str(&run(dir, &["--json", "init"])).unwrap();

    assert_eq!(out["initialized"], true);
    assert!(
        out["indexed"]["files"].as_u64().unwrap() > 0,
        "init reported no indexed files: {out}"
    );

    // The index must be on disk, not merely reported.
    let s = stats(dir);
    assert!(s["files"].as_u64().unwrap() > 0, "index is empty: {s}");
    assert!(
        s["symbols"].as_u64().unwrap() > 0,
        "no symbols extracted: {s}"
    );
}

#[test]
fn no_index_defers_the_build_and_first_query_still_works() {
    let tmp = fixture_repo();
    let dir = tmp.path();
    let out: Value = serde_json::from_str(&run(dir, &["--json", "init", "--no-index"])).unwrap();

    assert_eq!(out["initialized"], true);
    assert!(
        out["indexed"].is_null(),
        "--no-index still built an index: {out}"
    );
    assert_eq!(stats(dir)["files"].as_u64().unwrap(), 0);

    // Deferred, not lost: the lazy path still builds on first query.
    run(dir, &["context", "start game loop"]);
    assert!(stats(dir)["files"].as_u64().unwrap() > 0);
}

#[test]
fn init_respects_an_existing_customized_config() {
    let tmp = fixture_repo();
    let dir = tmp.path();
    std::fs::create_dir_all(dir.join("src")).unwrap();
    std::fs::write(
        dir.join("src").join("kept.ts"),
        "export function keptSymbol(): void {}\n",
    )
    .unwrap();

    // Initialize without indexing, narrow the config to one root, then re-init.
    // The index must honor the config on disk, not the defaults `init` loads
    // before that file exists.
    run(dir, &["init", "--no-index"]);
    let cfg_path = dir.join(".memlay").join("config.toml");
    let cfg = std::fs::read_to_string(&cfg_path).unwrap();
    assert!(cfg.contains("[index]"), "unexpected config shape: {cfg}");
    std::fs::write(
        &cfg_path,
        cfg.replace("roots = [\".\"]", "roots = [\"src\"]"),
    )
    .unwrap();

    run(dir, &["init"]);
    let s = stats(dir);
    assert_eq!(
        s["files"].as_u64().unwrap(),
        1,
        "init ignored the narrowed roots and indexed the whole tree: {s}"
    );
}