#![cfg(feature = "cli")]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
fn binary() -> &'static str {
env!("CARGO_BIN_EXE_codenexus")
}
fn run(args: &[&str]) -> (i32, String, String) {
let output = Command::new(binary())
.args(args)
.output()
.unwrap_or_else(|e| panic!("failed to spawn codenexus: {e}"));
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
fn run_with_env(args: &[&str], env: &[(&str, &str)]) -> (i32, String, String) {
let mut cmd = Command::new(binary());
cmd.args(args);
for (k, v) in env {
cmd.env(k, v);
}
let output = cmd
.output()
.unwrap_or_else(|e| panic!("failed to spawn codenexus: {e}"));
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
fn run_with_stdin(args: &[&str], stdin: &[u8]) -> (i32, String, String) {
let output = Command::new(binary())
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(stdin)?;
child.wait_with_output()
})
.unwrap_or_else(|e| panic!("failed to spawn codenexus: {e}"));
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
fn write_file(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, content).unwrap();
}
fn build_rust_repo(dir: &Path) {
write_file(
dir,
"src/main.rs",
"fn main() {\n helper();\n}\n\nfn helper() {\n println!(\"hello\");\n}\n",
);
}
fn index_args_with(repo: &Path, db: &Path, force: bool) -> Vec<String> {
vec![
"index".to_string(),
"--path".to_string(),
repo.to_string_lossy().to_string(),
"--name".to_string(),
"demo".to_string(),
format!("--force={force}"),
"--lsp=false".to_string(),
"--embed=false".to_string(),
"--ram_first=false".to_string(),
"--db".to_string(),
db.to_string_lossy().to_string(),
]
}
fn index_args(repo: &Path, db: &Path) -> Vec<String> {
index_args_with(repo, db, false)
}
fn index_fresh_repo() -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
fs::create_dir_all(&repo).unwrap();
build_rust_repo(&repo);
let db = tmp.path().join("test.lbug");
let args = index_args(&repo, &db);
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let (code, stdout, stderr) = run(&arg_refs);
assert_eq!(
code, 0,
"index should exit 0, stderr: {stderr}, stdout: {stdout}"
);
(tmp, db)
}
#[test]
fn index_builds_rust_repo_succeeds() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
fs::create_dir_all(&repo).unwrap();
build_rust_repo(&repo);
let db = tmp.path().join("test.lbug");
let args = index_args(&repo, &db);
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let (code, stdout, stderr) = run(&arg_refs);
assert_eq!(code, 0, "index should exit 0, stderr: {stderr}");
assert!(
stdout.contains("\"files_indexed\"") && stdout.contains("\"nodes_created\""),
"stdout should contain index stats JSON, got: {stdout}"
);
}
#[test]
fn index_nonexistent_path_fails() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("test.lbug");
let args = index_args_with(
Path::new("/nonexistent/path/does_not_exist_xyz"),
&db,
false,
);
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let (code, _stdout, _stderr) = run(&arg_refs);
assert_ne!(code, 0, "nonexistent path should exit non-zero, got {code}");
}
#[test]
fn index_force_rebuild_exits_0() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let args = index_args_with(&repo, &db, true);
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let (code, stdout, stderr) = run(&arg_refs);
assert_eq!(code, 0, "force rebuild should exit 0, stderr: {stderr}");
assert!(
stdout.contains("\"files_indexed\""),
"should produce index stats: {stdout}"
);
}
#[test]
fn index_incremental_skips_unchanged() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let args = index_args_with(&repo, &db, false);
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let (code, stdout, stderr) = run(&arg_refs);
assert_eq!(code, 0, "incremental index should exit 0, stderr: {stderr}");
assert!(
stdout.contains("\"files_skipped\""),
"should report skipped files: {stdout}"
);
}
#[cfg(unix)]
#[cfg(feature = "daemon")]
mod daemon_hot_update {
use super::*;
fn spawn_daemon(repo: &Path, db: &Path, debounce_ms: u64) -> std::process::Child {
let mut cmd = Command::new(binary());
cmd.args([
"daemon",
"--path",
repo.to_str().unwrap(),
"--name",
"demo",
"--db",
db.to_str().unwrap(),
"--debounce-ms",
&debounce_ms.to_string(),
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd.spawn().unwrap_or_else(|e| panic!("spawn daemon: {e}"))
}
fn send_sigterm(child: &std::process::Child) {
let pid = child.id();
let exit = Command::new("/bin/kill")
.args(["-TERM", &pid.to_string()])
.status()
.expect("/bin/kill -TERM");
assert!(exit.success(), "kill -TERM should succeed for pid {pid}");
}
fn wait_for_exit(child: &mut std::process::Child, timeout: Duration) -> i32 {
let deadline = std::time::Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => return status.code().unwrap_or(-1),
Ok(None) => {
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!("daemon did not exit within {timeout:?}");
}
thread::sleep(Duration::from_millis(100));
}
Err(e) => panic!("wait failed: {e}"),
}
}
}
fn query_names(db: &Path) -> String {
let args: &[&str] = &[
"query",
"--cypher",
"MATCH (n) RETURN n.name AS name",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "query should exit 0, stderr: {stderr}");
stdout
}
fn query_function_names(db: &Path) -> String {
let args: &[&str] = &[
"query",
"--cypher",
"MATCH (n:Function) RETURN n.name AS name",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, _stderr) = run(args);
assert_eq!(code, 0, "function query should exit 0");
stdout
}
#[test]
fn daemon_hot_update_detects_new_file() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let before = query_function_names(&db);
assert!(
before.contains("helper"),
"initial index should contain 'helper': {before}"
);
assert!(
!before.contains("new_func"),
"new_func should NOT exist before daemon hot update: {before}"
);
let mut daemon = spawn_daemon(&repo, &db, 300);
thread::sleep(Duration::from_millis(1000));
write_file(
&repo,
"src/new_func.rs",
"fn new_func() {\n helper();\n}\n",
);
thread::sleep(Duration::from_millis(4000));
send_sigterm(&daemon);
let exit = wait_for_exit(&mut daemon, Duration::from_secs(3));
assert_eq!(exit, 0, "daemon should exit 0 after SIGTERM, got {exit}");
let after = query_function_names(&db);
assert!(
after.contains("new_func"),
"new_func should appear after daemon hot update: {after}"
);
assert!(
after.contains("helper"),
"helper should still be present: {after}"
);
}
#[test]
fn daemon_hot_update_detects_modified_file() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let before = query_function_names(&db);
assert!(
before.contains("helper"),
"initial: helper present: {before}"
);
assert!(
!before.contains("extra_fn"),
"initial: extra_fn absent: {before}"
);
let mut daemon = spawn_daemon(&repo, &db, 300);
thread::sleep(Duration::from_millis(1000));
write_file(
&repo,
"src/main.rs",
"fn main() {\n helper();\n}\n\nfn helper() {\n println!(\"hello\");\n}\n\nfn extra_fn() {\n helper();\n}\n",
);
thread::sleep(Duration::from_millis(4000));
send_sigterm(&daemon);
let exit = wait_for_exit(&mut daemon, Duration::from_secs(3));
assert_eq!(exit, 0, "daemon should exit 0 after SIGTERM, got {exit}");
let after = query_function_names(&db);
assert!(
after.contains("extra_fn"),
"extra_fn should appear after modify: {after}"
);
}
#[test]
fn daemon_hot_update_ignores_non_code_file() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let before = query_function_names(&db);
let mut daemon = spawn_daemon(&repo, &db, 300);
thread::sleep(Duration::from_millis(600));
write_file(&repo, "notes.txt", "hello world\n");
write_file(&repo, "README.md", "# readme\n");
thread::sleep(Duration::from_millis(1500));
send_sigterm(&daemon);
let exit = wait_for_exit(&mut daemon, Duration::from_secs(3));
assert_eq!(exit, 0, "daemon should exit 0 after SIGTERM, got {exit}");
let after = query_function_names(&db);
assert!(
after.contains("main") && after.contains("helper"),
"functions should still be present after non-code file add: {after}"
);
assert_eq!(
before.matches("helper").count(),
after.matches("helper").count(),
"helper occurrence count should not change"
);
let all_names = query_names(&db);
assert!(
!all_names.contains("notes.txt"),
"notes.txt should not be indexed: {all_names}"
);
assert!(
!all_names.contains("README.md"),
"README.md should not be indexed: {all_names}"
);
}
#[test]
fn daemon_nonexistent_path_exits_2() {
let (tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"daemon",
"--path",
"/nonexistent/path/xyz_abc",
"--name",
"demo",
"--db",
db.to_str().unwrap(),
];
let (code, _stdout, _stderr) = run(args);
assert_eq!(
code, 2,
"daemon with nonexistent path should exit 2 (InvalidInput), got {code}"
);
let _tmp = tmp;
}
}
mod post_index {
use super::*;
#[test]
fn list_shows_indexed_project() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &["list", "--db", db.to_str().unwrap()];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "list should exit 0, stderr: {stderr}");
assert!(
stdout.contains("\"demo\""),
"list should show 'demo' project: {stdout}"
);
}
#[test]
fn status_shows_project() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &["status", "--db", db.to_str().unwrap()];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "status should exit 0, stderr: {stderr}");
assert!(
stdout.contains("demo") || stdout.contains("project"),
"status should mention project: {stdout}"
);
}
#[test]
fn query_returns_node_names() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"query",
"--cypher",
"MATCH (n:Function) RETURN n.name AS name",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "query should exit 0, stderr: {stderr}");
assert!(
stdout.contains("helper"),
"query should return 'helper': {stdout}"
);
}
#[test]
fn query_invalid_cypher_exits_2() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"query",
"--cypher",
"THIS IS NOT CYPHER @@@",
"--db",
db.to_str().unwrap(),
];
let (code, _stdout, _stderr) = run(args);
assert_eq!(code, 2, "invalid Cypher should exit 2 (InvalidInput)");
}
#[test]
fn search_exact_finds_symbol() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"search",
"--text=helper",
"--fulltext=false",
"--limit=10",
"--mode=exact",
"--project=demo",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "search should exit 0, stderr: {stderr}");
assert!(
stdout.contains("\"helper\""),
"search should find 'helper': {stdout}"
);
assert!(
stdout.contains("\"results\""),
"search should return results array: {stdout}"
);
}
#[test]
fn context_returns_symbol_view() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"context",
"--symbol=helper",
"--depth=1",
"--project=demo",
"--enhanced=false",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "context should exit 0, stderr: {stderr}");
assert!(
stdout.contains("\"symbol\"") && stdout.contains("helper"),
"context should return symbol info: {stdout}"
);
assert!(
stdout.contains("\"incoming\""),
"context should show incoming edges: {stdout}"
);
}
#[test]
fn context_nonexistent_symbol_exits_2() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"context",
"--symbol=does_not_exist_xyz",
"--depth=1",
"--project=demo",
"--enhanced=false",
"--db",
db.to_str().unwrap(),
];
let (code, _stdout, _stderr) = run(args);
assert_eq!(code, 2, "nonexistent symbol should exit 2");
}
#[test]
fn impact_returns_blast_radius() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"impact",
"--symbol=helper",
"--depth=3",
"--edge_types=CALLS",
"--max_depth=3",
"--include_tests=false",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "impact should exit 0, stderr: {stderr}");
assert!(
stdout.contains("\"node_count\"") && stdout.contains("\"affected\""),
"impact should return blast radius: {stdout}"
);
}
#[test]
fn trace_returns_call_paths() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"trace",
"--symbol=main",
"--trace_type=calls",
"--depth=3",
"--path_filter=",
"--detect_cycles=false",
"--cross_service=false",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "trace should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"trace should produce output: stderr={stderr}"
);
}
#[test]
fn route_map_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &["route_map", "--project=demo", "--db", db.to_str().unwrap()];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "route_map should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"route_map should produce output: {stdout}"
);
}
#[test]
fn dead_code_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"dead_code",
"--project=demo",
"--entry=main",
"--check_exported=false",
"--check_ffi=false",
"--edge_types=CALLS",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "dead_code should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"dead_code should produce output: {stdout}"
);
}
#[test]
fn architecture_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"architecture",
"--project=demo",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "architecture should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"architecture should produce output: {stdout}"
);
}
#[test]
fn complexity_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &["complexity", "--project=demo", "--db", db.to_str().unwrap()];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "complexity should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"complexity should produce output: {stdout}"
);
}
#[test]
fn cross_service_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"cross_service",
"--project=demo",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "cross_service should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"cross_service should produce output: {stdout}"
);
}
#[test]
fn detect_changes_on_non_git_repo_exits_gracefully() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let args: &[&str] = &[
"detect_changes",
"--path",
repo.to_str().unwrap(),
"--mode=staged",
"--db",
db.to_str().unwrap(),
];
let (code, _stdout, _stderr) = run(args);
assert!(
code == 0 || code == 2,
"detect_changes on non-git should exit 0 or 2, got {code}"
);
}
#[test]
fn rename_dry_run_exits_0() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let args: &[&str] = &[
"rename",
"--from=helper",
"--to=helper_renamed",
"--path",
repo.to_str().unwrap(),
"--apply=false",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "rename dry-run should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"rename should produce output: {stdout}"
);
}
#[test]
fn api_impact_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &["api_impact", "--project=demo", "--db", db.to_str().unwrap()];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "api_impact should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"api_impact should produce output: {stdout}"
);
}
#[test]
fn shape_check_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &[
"shape_check",
"--project=demo",
"--db",
db.to_str().unwrap(),
];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "shape_check should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"shape_check should produce output: {stdout}"
);
}
#[test]
fn community_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &["community", "--project=demo", "--db", db.to_str().unwrap()];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "community should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"community should produce output: {stdout}"
);
}
#[test]
fn tool_map_exits_0() {
let (_tmp, db) = index_fresh_repo();
let args: &[&str] = &["tool_map", "--project=demo", "--db", db.to_str().unwrap()];
let (code, stdout, stderr) = run(args);
assert_eq!(code, 0, "tool_map should exit 0, stderr: {stderr}");
assert!(
!stdout.trim().is_empty(),
"tool_map should produce output: {stdout}"
);
}
#[test]
fn export_creates_artifact() {
let (tmp, db) = index_fresh_repo();
let output = tmp.path().join("export.tar.zst");
let args: &[&str] = &[
"export",
"--output",
output.to_str().unwrap(),
"--project=demo",
"--db",
db.to_str().unwrap(),
];
let (code, _stdout, stderr) = run(args);
assert_eq!(code, 0, "export should exit 0, stderr: {stderr}");
assert!(
output.exists(),
"export artifact should exist at {output:?}"
);
assert!(
fs::metadata(&output).map(|m| m.len()).unwrap_or(0) > 0,
"export artifact should be non-empty"
);
}
#[test]
fn clean_removes_project() {
let (_tmp, db) = index_fresh_repo();
let args_before: &[&str] = &["list", "--db", db.to_str().unwrap()];
let (code, stdout, _) = run(args_before);
assert_eq!(code, 0);
assert!(stdout.contains("demo"), "project should exist before clean");
let args: &[&str] = &["clean", "--project=demo", "--db", db.to_str().unwrap()];
let (code, _stdout, stderr) = run(args);
assert_eq!(code, 0, "clean should exit 0, stderr: {stderr}");
let args_after: &[&str] = &["list", "--db", db.to_str().unwrap()];
let (code, stdout, _) = run(args_after);
assert_eq!(code, 0);
assert!(
!stdout.contains("demo"),
"project should be gone after clean: {stdout}"
);
}
#[test]
fn hook_reads_stdin_and_responds() {
let tmp = TempDir::new().unwrap();
let db = tmp.path().join("test.lbug");
let payload = b"{}";
let args: &[&str] = &["hook", "--db", db.to_str().unwrap()];
let (code, _stdout, _stderr) = run_with_stdin(args, payload);
assert!(
code == 0 || code == 2,
"hook should exit 0 or 2, got {code}"
);
}
#[test]
fn setup_with_force_false_exits_gracefully() {
let tmp = TempDir::new().unwrap();
let home = tmp.path().join("home");
fs::create_dir_all(&home).unwrap();
let db = tmp.path().join("test.lbug");
let args: &[&str] = &["setup", "--force=false", "--db", db.to_str().unwrap()];
let (code, _stdout, _stderr) = run_with_env(args, &[("HOME", home.to_str().unwrap())]);
assert!(
code == 0 || code == 2,
"setup should exit 0 or 2, got {code}"
);
}
#[test]
fn lsp_goto_def_without_server_exits_gracefully() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let file = repo.join("src/main.rs");
let args: &[&str] = &[
"lsp_goto_def",
"--file",
file.to_str().unwrap(),
"--line=1",
"--col=1",
"--workspace",
repo.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
];
let (code, _stdout, _stderr) = run(args);
assert!(
code == 1 || code == 2,
"lsp_goto_def without server should exit 1 or 2, got {code}"
);
}
#[test]
fn lsp_hover_without_server_exits_gracefully() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let file = repo.join("src/main.rs");
let args: &[&str] = &[
"lsp_hover",
"--file",
file.to_str().unwrap(),
"--line=1",
"--col=1",
"--workspace",
repo.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
];
let (code, _stdout, _stderr) = run(args);
assert!(
code == 1 || code == 2,
"lsp_hover without server should exit 1 or 2, got {code}"
);
}
#[test]
fn export_then_import_roundtrip() {
let (tmp, db) = index_fresh_repo();
let repo = tmp.path().join("repo");
let artifact = tmp.path().join("export.tar.zst");
let export_args: &[&str] = &[
"export",
"--output",
artifact.to_str().unwrap(),
"--project=demo",
"--db",
db.to_str().unwrap(),
];
let (code, _, stderr) = run(export_args);
assert_eq!(code, 0, "export should exit 0, stderr: {stderr}");
assert!(artifact.exists(), "artifact should exist");
let db2 = tmp.path().join("imported.lbug");
let import_args: &[&str] = &[
"import",
"--input",
artifact.to_str().unwrap(),
"--reindex=false",
"--path",
repo.to_str().unwrap(),
"--name=demo_imported",
"--db",
db2.to_str().unwrap(),
];
let (code, stdout, stderr) = run(import_args);
assert_eq!(
code, 0,
"import should exit 0, stderr: {stderr}, stdout: {stdout}"
);
}
}