use std::path::Path;
use std::process::Command;
use assert_cmd::prelude::*;
use tempfile::TempDir;
fn mnem(repo: &Path, args: &[&str]) -> Command {
let mut cmd = Command::cargo_bin("mnem").expect("mnem binary built");
cmd.current_dir(repo).arg("-R").arg(repo);
cmd.env("HOME", repo);
cmd.env("USERPROFILE", repo);
cmd.env("MNEM_DISABLE_GLOBAL_CONFIG", "1");
cmd.env_remove("MNEM_EMBED_PROVIDER");
cmd.env_remove("MNEM_EMBED_MODEL");
cmd.env_remove("MNEM_EMBED_API_KEY_ENV");
cmd.env_remove("MNEM_EMBED_BASE_URL");
for a in args {
cmd.arg(a);
}
cmd
}
fn parse_count(stdout: &str, label: &str) -> usize {
let suffix = format!(" {label}");
stdout
.split(',')
.find_map(|seg| {
let seg = seg.trim();
let num_str = seg.strip_suffix(&suffix)?;
let num_str = num_str
.strip_prefix("ingested ")
.unwrap_or(num_str)
.trim();
num_str.parse::<usize>().ok()
})
.unwrap_or_else(|| panic!("could not find '{label}' count in: {stdout}"))
}
#[test]
fn ingest_rust_file_succeeds() {
let td = TempDir::new().expect("tmp");
let repo = td.path();
mnem(repo, &["init"])
.ok()
.expect("mnem init should succeed");
let file = repo.join("lib.rs");
std::fs::write(
&file,
"pub fn add(a: i32, b: i32) -> i32 { a + b }\n\
pub fn subtract(a: i32, b: i32) -> i32 { a - b }\n",
)
.unwrap();
let out = mnem(repo, &["ingest", file.to_str().unwrap()])
.output()
.expect("spawn");
assert!(
out.status.success(),
"mnem ingest on a .rs file should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
stdout.contains("ingested"),
"stdout should contain 'ingested'; got: {stdout}"
);
let chunk_count = parse_count(&stdout, "chunks");
assert!(
chunk_count >= 2,
"Rust file with 2 top-level functions must produce >= 2 structural chunks; got {chunk_count}"
);
}
#[test]
fn ingest_python_file_succeeds() {
let td = TempDir::new().expect("tmp");
let repo = td.path();
mnem(repo, &["init"])
.ok()
.expect("mnem init should succeed");
let file = repo.join("util.py");
std::fs::write(
&file,
"def hello():\n return 'world'\n\ndef add(a, b):\n return a + b\n",
)
.unwrap();
let out = mnem(repo, &["ingest", file.to_str().unwrap()])
.output()
.expect("spawn");
assert!(
out.status.success(),
"mnem ingest on a .py file should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
stdout.contains("ingested"),
"stdout should contain 'ingested'; got: {stdout}"
);
let chunk_count = parse_count(&stdout, "chunks");
assert!(
chunk_count >= 2,
"Python file with 2 top-level functions must produce >= 2 structural chunks; got {chunk_count}"
);
}
#[test]
fn ingest_recursive_directory_with_mixed_extensions() {
let td = TempDir::new().expect("tmp");
let repo = td.path();
mnem(repo, &["init"])
.ok()
.expect("mnem init should succeed");
let src = repo.join("src");
std::fs::create_dir(&src).unwrap();
std::fs::write(
src.join("lib.rs"),
"pub fn add(a: i32, b: i32) -> i32 { a + b }\n\
pub fn sub(a: i32, b: i32) -> i32 { a - b }\n",
)
.unwrap();
std::fs::write(
src.join("util.py"),
"def greet(name):\n return f\"hello {name}\"\ndef farewell(name):\n return f\"bye {name}\"\n",
)
.unwrap();
std::fs::write(
src.join("notes.md"),
"# Notes\n\nSome project notes here.\n",
)
.unwrap();
let out = mnem(repo, &["ingest", "--recursive", src.to_str().unwrap()])
.output()
.expect("spawn");
assert!(
out.status.success(),
"mnem ingest --recursive on mixed src/ dir should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
stdout.contains("ingested"),
"stdout should contain 'ingested'; got: {stdout}"
);
assert!(
stdout.contains("ingested 3 files"),
"expected 'ingested 3 files' in stdout, got: {stdout}"
);
let chunk_count = parse_count(&stdout, "chunks");
assert!(
chunk_count >= 5,
"mixed recursive ingest of 2+2 code functions + 1 md file must produce >= 5 chunks; \
got {chunk_count}"
);
}
#[test]
fn ingest_unsupported_extension_falls_back_to_text() {
let td = TempDir::new().expect("tmp");
let repo = td.path();
mnem(repo, &["init"])
.ok()
.expect("mnem init should succeed");
let file = repo.join("data.xyz");
std::fs::write(&file, "hello world\n").unwrap();
let out = mnem(repo, &["ingest", file.to_str().unwrap()])
.output()
.expect("spawn");
assert!(
out.status.success(),
"mnem ingest on unknown extension should succeed (Text fallback); stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
stdout.contains("ingested"),
"stdout should contain 'ingested'; got: {stdout}"
);
let chunk_count = parse_count(&stdout, "chunks");
assert_eq!(
chunk_count,
1,
"text-fallback for unknown extension must produce exactly 1 chunk for a single-line file; got {chunk_count}"
);
}
#[test]
fn ingest_comment_only_code_file_produces_one_chunk() {
let td = TempDir::new().expect("tmp");
let repo = td.path();
mnem(repo, &["init"])
.ok()
.expect("mnem init should succeed");
let file = repo.join("comment_only.rs");
std::fs::write(&file, "use std::io;\n// no function definitions in this file\n").unwrap();
let out = mnem(repo, &["ingest", file.to_str().unwrap()])
.output()
.expect("spawn");
assert!(
out.status.success(),
"mnem ingest on a comment-only .rs file should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
stdout.contains("ingested"),
"stdout should contain 'ingested'; got: {stdout}"
);
let chunk_count = parse_count(&stdout, "chunks");
assert_eq!(
chunk_count, 1,
"a comment-only .rs file must produce exactly 1 chunk via the fallback section; \
got {chunk_count}"
);
let node_count = parse_count(&stdout, "nodes");
assert_eq!(
node_count,
2,
"comment-only .rs file must produce exactly 2 nodes (1 doc + 1 chunk node); got {node_count}"
);
}
#[test]
fn ingest_invalid_utf8_code_file_fails_with_error() {
let dir = TempDir::new().unwrap();
mnem(dir.path(), &["init"])
.assert()
.success();
let bad_rs = dir.path().join("bad.rs");
std::fs::write(&bad_rs, &[0xff, 0xfe, 0x80, 0x81, 0x82]).unwrap();
let out = mnem(dir.path(), &["ingest", "bad.rs"])
.assert()
.failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
!stderr.is_empty(),
"ingest of invalid-UTF-8 file must emit an error message"
);
assert!(
stderr.contains("bad.rs"),
"error message must mention the file that caused the failure; got: {stderr}"
);
}
#[test]
fn ingest_recursive_skips_unsupported_extensions() {
let dir = TempDir::new().unwrap();
mnem(dir.path(), &["init"])
.assert()
.success();
let src = dir.path().join("src");
std::fs::create_dir(&src).unwrap();
std::fs::write(src.join("lib.rs"), "pub fn foo() -> u32 { 42 }\npub fn bar() -> u32 { 1 }\n").unwrap();
std::fs::write(src.join("notes.txt"), "project notes\n").unwrap();
std::fs::write(src.join("config.unknown_ext_xyz"), "ignored content\n").unwrap();
let out = mnem(dir.path(), &["ingest", "--recursive", src.to_str().unwrap()])
.assert()
.success();
let stdout = String::from_utf8_lossy(&out.get_output().stdout).to_string();
assert!(
stdout.contains("ingested 2 files"),
"recursive walk must skip unsupported extensions; expected 'ingested 2 files', got: {stdout}"
);
let chunk_count = parse_count(&stdout, "chunks");
assert!(
chunk_count >= 3,
"recursive ingest of lib.rs (2 fns → 2 chunks) + notes.txt (1 → 1 chunk) must total >= 3; got {chunk_count}"
);
}
#[test]
fn ingest_recursive_no_supported_files_fails_with_error() {
let dir = TempDir::new().unwrap();
mnem(dir.path(), &["init", dir.path().to_str().unwrap()])
.assert()
.success();
let src = dir.path().join("only_unknown");
std::fs::create_dir(&src).unwrap();
std::fs::write(src.join("data.unknown_ext_xyz"), "some data\n").unwrap();
std::fs::write(src.join("more.another_unknown"), "more data\n").unwrap();
let out = mnem(dir.path(), &["ingest", "--recursive", src.to_str().unwrap()])
.assert()
.failure();
let stderr = String::from_utf8_lossy(&out.get_output().stderr).to_string();
assert!(
stderr.contains("no ingestable files found"),
"recursive walk with no supported files must fail with a clear error message; got: {stderr}"
);
}