mod helpers;
use std::fs;
use omnigraph::db::{InitOptions, Omnigraph, ReadTarget};
use omnigraph_compiler::schema::parser::parse_schema;
use omnigraph_compiler::{build_schema_ir, schema_ir_pretty_json};
use helpers::*;
#[tokio::test]
async fn init_creates_graph() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let db = Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
assert!(dir.path().join("_schema.pg").exists());
assert!(dir.path().join("_schema.ir.json").exists());
assert!(dir.path().join("__schema_state.json").exists());
let snap = snapshot_main(&db).await.unwrap();
assert!(snap.entry("node:Person").is_some());
assert!(snap.entry("node:Company").is_some());
assert!(snap.entry("edge:Knows").is_some());
assert!(snap.entry("edge:WorksAt").is_some());
assert_eq!(db.catalog().node_types.len(), 2);
assert_eq!(db.catalog().edge_types.len(), 2);
assert_eq!(
db.catalog().node_types["Person"].key_property(),
Some("name")
);
}
#[tokio::test]
async fn open_reads_existing_graph() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
let db = Omnigraph::open(uri).await.unwrap();
assert_eq!(db.catalog().node_types.len(), 2);
assert_eq!(db.catalog().edge_types.len(), 2);
let snap = snapshot_main(&db).await.unwrap();
assert!(snap.entry("node:Person").is_some());
assert!(snap.entry("edge:Knows").is_some());
}
#[tokio::test]
async fn open_bootstraps_legacy_schema_state_for_main_only_graph() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
fs::remove_file(dir.path().join("_schema.ir.json")).unwrap();
fs::remove_file(dir.path().join("__schema_state.json")).unwrap();
let db = Omnigraph::open(uri).await.unwrap();
assert_eq!(db.catalog().node_types.len(), 2);
assert!(dir.path().join("_schema.ir.json").exists());
assert!(dir.path().join("__schema_state.json").exists());
}
#[tokio::test]
async fn open_rejects_legacy_graph_with_public_branch() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let mut db = Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
db.branch_create("feature").await.unwrap();
fs::remove_file(dir.path().join("_schema.ir.json")).unwrap();
fs::remove_file(dir.path().join("__schema_state.json")).unwrap();
let err = match Omnigraph::open(uri).await {
Ok(_) => panic!("expected legacy graph with public branch to fail schema bootstrap"),
Err(err) => err,
};
assert!(
err.to_string()
.contains("public branches block schema evolution entirely")
);
}
#[tokio::test]
async fn long_lived_handle_rejects_schema_source_drift() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let db = Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
let drifted = TEST_SCHEMA.replace("age: I32?", "age: I64?");
fs::write(dir.path().join("_schema.pg"), drifted).unwrap();
let err = match db.snapshot_of(ReadTarget::branch("main")).await {
Ok(_) => panic!("expected schema source drift to be rejected"),
Err(err) => err,
};
assert!(
err.to_string()
.contains("current _schema.pg no longer matches the accepted compiled schema")
);
}
#[tokio::test]
async fn long_lived_handle_rejects_schema_ir_drift() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let db = Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
fs::write(dir.path().join("_schema.ir.json"), "{not valid json").unwrap();
let err = match db.snapshot_of(ReadTarget::branch("main")).await {
Ok(_) => panic!("expected schema IR drift to be rejected"),
Err(err) => err,
};
assert!(
err.to_string()
.contains("accepted compiled schema contract in _schema.ir.json is invalid")
);
}
#[tokio::test]
async fn long_lived_handle_rejects_ir_and_source_updates_without_state_update() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let db = Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
let drifted = TEST_SCHEMA.replace("age: I32?", "age: I64?");
let drifted_ast = parse_schema(&drifted).unwrap();
let drifted_ir = build_schema_ir(&drifted_ast).unwrap();
let drifted_ir_json = schema_ir_pretty_json(&drifted_ir).unwrap();
fs::write(dir.path().join("_schema.pg"), drifted).unwrap();
fs::write(dir.path().join("_schema.ir.json"), drifted_ir_json).unwrap();
let err = match db.snapshot_of(ReadTarget::branch("main")).await {
Ok(_) => panic!("expected schema state mismatch to be rejected"),
Err(err) => err,
};
assert!(
err.to_string()
.contains("accepted compiled schema does not match the recorded schema state")
);
}
#[tokio::test]
async fn comment_only_schema_edit_keeps_schema_state_valid() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let db = Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
let commented = format!("// comment-only drift\n{}", TEST_SCHEMA);
fs::write(dir.path().join("_schema.pg"), commented).unwrap();
let snapshot = db.snapshot_of(ReadTarget::branch("main")).await.unwrap();
assert!(snapshot.entry("node:Person").is_some());
}
#[tokio::test]
async fn open_nonexistent_fails() {
let result = Omnigraph::open("/tmp/nonexistent_omnigraph_test_xyz").await;
assert!(result.is_err());
}
#[tokio::test]
async fn snapshot_version_is_pinned() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let mut db = Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
let snap1 = snapshot_main(&db).await.unwrap();
let v1 = snap1.version();
omnigraph::loader::load_jsonl(
&mut db,
r#"{"type": "Person", "data": {"name": "Alice", "age": 30}}"#,
omnigraph::loader::LoadMode::Overwrite,
)
.await
.unwrap();
let snap2 = snapshot_main(&db).await.unwrap();
assert!(snap2.version() > v1);
assert_eq!(snap1.version(), v1);
}
#[tokio::test]
async fn init_on_existing_graph_uri_does_not_destroy_existing_schema() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
Omnigraph::init(uri, TEST_SCHEMA).await.unwrap();
let original_schema_pg = fs::read_to_string(dir.path().join("_schema.pg")).unwrap();
let original_schema_ir = fs::read_to_string(dir.path().join("_schema.ir.json")).unwrap();
let original_schema_state = fs::read_to_string(dir.path().join("__schema_state.json")).unwrap();
let different_schema = "node Other { id: String @key }\n";
let result = Omnigraph::init(uri, different_schema).await;
assert!(
result.is_err(),
"init against an existing graph URI must error, not silently overwrite"
);
assert!(
dir.path().join("_schema.pg").exists(),
"_schema.pg must not be deleted by a failed re-init"
);
assert!(
dir.path().join("_schema.ir.json").exists(),
"_schema.ir.json must not be deleted by a failed re-init"
);
assert!(
dir.path().join("__schema_state.json").exists(),
"__schema_state.json must not be deleted by a failed re-init"
);
assert_eq!(
fs::read_to_string(dir.path().join("_schema.pg")).unwrap(),
original_schema_pg,
"_schema.pg contents must be preserved when re-init is rejected"
);
assert_eq!(
fs::read_to_string(dir.path().join("_schema.ir.json")).unwrap(),
original_schema_ir,
"_schema.ir.json contents must be preserved when re-init is rejected"
);
assert_eq!(
fs::read_to_string(dir.path().join("__schema_state.json")).unwrap(),
original_schema_state,
"__schema_state.json contents must be preserved when re-init is rejected"
);
}
#[tokio::test]
async fn init_with_force_recovers_from_orphan_schema_files() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
fs::write(dir.path().join("_schema.pg"), TEST_SCHEMA).unwrap();
let strict_err = match Omnigraph::init(uri, TEST_SCHEMA).await {
Ok(_) => panic!("strict init must refuse when orphan _schema.pg exists"),
Err(e) => e,
};
assert!(
strict_err.to_string().contains("already initialized"),
"strict init must surface AlreadyInitialized (sanity check); got: {strict_err}"
);
let db = Omnigraph::init_with_options(uri, TEST_SCHEMA, InitOptions { force: true })
.await
.expect("force init must succeed when only orphan schema files block strict init");
assert!(
db.catalog().node_types.contains_key("Person"),
"force-recovered graph must have the new catalog installed"
);
assert!(
dir.path().join("__schema_state.json").exists(),
"force-recovered graph must have full schema state written"
);
}
#[tokio::test]
async fn schema_annotations_persist_into_ir_json_on_init() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let schema = r#"
node Task @description("Tracked work item") @instruction("Prefer querying by slug") {
slug: String @key @description("Stable external identifier")
}
edge DependsOn: Task -> Task @description("Hard dependency") @instruction("Use only for blockers")
"#;
Omnigraph::init(uri, schema).await.unwrap();
let ir_json = fs::read_to_string(dir.path().join("_schema.ir.json")).unwrap();
let ir: serde_json::Value = serde_json::from_str(&ir_json).unwrap();
let anns = |v: &serde_json::Value| -> std::collections::BTreeMap<String, String> {
v["annotations"]
.as_array()
.unwrap()
.iter()
.filter_map(|a| {
Some((
a["name"].as_str()?.to_string(),
a["value"].as_str()?.to_string(),
))
})
.collect()
};
let node = ir["nodes"]
.as_array()
.unwrap()
.iter()
.find(|n| n["name"] == "Task")
.unwrap();
let node_anns = anns(node);
assert_eq!(node_anns.get("description").map(String::as_str), Some("Tracked work item"));
assert_eq!(
node_anns.get("instruction").map(String::as_str),
Some("Prefer querying by slug"),
"node @instruction persists into _schema.ir.json"
);
let prop = node["properties"]
.as_array()
.unwrap()
.iter()
.find(|p| p["name"] == "slug")
.unwrap();
assert_eq!(
anns(prop).get("description").map(String::as_str),
Some("Stable external identifier"),
"property @description persists into _schema.ir.json"
);
let edge = ir["edges"]
.as_array()
.unwrap()
.iter()
.find(|e| e["name"] == "DependsOn")
.unwrap();
let edge_anns = anns(edge);
assert_eq!(edge_anns.get("description").map(String::as_str), Some("Hard dependency"));
assert_eq!(edge_anns.get("instruction").map(String::as_str), Some("Use only for blockers"));
}
#[tokio::test]
async fn init_rejects_instruction_on_property() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let schema = r#"
node Task {
slug: String @key @instruction("bad")
}
"#;
let err = match Omnigraph::init(uri, schema).await {
Ok(_) => panic!("property-level @instruction must abort init"),
Err(err) => err,
};
assert!(
err.to_string().contains("@instruction is only supported on node and edge types"),
"property-level @instruction must abort init: {err}"
);
assert!(
!dir.path().join("_schema.ir.json").exists(),
"rejected init must not persist a schema IR"
);
}