mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-12 01:45:14 +02:00
~/.omnigraph/config.yaml joins the resolution chains as the operator surface: operator.actor becomes the last hop of THE actor chain (--as > legacy cli.actor during the RFC-008 window > operator.actor > none, one implementation for direct-engine and cluster commands alike) and defaults.output joins the read-format cascade below every more-specific source. Discovery honors $OMNIGRAPH_HOME (tilde-expanded, #139 finding 9); an absent file is an empty layer; unknown keys WARN and load (a file written for later slices must not break this CLI); malformed YAML is a loud error. The module is CLI-only — the server never reads operator config (invariant 11 by construction). $OMNIGRAPH_CONFIG becomes a first-class stand-in for --config in load_config (flag > env > ./omnigraph.yaml), one meaning in both binaries. The test harness pins hermeticity: spawned binaries get a nonexistent OMNIGRAPH_HOME by default so no test ever reads the developer's real operator config. New coverage: loader unit tests, the env-precedence matrix on load_config_in, and spawned-binary e2es for the actor chain (operator wins with no flag/legacy key; legacy outranks it; --as wins) and the format cascade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1677 lines
46 KiB
Rust
1677 lines
46 KiB
Rust
//! Data commands: load/read/change/branch/commit/export/snapshot/policy/embed/maintenance.
|
|
//! Moved verbatim from tests/cli.rs in the modularization.
|
|
|
|
use std::fs;
|
|
|
|
use serde_json::Value;
|
|
use tempfile::tempdir;
|
|
|
|
mod support;
|
|
|
|
use support::*;
|
|
|
|
|
|
#[test]
|
|
fn short_version_flag_prints_current_cli_version() {
|
|
let output = output_success(cli().arg("-v"));
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert_eq!(
|
|
stdout.trim(),
|
|
format!("omnigraph {}", env!("CARGO_PKG_VERSION"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn long_version_flag_prints_current_cli_version() {
|
|
let output = output_success(cli().arg("--version"));
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert_eq!(
|
|
stdout.trim(),
|
|
format!("omnigraph {}", env!("CARGO_PKG_VERSION"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn embed_seed_fills_missing_and_preserves_existing_vectors_by_default() {
|
|
let temp = tempdir().unwrap();
|
|
let seed = write_seed_fixture(temp.path());
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.env("OMNIGRAPH_EMBEDDINGS_MOCK", "1")
|
|
.arg("embed")
|
|
.arg("--seed")
|
|
.arg(&seed)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["mode"], "fill_missing");
|
|
assert_eq!(payload["embedded_rows"], 1);
|
|
assert_eq!(payload["selected_rows"], 2);
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
assert_eq!(
|
|
embedded[0]["data"]["embedding"].as_array().unwrap().len(),
|
|
4
|
|
);
|
|
assert_eq!(
|
|
embedded[1]["data"]["embedding"],
|
|
serde_json::json!([0.1, 0.2])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn embed_clean_removes_selected_embeddings() {
|
|
let temp = tempdir().unwrap();
|
|
let seed = write_seed_fixture(temp.path());
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("embed")
|
|
.arg("--seed")
|
|
.arg(&seed)
|
|
.arg("--clean")
|
|
.arg("--select")
|
|
.arg("Decision:slug=dec-beta")
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["mode"], "clean");
|
|
assert_eq!(payload["cleaned_rows"], 1);
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
assert!(embedded[0]["data"].get("embedding").is_none());
|
|
assert!(embedded[1]["data"].get("embedding").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn embed_select_reembeds_only_matching_rows() {
|
|
let temp = tempdir().unwrap();
|
|
let seed = write_seed_fixture(temp.path());
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.env("OMNIGRAPH_EMBEDDINGS_MOCK", "1")
|
|
.arg("embed")
|
|
.arg("--seed")
|
|
.arg(&seed)
|
|
.arg("--select")
|
|
.arg("Decision:slug=dec-beta")
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["mode"], "reembed_selected");
|
|
assert_eq!(payload["embedded_rows"], 1);
|
|
assert_eq!(payload["selected_rows"], 1);
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
assert!(embedded[0]["data"].get("embedding").is_none());
|
|
assert_ne!(
|
|
embedded[1]["data"]["embedding"],
|
|
serde_json::json!([0.1, 0.2])
|
|
);
|
|
assert_eq!(
|
|
embedded[1]["data"]["embedding"].as_array().unwrap().len(),
|
|
4
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn embed_seed_preserves_non_entity_rows() {
|
|
let temp = tempdir().unwrap();
|
|
let seed = write_seed_fixture_with_edge(temp.path());
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.env("OMNIGRAPH_EMBEDDINGS_MOCK", "1")
|
|
.arg("embed")
|
|
.arg("--seed")
|
|
.arg(&seed)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["rows"], 3);
|
|
assert_eq!(payload["embedded_rows"], 1);
|
|
|
|
let embedded = read_embedded_rows(temp.path().join("build/seed.embedded.jsonl"));
|
|
assert_eq!(embedded.len(), 3);
|
|
assert_eq!(embedded[2]["edge"], "Triggered");
|
|
assert_eq!(embedded[2]["from"], "sig-alpha");
|
|
assert_eq!(embedded[2]["to"], "dec-alpha");
|
|
}
|
|
|
|
#[test]
|
|
fn repair_json_reports_noop_on_clean_graph() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let output = output_success(cli().arg("repair").arg("--json").arg(&graph));
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["confirm"], false);
|
|
assert_eq!(payload["force"], false);
|
|
assert_eq!(payload["manifest_version"], Value::Null);
|
|
let tables = payload["tables"].as_array().unwrap();
|
|
assert_eq!(tables.len(), 4);
|
|
assert!(tables.iter().all(|table| {
|
|
table["classification"] == "no_drift" && table["action"] == "no_op"
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn repair_confirm_json_refuses_suspicious_drift_with_nonzero_exit_then_force_succeeds() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
let graph_manifest_before = manifest_dataset_version(&graph);
|
|
let (table_manifest_before, table_head_before) = forge_person_delete_drift(&graph);
|
|
|
|
let refused = output_failure(
|
|
cli()
|
|
.arg("repair")
|
|
.arg("--confirm")
|
|
.arg("--json")
|
|
.arg(&graph),
|
|
);
|
|
let refused_payload: Value = serde_json::from_slice(&refused.stdout).unwrap();
|
|
assert_eq!(refused_payload["manifest_version"], Value::Null);
|
|
let person = refused_payload["tables"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|table| table["table_key"] == "node:Person")
|
|
.unwrap();
|
|
assert_eq!(person["classification"], "suspicious");
|
|
assert_eq!(person["action"], "refused");
|
|
assert!(
|
|
String::from_utf8_lossy(&refused.stderr).contains("repair refused"),
|
|
"stderr should explain the non-zero exit; got: {}",
|
|
String::from_utf8_lossy(&refused.stderr)
|
|
);
|
|
assert_eq!(manifest_dataset_version(&graph), graph_manifest_before);
|
|
|
|
let forced = output_success(
|
|
cli()
|
|
.arg("repair")
|
|
.arg("--force")
|
|
.arg("--confirm")
|
|
.arg("--json")
|
|
.arg(&graph),
|
|
);
|
|
let forced_payload: Value = serde_json::from_slice(&forced.stdout).unwrap();
|
|
let forced_manifest = forced_payload["manifest_version"].as_u64().unwrap();
|
|
assert!(forced_manifest > graph_manifest_before);
|
|
let person = forced_payload["tables"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|table| table["table_key"] == "node:Person")
|
|
.unwrap();
|
|
assert_eq!(person["classification"], "suspicious");
|
|
assert_eq!(person["action"], "forced");
|
|
assert_eq!(person["manifest_version"], table_manifest_before);
|
|
assert_eq!(person["lance_head_version"], table_head_before);
|
|
assert_eq!(manifest_dataset_version(&graph), forced_manifest);
|
|
}
|
|
|
|
#[test]
|
|
fn query_lint_json_with_schema_reports_warnings() {
|
|
let temp = tempdir().unwrap();
|
|
let schema_path = temp.path().join("schema.pg");
|
|
let query_path = temp.path().join("queries.gq");
|
|
write_file(
|
|
&schema_path,
|
|
r#"
|
|
node Policy {
|
|
slug: String @key
|
|
name: String?
|
|
effectiveTo: DateTime?
|
|
}
|
|
"#,
|
|
);
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query update_policy($slug: String, $name: String) {
|
|
update Policy set { name: $name } where slug = $slug
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["status"], "ok");
|
|
assert_eq!(payload["schema_source"]["kind"], "file");
|
|
assert_eq!(payload["queries_processed"], 1);
|
|
assert_eq!(payload["warnings"], 1);
|
|
assert_eq!(payload["findings"][0]["code"], "L201");
|
|
assert_eq!(
|
|
payload["findings"][0]["message"],
|
|
"Policy.effectiveTo exists in schema but no update query sets it"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn lint_top_level_matches_deprecated_query_lint_output() {
|
|
let temp = tempdir().unwrap();
|
|
let schema_path = temp.path().join("schema.pg");
|
|
let query_path = temp.path().join("queries.gq");
|
|
write_file(
|
|
&schema_path,
|
|
r#"
|
|
node Person {
|
|
name: String
|
|
}
|
|
"#,
|
|
);
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query list_people() {
|
|
match { $p: Person }
|
|
return { $p.name }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let canonical = output_success(
|
|
cli()
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path)
|
|
.arg("--json"),
|
|
);
|
|
let deprecated_lint = output_success(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path)
|
|
.arg("--json"),
|
|
);
|
|
let deprecated_check = output_success(
|
|
cli()
|
|
.arg("query")
|
|
.arg("check")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path)
|
|
.arg("--json"),
|
|
);
|
|
|
|
assert_eq!(stdout_string(&canonical), stdout_string(&deprecated_lint));
|
|
assert_eq!(stdout_string(&canonical), stdout_string(&deprecated_check));
|
|
|
|
// Canonical form must NOT emit the deprecation warning.
|
|
let canonical_stderr = String::from_utf8(canonical.stderr).unwrap();
|
|
assert!(
|
|
!canonical_stderr.contains("deprecated"),
|
|
"`omnigraph lint` is canonical and must not warn; got stderr: {canonical_stderr}"
|
|
);
|
|
|
|
// Deprecated forms MUST emit the one-line warning, pointing at the
|
|
// new top-level `omnigraph lint`.
|
|
let lint_stderr = String::from_utf8(deprecated_lint.stderr).unwrap();
|
|
assert!(
|
|
lint_stderr.contains("`omnigraph query lint` is deprecated")
|
|
&& lint_stderr.contains("`omnigraph lint`"),
|
|
"expected deprecation warning pointing at `omnigraph lint`; got: {lint_stderr}"
|
|
);
|
|
let check_stderr = String::from_utf8(deprecated_check.stderr).unwrap();
|
|
assert!(
|
|
check_stderr.contains("`omnigraph query check` is deprecated")
|
|
&& check_stderr.contains("`omnigraph lint`"),
|
|
"expected deprecation warning pointing at `omnigraph lint`; got: {check_stderr}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deprecated_check_top_level_rewrites_to_lint() {
|
|
let temp = tempdir().unwrap();
|
|
let schema_path = temp.path().join("schema.pg");
|
|
let query_path = temp.path().join("queries.gq");
|
|
write_file(
|
|
&schema_path,
|
|
r#"
|
|
node Person {
|
|
name: String
|
|
}
|
|
"#,
|
|
);
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query list_people() {
|
|
match { $p: Person }
|
|
return { $p.name }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let canonical = output_success(
|
|
cli()
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path)
|
|
.arg("--json"),
|
|
);
|
|
let deprecated_check = output_success(
|
|
cli()
|
|
.arg("check")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path)
|
|
.arg("--json"),
|
|
);
|
|
|
|
assert_eq!(stdout_string(&canonical), stdout_string(&deprecated_check));
|
|
|
|
let check_stderr = String::from_utf8(deprecated_check.stderr).unwrap();
|
|
assert!(
|
|
check_stderr.contains("`omnigraph check` is deprecated")
|
|
&& check_stderr.contains("`omnigraph lint`"),
|
|
"expected `omnigraph check` deprecation warning pointing at `omnigraph lint`; got: {check_stderr}"
|
|
);
|
|
|
|
// `check` must NOT appear in the canonical `omnigraph --help` output —
|
|
// agents copy the surface from help text and would otherwise emit both
|
|
// names interchangeably.
|
|
let help = cli().arg("--help").output().unwrap();
|
|
let stdout = String::from_utf8(help.stdout).unwrap();
|
|
let check_aliased = stdout
|
|
.lines()
|
|
.any(|line| line.trim_start().starts_with("lint") && line.contains("check"));
|
|
assert!(
|
|
!check_aliased,
|
|
"`check` must not be advertised as a visible alias of `lint`; help output: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deprecated_read_and_change_subcommands_emit_warnings() {
|
|
// Both subcommands require `--query`/`--query-string`/`--alias`, so
|
|
// invoking them with no args will exit non-zero. That's fine --
|
|
// we only care that the deprecation warning is printed before the
|
|
// argument-required error.
|
|
let output = cli().arg("read").output().unwrap();
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
stderr.contains("`omnigraph read` is deprecated") && stderr.contains("`omnigraph query`"),
|
|
"expected `omnigraph read` deprecation warning; got: {stderr}"
|
|
);
|
|
|
|
let output = cli().arg("change").output().unwrap();
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
stderr.contains("`omnigraph change` is deprecated")
|
|
&& stderr.contains("`omnigraph mutate`"),
|
|
"expected `omnigraph change` deprecation warning; got: {stderr}"
|
|
);
|
|
|
|
// Sanity check the inverse: the canonical names must NOT print the
|
|
// deprecation banner.
|
|
let output = cli().arg("query").arg("--help").output().unwrap();
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
!stderr.contains("deprecated"),
|
|
"`omnigraph query` is canonical and must not warn; got: {stderr}"
|
|
);
|
|
let output = cli().arg("mutate").arg("--help").output().unwrap();
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
!stderr.contains("deprecated"),
|
|
"`omnigraph mutate` is canonical and must not warn; got: {stderr}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_lint_can_use_local_graph_via_positional_uri() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
let query_path = temp.path().join("queries.gq");
|
|
init_graph(&graph);
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query list_people() {
|
|
match { $p: Person }
|
|
return { $p.name }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--json")
|
|
.arg(&graph),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["status"], "ok");
|
|
assert_eq!(payload["schema_source"]["kind"], "graph");
|
|
assert_eq!(
|
|
payload["schema_source"]["uri"].as_str(),
|
|
Some(graph.to_string_lossy().as_ref())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_lint_can_resolve_graph_and_query_from_config() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
let config_path = temp.path().join("omnigraph.yaml");
|
|
init_graph(&graph);
|
|
write_query_file(
|
|
&temp.path().join("queries.gq"),
|
|
r#"
|
|
query list_people() {
|
|
match { $p: Person }
|
|
return { $p.name }
|
|
}
|
|
"#,
|
|
);
|
|
write_config(&config_path, &local_yaml_config(&graph));
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg("queries.gq")
|
|
.arg("--config")
|
|
.arg(&config_path)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["status"], "ok");
|
|
assert_eq!(payload["schema_source"]["kind"], "graph");
|
|
assert_eq!(
|
|
payload["schema_source"]["uri"].as_str(),
|
|
Some(graph.to_string_lossy().as_ref())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_lint_rejects_http_targets_without_schema() {
|
|
let temp = tempdir().unwrap();
|
|
let query_path = temp.path().join("queries.gq");
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query list_people() {
|
|
match { $p: Person }
|
|
return { $p.name }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("http://127.0.0.1:8080"),
|
|
);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr.contains("query lint is only supported against local graph URIs in this milestone")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_lint_requires_schema_or_resolvable_graph_target() {
|
|
let temp = tempdir().unwrap();
|
|
let query_path = temp.path().join("queries.gq");
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query list_people() {
|
|
match { $p: Person }
|
|
return { $p.name }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path),
|
|
);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr.contains("query lint requires --schema <schema.pg> or a resolvable graph target")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_lint_human_output_reports_warnings() {
|
|
let temp = tempdir().unwrap();
|
|
let schema_path = temp.path().join("schema.pg");
|
|
let query_path = temp.path().join("queries.gq");
|
|
write_file(
|
|
&schema_path,
|
|
r#"
|
|
node Policy {
|
|
slug: String @key
|
|
name: String?
|
|
effectiveTo: DateTime?
|
|
}
|
|
"#,
|
|
);
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query update_policy($slug: String, $name: String) {
|
|
update Policy set { name: $name } where slug = $slug
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path),
|
|
);
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert!(stdout.contains("OK query `update_policy` (mutation)"));
|
|
assert!(
|
|
stdout.contains("WARN Policy.effectiveTo exists in schema but no update query sets it")
|
|
);
|
|
assert!(stdout.contains(
|
|
"INFO Lint complete: 1 queries processed (0 error(s), 1 warning(s), 0 info item(s))"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn query_lint_human_output_reports_strict_validation_errors() {
|
|
let temp = tempdir().unwrap();
|
|
let schema_path = temp.path().join("schema.pg");
|
|
let query_path = temp.path().join("queries.gq");
|
|
write_file(
|
|
&schema_path,
|
|
r#"
|
|
node Policy {
|
|
slug: String @key
|
|
name: String?
|
|
}
|
|
"#,
|
|
);
|
|
write_query_file(
|
|
&query_path,
|
|
r#"
|
|
query bad_update($slug: String) {
|
|
update Policy set { priority_level: "high" } where slug = $slug
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("query")
|
|
.arg("lint")
|
|
.arg("--query")
|
|
.arg(&query_path)
|
|
.arg("--schema")
|
|
.arg(&schema_path),
|
|
);
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert!(stdout.contains("ERROR query `bad_update`:"));
|
|
assert!(stdout.contains("Policy"));
|
|
assert!(stdout.contains(
|
|
"INFO Lint complete: 1 queries processed (1 error(s), 0 warning(s), 0 info item(s))"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn load_json_outputs_summary_for_main_branch() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
let data = fixture("test.jsonl");
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("load")
|
|
.arg("--mode")
|
|
.arg("overwrite")
|
|
.arg("--data")
|
|
.arg(&data)
|
|
.arg("--json")
|
|
.arg(&graph),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["branch"], "main");
|
|
assert_eq!(payload["mode"], "overwrite");
|
|
assert_eq!(payload["nodes_loaded"], 6);
|
|
assert_eq!(payload["edges_loaded"], 5);
|
|
assert_eq!(payload["node_types_loaded"], 2);
|
|
assert_eq!(payload["edge_types_loaded"], 2);
|
|
}
|
|
|
|
#[test]
|
|
fn load_into_feature_branch_with_merge_mode_succeeds() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature"),
|
|
);
|
|
|
|
let feature_data = temp.path().join("feature.jsonl");
|
|
write_jsonl(
|
|
&feature_data,
|
|
r#"{"type":"Person","data":{"name":"Alice","age":31}}"#,
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("load")
|
|
.arg("--data")
|
|
.arg(&feature_data)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--mode")
|
|
.arg("merge")
|
|
.arg(&graph),
|
|
);
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert!(stdout.contains("branch feature"));
|
|
assert!(stdout.contains("with merge"));
|
|
assert!(stdout.contains("1 nodes across 1 node types"));
|
|
}
|
|
|
|
#[test]
|
|
fn read_json_outputs_rows_for_named_query() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
let queries = fixture("test.gq");
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&graph)
|
|
.arg("--query")
|
|
.arg(&queries)
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["query_name"], "get_person");
|
|
assert_eq!(payload["target"]["branch"], "main");
|
|
assert_eq!(payload["row_count"], 1);
|
|
assert_eq!(payload["rows"][0]["p.name"], "Alice");
|
|
}
|
|
|
|
#[test]
|
|
fn export_jsonl_outputs_source_rows_for_selected_branch_and_type() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature"),
|
|
);
|
|
|
|
let feature_data = temp.path().join("feature-export.jsonl");
|
|
write_jsonl(
|
|
&feature_data,
|
|
r#"{"type":"Person","data":{"name":"Eve","age":29}}"#,
|
|
);
|
|
output_success(
|
|
cli()
|
|
.arg("load")
|
|
.arg("--data")
|
|
.arg(&feature_data)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--mode")
|
|
.arg("append")
|
|
.arg(&graph),
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("export")
|
|
.arg(&graph)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--type")
|
|
.arg("Person")
|
|
.arg("--jsonl"),
|
|
);
|
|
let rows = stdout_string(&output)
|
|
.lines()
|
|
.map(|line| serde_json::from_str::<Value>(line).unwrap())
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(rows.len(), 5);
|
|
assert!(rows.iter().all(|row| row["type"] == "Person"));
|
|
assert!(rows.iter().all(|row| row.get("edge").is_none()));
|
|
assert!(
|
|
rows.iter()
|
|
.any(|row| row["data"]["name"].as_str() == Some("Eve"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn policy_validate_accepts_valid_policy_file() {
|
|
let temp = tempdir().unwrap();
|
|
let (config, _) = write_policy_config_fixture(temp.path());
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("policy")
|
|
.arg("validate")
|
|
.arg("--config")
|
|
.arg(&config),
|
|
);
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert!(stdout.contains("policy valid:"));
|
|
assert!(stdout.contains("policy.yaml"));
|
|
assert!(stdout.contains("[2 actors]"));
|
|
}
|
|
|
|
#[test]
|
|
fn policy_validate_fails_for_invalid_policy_file() {
|
|
let temp = tempdir().unwrap();
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
let policy = temp.path().join("policy.yaml");
|
|
fs::write(
|
|
&config,
|
|
r#"
|
|
project:
|
|
name: policy-test-graph
|
|
policy:
|
|
file: ./policy.yaml
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
&policy,
|
|
r#"
|
|
version: 1
|
|
groups:
|
|
team: [act-andrew]
|
|
rules:
|
|
- id: duplicate
|
|
allow:
|
|
actors: { group: team }
|
|
actions: [read]
|
|
branch_scope: any
|
|
- id: duplicate
|
|
allow:
|
|
actors: { group: team }
|
|
actions: [export]
|
|
branch_scope: any
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("policy")
|
|
.arg("validate")
|
|
.arg("--config")
|
|
.arg(&config),
|
|
);
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(stderr.contains("duplicate policy rule id"));
|
|
}
|
|
|
|
#[test]
|
|
fn policy_test_runs_declarative_cases() {
|
|
let temp = tempdir().unwrap();
|
|
let (config, _) = write_policy_config_fixture(temp.path());
|
|
|
|
let output = output_success(cli().arg("policy").arg("test").arg("--config").arg(&config));
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert!(stdout.contains("policy tests passed: 2 cases"));
|
|
}
|
|
|
|
#[test]
|
|
fn policy_explain_reports_decision_and_matched_rule() {
|
|
let temp = tempdir().unwrap();
|
|
let (config, _) = write_policy_config_fixture(temp.path());
|
|
|
|
let allow = output_success(
|
|
cli()
|
|
.arg("policy")
|
|
.arg("explain")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--actor")
|
|
.arg("act-andrew")
|
|
.arg("--action")
|
|
.arg("change")
|
|
.arg("--branch")
|
|
.arg("feature"),
|
|
);
|
|
let allow_stdout = stdout_string(&allow);
|
|
assert!(allow_stdout.contains("decision: allow"));
|
|
assert!(allow_stdout.contains("matched_rule: team-write"));
|
|
|
|
let deny = output_success(
|
|
cli()
|
|
.arg("policy")
|
|
.arg("explain")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--actor")
|
|
.arg("act-bruno")
|
|
.arg("--action")
|
|
.arg("change")
|
|
.arg("--branch")
|
|
.arg("main"),
|
|
);
|
|
let deny_stdout = stdout_string(&deny);
|
|
assert!(deny_stdout.contains("decision: deny"));
|
|
assert!(deny_stdout.contains("message: policy denied action 'change' on branch 'main'"));
|
|
}
|
|
|
|
#[test]
|
|
fn read_can_resolve_uri_from_config() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
write_config(&config, &local_yaml_config(&graph));
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["row_count"], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn read_csv_format_outputs_header_and_row_values() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&graph)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--format")
|
|
.arg("csv"),
|
|
);
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert!(stdout.lines().next().unwrap().contains("p.name"));
|
|
assert!(stdout.contains("Alice"));
|
|
}
|
|
|
|
/// RFC-007 PR 1: the format cascade's operator hop — `defaults.output` in
|
|
/// ~/.omnigraph/config.yaml applies when nothing more specific is given,
|
|
/// and `--format` still wins over it.
|
|
#[test]
|
|
fn read_uses_operator_default_output_format() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
let operator_home = tempdir().unwrap();
|
|
fs::write(
|
|
operator_home.path().join("config.yaml"),
|
|
"defaults:\n output: csv\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let read = |extra: &[&str]| {
|
|
let mut command = cli();
|
|
command
|
|
.env("OMNIGRAPH_HOME", operator_home.path())
|
|
.arg("read")
|
|
.arg(&graph)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#);
|
|
for arg in extra {
|
|
command.arg(arg);
|
|
}
|
|
stdout_string(&output_success(&mut command))
|
|
};
|
|
|
|
let stdout = read(&[]);
|
|
assert!(
|
|
stdout.lines().next().unwrap().contains("p.name") && stdout.contains("Alice"),
|
|
"operator defaults.output: csv applies with no --format: {stdout}"
|
|
);
|
|
let stdout = read(&["--format", "jsonl"]);
|
|
assert!(
|
|
stdout.starts_with('{'),
|
|
"--format wins over the operator default: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_jsonl_format_outputs_metadata_header_first() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&graph)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--format")
|
|
.arg("jsonl"),
|
|
);
|
|
let stdout = stdout_string(&output);
|
|
let mut lines = stdout.lines();
|
|
assert!(lines.next().unwrap().contains("\"kind\":\"metadata\""));
|
|
assert!(lines.next().unwrap().contains("\"p.name\":\"Alice\""));
|
|
}
|
|
|
|
#[test]
|
|
fn change_json_outputs_affected_counts_and_persists() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
let mutation_file = temp.path().join("mutations.gq");
|
|
write_query_file(
|
|
&mutation_file,
|
|
r#"
|
|
query insert_person($name: String, $age: I32) {
|
|
insert Person { name: $name, age: $age }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg(&graph)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Eve","age":29}"#)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["branch"], "main");
|
|
assert_eq!(payload["query_name"], "insert_person");
|
|
assert_eq!(payload["affected_nodes"], 1);
|
|
assert_eq!(payload["affected_edges"], 0);
|
|
|
|
let verify = output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&graph)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Eve"}"#)
|
|
.arg("--json"),
|
|
);
|
|
let verify_payload: Value = serde_json::from_slice(&verify.stdout).unwrap();
|
|
assert_eq!(verify_payload["row_count"], 1);
|
|
assert_eq!(verify_payload["rows"][0]["p.name"], "Eve");
|
|
}
|
|
|
|
#[test]
|
|
fn change_can_resolve_uri_and_branch_from_config() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
write_config(&config, &local_yaml_config(&graph));
|
|
let mutation_file = temp.path().join("config-mutations.gq");
|
|
write_query_file(
|
|
&mutation_file,
|
|
r#"
|
|
query insert_person($name: String, $age: I32) {
|
|
insert Person { name: $name, age: $age }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Mia","age":30}"#)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["branch"], "main");
|
|
assert_eq!(payload["affected_nodes"], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn read_requires_name_for_multi_query_files() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&graph)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq")),
|
|
);
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(stderr.contains("multiple queries"));
|
|
}
|
|
|
|
#[test]
|
|
fn read_supports_inline_query_string() {
|
|
let temp = tempdir().unwrap();
|
|
let repo = graph_path(temp.path());
|
|
init_graph(&repo);
|
|
load_fixture(&repo);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&repo)
|
|
.arg("-e")
|
|
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name, $p.age } }")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["query_name"], "find");
|
|
assert_eq!(payload["row_count"], 1);
|
|
assert_eq!(payload["rows"][0]["p.name"], "Alice");
|
|
}
|
|
|
|
#[test]
|
|
fn change_supports_inline_query_string() {
|
|
let temp = tempdir().unwrap();
|
|
let repo = graph_path(temp.path());
|
|
init_graph(&repo);
|
|
load_fixture(&repo);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg(&repo)
|
|
.arg("--query-string")
|
|
.arg("query add($name: String, $age: I32) { insert Person { name: $name, age: $age } }")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Inline","age":42}"#)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["query_name"], "add");
|
|
assert_eq!(payload["affected_nodes"], 1);
|
|
|
|
let verify = output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&repo)
|
|
.arg("-e")
|
|
.arg("query find($name: String) { match { $p: Person { name: $name } } return { $p.name } }")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Inline"}"#)
|
|
.arg("--json"),
|
|
);
|
|
let verify_payload: Value = serde_json::from_slice(&verify.stdout).unwrap();
|
|
assert_eq!(verify_payload["row_count"], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn read_rejects_query_string_combined_with_query() {
|
|
let temp = tempdir().unwrap();
|
|
let repo = graph_path(temp.path());
|
|
init_graph(&repo);
|
|
load_fixture(&repo);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&repo)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("-e")
|
|
.arg("query whatever() { match { $p: Person } return { $p.name } }"),
|
|
);
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
stderr.contains("cannot be used") || stderr.contains("conflict"),
|
|
"expected clap conflict error, got: {stderr}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_rejects_empty_query_string() {
|
|
let temp = tempdir().unwrap();
|
|
let repo = graph_path(temp.path());
|
|
init_graph(&repo);
|
|
load_fixture(&repo);
|
|
|
|
let output = output_failure(cli().arg("read").arg(&repo).arg("-e").arg(""));
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
stderr.contains("must not be empty"),
|
|
"expected empty-string rejection, got: {stderr}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn branch_create_json_outputs_source_and_name() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature")
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["from"], "main");
|
|
assert_eq!(payload["name"], "feature");
|
|
assert_eq!(payload["uri"], graph.to_string_lossy().as_ref());
|
|
}
|
|
|
|
#[test]
|
|
fn branch_list_outputs_sorted_branches() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("zeta"),
|
|
);
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("alpha"),
|
|
);
|
|
|
|
let output = output_success(cli().arg("branch").arg("list").arg("--uri").arg(&graph));
|
|
let stdout = stdout_string(&output);
|
|
let lines = stdout
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(lines, vec!["alpha", "main", "zeta"]);
|
|
}
|
|
|
|
#[test]
|
|
fn branch_delete_json_outputs_name_and_removes_branch() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature"),
|
|
);
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("delete")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("feature")
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["name"], "feature");
|
|
assert_eq!(payload["uri"], graph.to_string_lossy().as_ref());
|
|
|
|
let listed = output_success(cli().arg("branch").arg("list").arg("--uri").arg(&graph));
|
|
let stdout = stdout_string(&listed);
|
|
let lines = stdout
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(lines, vec!["main"]);
|
|
}
|
|
|
|
#[test]
|
|
fn branch_delete_rejects_main() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("delete")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("main"),
|
|
);
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(stderr.contains("cannot delete branch 'main'"));
|
|
}
|
|
|
|
#[test]
|
|
fn branch_merge_defaults_target_to_main() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature"),
|
|
);
|
|
|
|
let feature_data = temp.path().join("feature.jsonl");
|
|
write_jsonl(
|
|
&feature_data,
|
|
r#"{"type":"Person","data":{"name":"Eve","age":29}}"#,
|
|
);
|
|
output_success(
|
|
cli()
|
|
.arg("load")
|
|
.arg("--data")
|
|
.arg(&feature_data)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--mode")
|
|
.arg("append")
|
|
.arg(&graph),
|
|
);
|
|
|
|
let merge_output = output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("merge")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("feature")
|
|
.arg("--json"),
|
|
);
|
|
let merge_payload: Value = serde_json::from_slice(&merge_output.stdout).unwrap();
|
|
assert_eq!(merge_payload["source"], "feature");
|
|
assert_eq!(merge_payload["target"], "main");
|
|
assert_eq!(merge_payload["outcome"], "fast_forward");
|
|
|
|
let snapshot_output = output_success(
|
|
cli()
|
|
.arg("snapshot")
|
|
.arg(&graph)
|
|
.arg("--branch")
|
|
.arg("main")
|
|
.arg("--json"),
|
|
);
|
|
let snapshot: Value = serde_json::from_slice(&snapshot_output.stdout).unwrap();
|
|
let person_row_count = snapshot["tables"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|table| table["table_key"] == "node:Person")
|
|
.unwrap()["row_count"]
|
|
.as_u64()
|
|
.unwrap();
|
|
assert_eq!(person_row_count, 5);
|
|
}
|
|
|
|
#[test]
|
|
fn branch_merge_supports_explicit_target() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature"),
|
|
);
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("experiment"),
|
|
);
|
|
|
|
let feature_data = temp.path().join("feature-explicit.jsonl");
|
|
write_jsonl(
|
|
&feature_data,
|
|
r#"{"type":"Person","data":{"name":"Frank","age":41}}"#,
|
|
);
|
|
output_success(
|
|
cli()
|
|
.arg("load")
|
|
.arg("--data")
|
|
.arg(&feature_data)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--mode")
|
|
.arg("append")
|
|
.arg(&graph),
|
|
);
|
|
|
|
let merge_output = output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("merge")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("feature")
|
|
.arg("--into")
|
|
.arg("experiment")
|
|
.arg("--json"),
|
|
);
|
|
let merge_payload: Value = serde_json::from_slice(&merge_output.stdout).unwrap();
|
|
assert_eq!(merge_payload["target"], "experiment");
|
|
assert_eq!(merge_payload["outcome"], "fast_forward");
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_json_returns_manifest_version_and_tables() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let output = output_success(cli().arg("snapshot").arg(&graph).arg("--json"));
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["branch"], "main");
|
|
assert_eq!(
|
|
payload["manifest_version"].as_u64().unwrap(),
|
|
manifest_dataset_version(&graph)
|
|
);
|
|
assert!(payload["tables"].as_array().unwrap().len() >= 4);
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_can_resolve_uri_from_config() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
let config = temp.path().join("omnigraph.yaml");
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
write_config(&config, &local_yaml_config(&graph));
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("snapshot")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(payload["branch"], "main");
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_human_output_includes_branch_and_table_summaries() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let output = output_success(cli().arg("snapshot").arg(&graph));
|
|
let stdout = stdout_string(&output);
|
|
|
|
assert!(stdout.contains("branch: main"));
|
|
assert!(stdout.contains("manifest_version:"));
|
|
assert!(stdout.contains("node:Person v"));
|
|
assert!(stdout.contains("edge:Knows v"));
|
|
}
|
|
|
|
#[test]
|
|
fn commit_show_accepts_long_uri_flag() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let list = output_success(cli().arg("commit").arg("list").arg(&graph).arg("--json"));
|
|
let list_payload: Value = serde_json::from_slice(&list.stdout).unwrap();
|
|
let commit_id = list_payload["commits"][0]["graph_commit_id"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
let output = output_success(
|
|
cli()
|
|
.arg("commit")
|
|
.arg("show")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg(&commit_id)
|
|
.arg("--json"),
|
|
);
|
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
|
|
assert_eq!(payload["graph_commit_id"], commit_id);
|
|
assert!(payload["manifest_version"].as_u64().unwrap() >= 1);
|
|
}
|
|
|
|
#[test]
|
|
fn cli_fails_for_missing_graph() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
|
|
let output = output_failure(cli().arg("snapshot").arg(&graph));
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
stderr.contains("_schema.pg")
|
|
|| stderr.contains("No such file")
|
|
|| stderr.contains("not found")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cli_fails_for_missing_schema_or_data_file() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
let missing_schema = temp.path().join("missing.pg");
|
|
let missing_data = temp.path().join("missing.jsonl");
|
|
|
|
let init_output = output_failure(
|
|
cli()
|
|
.arg("init")
|
|
.arg("--schema")
|
|
.arg(&missing_schema)
|
|
.arg(&graph),
|
|
);
|
|
assert!(
|
|
String::from_utf8(init_output.stderr)
|
|
.unwrap()
|
|
.contains("No such file")
|
|
);
|
|
|
|
init_graph(&graph);
|
|
let load_output = output_failure(
|
|
cli()
|
|
.arg("load")
|
|
.arg("--mode")
|
|
.arg("overwrite")
|
|
.arg("--data")
|
|
.arg(&missing_data)
|
|
.arg(&graph),
|
|
);
|
|
assert!(
|
|
String::from_utf8(load_output.stderr)
|
|
.unwrap()
|
|
.contains("No such file")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cli_fails_for_invalid_merge_requests() {
|
|
let temp = tempdir().unwrap();
|
|
let graph = graph_path(temp.path());
|
|
init_graph(&graph);
|
|
load_fixture(&graph);
|
|
|
|
let missing_branch = output_failure(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("merge")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("missing"),
|
|
);
|
|
let missing_branch_stderr = String::from_utf8(missing_branch.stderr).unwrap();
|
|
assert!(
|
|
missing_branch_stderr.contains("missing")
|
|
|| missing_branch_stderr.contains("head commit")
|
|
|| missing_branch_stderr.contains("not found")
|
|
);
|
|
|
|
let same_branch = output_failure(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("merge")
|
|
.arg("--uri")
|
|
.arg(&graph)
|
|
.arg("main")
|
|
.arg("--into")
|
|
.arg("main"),
|
|
);
|
|
assert!(
|
|
String::from_utf8(same_branch.stderr)
|
|
.unwrap()
|
|
.contains("distinct source and target")
|
|
);
|
|
}
|