use assert_cmd::Command;
use predicates::prelude::*;
fn cmd() -> Command {
Command::cargo_bin("velesdb").expect("velesdb binary not found")
}
fn create_graph_collection(dir: &std::path::Path, name: &str) {
cmd()
.args(["collection", "create-graph", dir.to_str().unwrap(), name])
.assert()
.success();
}
fn store_payload(dir: &std::path::Path, name: &str, node_id: &str) {
cmd()
.args([
"graph",
"store-payload",
dir.to_str().unwrap(),
name,
node_id,
"{}",
])
.assert()
.success();
}
#[test]
fn test_graph_help_lists_all_subcommands() {
cmd()
.args(["graph", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("add-edge"))
.stdout(predicate::str::contains("get-edges"))
.stdout(predicate::str::contains("degree"))
.stdout(predicate::str::contains("traverse"))
.stdout(predicate::str::contains("neighbors"))
.stdout(predicate::str::contains("store-payload"))
.stdout(predicate::str::contains("get-payload"));
}
#[test]
fn test_graph_add_edge_success() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "kg");
store_payload(temp.path(), "kg", "100");
store_payload(temp.path(), "kg", "200");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"kg",
"1", "100", "200", "KNOWS", ])
.assert()
.success()
.stdout(predicate::str::contains("Edge 1 added"))
.stdout(predicate::str::contains("KNOWS"));
}
#[test]
fn test_graph_add_edge_collection_not_found() {
let temp = tempfile::tempdir().unwrap();
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"nonexistent",
"1",
"100",
"200",
"KNOWS",
])
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn test_graph_add_edge_empty_label_rejected() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "kg");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"kg",
"1",
"100",
"200",
" ", ])
.assert()
.failure()
.stderr(predicate::str::contains("label"));
}
#[test]
fn test_graph_get_edges_empty() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
cmd()
.args(["graph", "get-edges", temp.path().to_str().unwrap(), "g"])
.assert()
.success()
.stdout(predicate::str::contains("No edges found"));
}
#[test]
fn test_graph_get_edges_after_add() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
for node_id in ["10", "20", "30"] {
store_payload(temp.path(), "g", node_id);
}
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"10",
"20",
"LIKES",
])
.assert()
.success();
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"2",
"20",
"30",
"FOLLOWS",
])
.assert()
.success();
cmd()
.args(["graph", "get-edges", temp.path().to_str().unwrap(), "g"])
.assert()
.success()
.stdout(predicate::str::contains("LIKES"))
.stdout(predicate::str::contains("FOLLOWS"))
.stdout(predicate::str::contains("Total: 2 edge(s)"));
}
#[test]
fn test_graph_get_edges_filter_by_label() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
for node_id in ["10", "20", "30"] {
store_payload(temp.path(), "g", node_id);
}
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"10",
"20",
"LIKES",
])
.assert()
.success();
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"2",
"20",
"30",
"FOLLOWS",
])
.assert()
.success();
cmd()
.args([
"graph",
"get-edges",
temp.path().to_str().unwrap(),
"g",
"--label",
"LIKES",
])
.assert()
.success()
.stdout(predicate::str::contains("LIKES"))
.stdout(predicate::str::contains("Total: 1 edge(s)"));
}
#[test]
fn test_graph_get_edges_json_format() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "10");
store_payload(temp.path(), "g", "20");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"10",
"20",
"REL",
])
.assert()
.success();
let output = cmd()
.args([
"graph",
"get-edges",
temp.path().to_str().unwrap(),
"g",
"--format",
"json",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
let arr = parsed.as_array().expect("JSON array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["id"], 1);
assert_eq!(arr[0]["source"], 10);
assert_eq!(arr[0]["target"], 20);
assert_eq!(arr[0]["label"], "REL");
}
#[test]
fn test_graph_degree_zero_for_isolated_node() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
let output = cmd()
.args([
"graph",
"degree",
temp.path().to_str().unwrap(),
"g",
"999",
"--format",
"json",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(parsed["in_degree"], 0);
assert_eq!(parsed["out_degree"], 0);
assert_eq!(parsed["total_degree"], 0);
}
#[test]
fn test_graph_degree_after_edges() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
for node_id in ["10", "20", "30"] {
store_payload(temp.path(), "g", node_id);
}
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"10",
"20",
"A",
])
.assert()
.success();
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"2",
"10",
"30",
"B",
])
.assert()
.success();
let output = cmd()
.args([
"graph",
"degree",
temp.path().to_str().unwrap(),
"g",
"10",
"--format",
"json",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(parsed["out_degree"], 2);
assert_eq!(parsed["in_degree"], 0);
assert_eq!(parsed["total_degree"], 2);
}
#[test]
fn test_graph_traverse_bfs_empty() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
cmd()
.args(["graph", "traverse", temp.path().to_str().unwrap(), "g", "1"])
.assert()
.success()
.stdout(predicate::str::contains("No results found"));
}
#[test]
fn test_graph_traverse_bfs_finds_reachable_nodes() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
for node_id in ["1", "2", "3"] {
store_payload(temp.path(), "g", node_id);
}
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"10",
"1",
"2",
"NEXT",
])
.assert()
.success();
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"11",
"2",
"3",
"NEXT",
])
.assert()
.success();
cmd()
.args([
"graph",
"traverse",
temp.path().to_str().unwrap(),
"g",
"1",
"--algorithm",
"bfs",
"--max-depth",
"5",
])
.assert()
.success()
.stdout(predicate::str::contains("Total: 2 result(s)"));
}
#[test]
fn test_graph_traverse_dfs() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "1");
store_payload(temp.path(), "g", "2");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"10",
"1",
"2",
"X",
])
.assert()
.success();
cmd()
.args([
"graph",
"traverse",
temp.path().to_str().unwrap(),
"g",
"1",
"--algorithm",
"dfs",
])
.assert()
.success()
.stdout(predicate::str::contains("DFS"))
.stdout(predicate::str::contains("Total: 1 result(s)"));
}
#[test]
fn test_graph_traverse_json_format() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "1");
store_payload(temp.path(), "g", "2");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"10",
"1",
"2",
"REL",
])
.assert()
.success();
let output = cmd()
.args([
"graph",
"traverse",
temp.path().to_str().unwrap(),
"g",
"1",
"--format",
"json",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
let arr = parsed.as_array().expect("JSON array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["target_id"], 2);
assert_eq!(arr[0]["depth"], 1);
}
#[test]
fn test_graph_traverse_with_rel_types() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
for node_id in ["1", "2", "3"] {
store_payload(temp.path(), "g", node_id);
}
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"10",
"1",
"2",
"KNOWS",
])
.assert()
.success();
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"11",
"1",
"3",
"FOLLOWS",
])
.assert()
.success();
cmd()
.args([
"graph",
"traverse",
temp.path().to_str().unwrap(),
"g",
"1",
"--rel-types",
"KNOWS",
])
.assert()
.success()
.stdout(predicate::str::contains("Total: 1 result(s)"));
}
#[test]
fn test_graph_neighbors_outgoing() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "10");
store_payload(temp.path(), "g", "20");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"10",
"20",
"A",
])
.assert()
.success();
cmd()
.args([
"graph",
"neighbors",
temp.path().to_str().unwrap(),
"g",
"10",
"--direction",
"out",
])
.assert()
.success()
.stdout(predicate::str::contains("Total: 1 edge(s)"));
}
#[test]
fn test_graph_neighbors_incoming() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "10");
store_payload(temp.path(), "g", "20");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"10",
"20",
"A",
])
.assert()
.success();
cmd()
.args([
"graph",
"neighbors",
temp.path().to_str().unwrap(),
"g",
"20",
"--direction",
"in",
])
.assert()
.success()
.stdout(predicate::str::contains("Total: 1 edge(s)"));
}
#[test]
fn test_graph_neighbors_both() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
for node_id in ["10", "20", "30"] {
store_payload(temp.path(), "g", node_id);
}
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"10",
"20",
"A",
])
.assert()
.success();
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"2",
"30",
"20",
"B",
])
.assert()
.success();
cmd()
.args([
"graph",
"neighbors",
temp.path().to_str().unwrap(),
"g",
"20",
"--direction",
"both",
])
.assert()
.success()
.stdout(predicate::str::contains("Total: 2 edge(s)"));
}
#[test]
fn test_graph_store_and_get_payload() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
cmd()
.args([
"graph",
"store-payload",
temp.path().to_str().unwrap(),
"g",
"42",
r#"{"name":"Alice","age":30}"#,
])
.assert()
.success()
.stdout(predicate::str::contains("Payload stored on node 42"));
let output = cmd()
.args([
"graph",
"get-payload",
temp.path().to_str().unwrap(),
"g",
"42",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert_eq!(parsed["name"], "Alice");
assert_eq!(parsed["age"], 30);
}
#[test]
fn test_graph_get_payload_returns_null_when_empty() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
cmd()
.args([
"graph",
"get-payload",
temp.path().to_str().unwrap(),
"g",
"999",
])
.assert()
.success()
.stdout(predicate::str::contains("null"));
}
#[test]
fn test_graph_store_payload_invalid_json() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
cmd()
.args([
"graph",
"store-payload",
temp.path().to_str().unwrap(),
"g",
"1",
"not-json",
])
.assert()
.failure()
.stderr(predicate::str::contains("Invalid JSON"));
}
const WAL_OP_ADD: u8 = 0x01;
fn seed_phantom_edge(
dir: &std::path::Path,
collection: &str,
id: u64,
source: u64,
target: u64,
label: &str,
) {
use std::io::Write;
let edge = serde_json::json!({
"id": id, "source": source, "target": target,
"label": label, "properties": {}
});
let edge_bytes = serde_json::to_vec(&edge).unwrap();
let body_len = u32::try_from(edge_bytes.len() + 1).unwrap();
let wal_path = dir.join(collection).join("edges.wal");
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&wal_path)
.expect("open edges.wal for append");
f.write_all(&body_len.to_le_bytes()).unwrap();
f.write_all(&[WAL_OP_ADD]).unwrap();
f.write_all(&edge_bytes).unwrap();
f.sync_all().unwrap();
}
#[test]
fn test_graph_help_lists_doctor_subcommand() {
cmd()
.args(["graph", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("doctor"));
}
#[test]
fn test_graph_doctor_purge_and_stub_are_mutually_exclusive() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
cmd()
.args([
"graph",
"doctor",
temp.path().to_str().unwrap(),
"g",
"--purge",
"--stub",
])
.assert()
.failure()
.stderr(predicate::str::contains("cannot be used with"));
}
#[test]
fn test_graph_doctor_clean_graph_reports_no_phantoms() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "1");
store_payload(temp.path(), "g", "2");
cmd()
.args([
"graph",
"add-edge",
temp.path().to_str().unwrap(),
"g",
"1",
"1",
"2",
"KNOWS",
])
.assert()
.success();
cmd()
.args(["graph", "doctor", temp.path().to_str().unwrap(), "g"])
.assert()
.success()
.stdout(predicate::str::contains("No phantom edges found"));
}
#[test]
fn test_graph_doctor_dry_run_reports_phantom_without_mutating() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "1");
seed_phantom_edge(temp.path(), "g", 99, 1, 2, "LEGACY");
cmd()
.args(["graph", "doctor", temp.path().to_str().unwrap(), "g"])
.assert()
.success()
.stdout(predicate::str::contains("1 phantom edge"))
.stdout(predicate::str::contains("Dry-run"));
cmd()
.args(["graph", "count", temp.path().to_str().unwrap(), "g"])
.assert()
.success()
.stdout(predicate::str::contains("Edges: 1"));
}
#[test]
fn test_graph_doctor_purge_removes_phantom_edge() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "1");
seed_phantom_edge(temp.path(), "g", 99, 1, 2, "LEGACY");
cmd()
.args([
"graph",
"doctor",
temp.path().to_str().unwrap(),
"g",
"--purge",
])
.assert()
.success()
.stdout(predicate::str::contains("1 phantom edge(s) removed"));
cmd()
.args(["graph", "count", temp.path().to_str().unwrap(), "g"])
.assert()
.success()
.stdout(predicate::str::contains("Edges: 0"));
}
#[test]
fn test_graph_doctor_stub_seeds_missing_endpoint_payload() {
let temp = tempfile::tempdir().unwrap();
create_graph_collection(temp.path(), "g");
store_payload(temp.path(), "g", "1");
seed_phantom_edge(temp.path(), "g", 99, 1, 2, "LEGACY");
cmd()
.args([
"graph",
"doctor",
temp.path().to_str().unwrap(),
"g",
"--stub",
])
.assert()
.success()
.stdout(predicate::str::contains("1 missing node(s) stubbed"));
cmd()
.args([
"graph",
"get-payload",
temp.path().to_str().unwrap(),
"g",
"2",
])
.assert()
.success()
.stdout(predicate::str::contains("{}"));
cmd()
.args(["graph", "count", temp.path().to_str().unwrap(), "g"])
.assert()
.success()
.stdout(predicate::str::contains("Edges: 1"));
}