mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-12 01:45:14 +02:00
PR 8 of the MR-668 multi-graph server work. CLI parity for the
v0.7.0 management surface: operators can now manage graphs from
the command line against a running multi-graph server.
omnigraph graphs list --target dev --json
omnigraph graphs create \
--target dev \
--graph-id beta \
--graph-uri /data/beta.omni \
--schema schema.pg
DELETE is intentionally absent — server-side DELETE was deferred from
v0.7.0 scope, and shipping a client subcommand for a server endpoint
that doesn't exist would be dead vocabulary. The help output, the
subcommand enum, and the test that pins it (`graphs_subcommand_help_
lists_list_and_create`) all agree.
CLI architecture (modeled on `BranchCommand`):
- New `Command::Graphs { command: GraphsCommand }` top-level variant.
- `GraphsCommand { List, Create }` enum.
- List: GET `<base>/graphs`. Stdout is `<graph_id>\t<uri>` per line,
or JSON via `--json`.
- Create: reads `--schema <path>` from local disk, inlines as
`schema: { source: <file> }` in the POST body (nested per
MR-668 decision 7). Optional `--policy-file <path>` becomes
`policy: { file: <path> }`. Returns 201 → "created graph X at Y"
or JSON via `--json`.
- Both subcommands reject local URI targets with a clear
"remote multi-graph server URL" error.
New API type imports in the CLI: `GraphCreateRequest`,
`GraphCreateResponse`, `GraphListResponse`, `GraphSchemaSpec`,
`GraphPolicySpec` — all from `omnigraph-server::api`.
Tests:
- cli.rs (4 new, non-network):
* `graphs_subcommand_help_lists_list_and_create` — pins the
deferral of `delete` (catches scope creep).
* `graphs_list_against_local_uri_errors_with_remote_only_message`
* `graphs_create_against_local_uri_errors_with_remote_only_message`
* `graphs_create_with_missing_schema_file_errors` — pins the
IO context in the schema-read error path.
- system_remote.rs (1 new, `#[ignore]` like its peers):
* `graphs_list_and_create_against_multi_graph_server` — spawns a
multi-mode server, calls `graphs list` (sees `alpha`),
`graphs create` (adds `beta`), `graphs list` again (sees both),
and confirms the new graph is reachable via its cluster route.
CLI suite: 62 tests green (58 existing + 4 new). The new ignored
end-to-end test runs locally with `cargo test --ignored`.
LOC: +159 main.rs (enum + handlers), +88 cli.rs (unit tests),
+131 system_remote.rs (integration test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1021 lines
29 KiB
Rust
1021 lines
29 KiB
Rust
mod support;
|
|
|
|
use std::fs;
|
|
|
|
use omnigraph::db::Omnigraph;
|
|
use reqwest::blocking::Client;
|
|
use serde_json::json;
|
|
|
|
use support::*;
|
|
|
|
const REMOTE_POLICY_E2E_YAML: &str = r#"
|
|
version: 1
|
|
groups:
|
|
team: [act-bruno]
|
|
admins: [act-ragnor]
|
|
protected_branches: [main]
|
|
rules:
|
|
- id: team-read
|
|
allow:
|
|
actors: { group: team }
|
|
actions: [read]
|
|
branch_scope: any
|
|
- id: team-branch-create
|
|
allow:
|
|
actors: { group: team }
|
|
actions: [branch_create]
|
|
target_branch_scope: unprotected
|
|
- id: team-write-unprotected
|
|
allow:
|
|
actors: { group: team }
|
|
actions: [change]
|
|
branch_scope: unprotected
|
|
- id: admins-promote
|
|
allow:
|
|
actors: { group: admins }
|
|
actions: [branch_merge]
|
|
target_branch_scope: protected
|
|
"#;
|
|
|
|
fn yaml_string(value: &str) -> String {
|
|
format!("'{}'", value.replace('\'', "''"))
|
|
}
|
|
|
|
fn remote_policy_server_config(graph: &SystemGraph) -> String {
|
|
format!(
|
|
"\
|
|
project:
|
|
name: remote-policy-e2e
|
|
graphs:
|
|
local:
|
|
uri: {}
|
|
server:
|
|
graph: local
|
|
policy:
|
|
file: ./policy.yaml
|
|
",
|
|
yaml_string(&graph.path().to_string_lossy())
|
|
)
|
|
}
|
|
|
|
fn remote_policy_client_config(url: &str) -> String {
|
|
format!(
|
|
"\
|
|
graphs:
|
|
dev:
|
|
uri: {}
|
|
bearer_token_env: POLICY_TEST_TOKEN
|
|
cli:
|
|
graph: dev
|
|
branch: main
|
|
query:
|
|
roots:
|
|
- .
|
|
auth:
|
|
env_file: ./.env.omni
|
|
",
|
|
yaml_string(url)
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_server_and_cli_end_to_end_flow() {
|
|
let graph = SystemGraph::loaded();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let mutation_file = graph.write_query(
|
|
"system-remote-change.gq",
|
|
r#"
|
|
query insert_person($name: String, $age: I32) {
|
|
insert Person { name: $name, age: $age }
|
|
}
|
|
"#,
|
|
);
|
|
let client = Client::new();
|
|
|
|
let health = client
|
|
.get(format!("{}/healthz", server.base_url))
|
|
.send()
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.json::<serde_json::Value>()
|
|
.unwrap();
|
|
assert_eq!(health["status"], "ok");
|
|
|
|
let local_snapshot = parse_stdout_json(&output_success(
|
|
cli().arg("snapshot").arg(graph.path()).arg("--json"),
|
|
));
|
|
let snapshot = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("snapshot")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(snapshot["branch"], "main");
|
|
assert_eq!(snapshot["tables"], local_snapshot["tables"]);
|
|
|
|
let local_read = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(graph.path())
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--json"),
|
|
));
|
|
let read_payload = parse_stdout_json(&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"),
|
|
));
|
|
assert_eq!(read_payload, local_read);
|
|
assert_eq!(read_payload["row_count"], 1);
|
|
assert_eq!(read_payload["rows"][0]["p.name"], "Alice");
|
|
|
|
let change_payload = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Mina","age":28}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(change_payload["affected_nodes"], 1);
|
|
|
|
let query_source = fs::read_to_string(fixture("test.gq")).unwrap();
|
|
let http_read = client
|
|
.post(format!("{}/read", server.base_url))
|
|
.json(&json!({
|
|
"branch": "main",
|
|
"query_source": query_source,
|
|
"query_name": "get_person",
|
|
"params": { "name": "Mina" }
|
|
}))
|
|
.send()
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.json::<serde_json::Value>()
|
|
.unwrap();
|
|
assert_eq!(http_read["row_count"], 1);
|
|
assert_eq!(http_read["rows"][0]["p.name"], "Mina");
|
|
|
|
let local_verify = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(graph.path())
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Mina"}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(local_verify["row_count"], 1);
|
|
assert_eq!(local_verify["rows"][0]["p.name"], "Mina");
|
|
|
|
// `run publish` / `run list` removed. Direct-to-target writes
|
|
// already landed via the change call above; the commit graph is now
|
|
// the audit surface (verified separately by `commit list`).
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_schema_apply_via_cli_updates_graph() {
|
|
let graph = SystemGraph::initialized();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let next_schema = graph.write_file(
|
|
"next.pg",
|
|
&fs::read_to_string(fixture("test.pg")).unwrap().replace(
|
|
" age: I32?\n}",
|
|
" age: I32?\n nickname: String?\n}",
|
|
),
|
|
);
|
|
|
|
let payload = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("schema")
|
|
.arg("apply")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--schema")
|
|
.arg(&next_schema)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(payload["applied"], true);
|
|
|
|
let db = tokio::runtime::Runtime::new()
|
|
.unwrap()
|
|
.block_on(Omnigraph::open(graph.path().to_string_lossy().as_ref()))
|
|
.unwrap();
|
|
assert!(
|
|
db.catalog().node_types["Person"]
|
|
.properties
|
|
.contains_key("nickname")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_schema_apply_rejects_unsupported_plan() {
|
|
let graph = SystemGraph::initialized();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let breaking_schema = graph.write_file(
|
|
"breaking.pg",
|
|
&fs::read_to_string(fixture("test.pg"))
|
|
.unwrap()
|
|
.replace("age: I32?", "age: I64?"),
|
|
);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("schema")
|
|
.arg("apply")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--schema")
|
|
.arg(&breaking_schema),
|
|
);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(stderr.contains("changing property type"));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_schema_apply_rejects_when_non_main_branch_exists() {
|
|
let graph = SystemGraph::initialized();
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("--uri")
|
|
.arg(graph.path())
|
|
.arg("feature"),
|
|
);
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let next_schema = graph.write_file(
|
|
"next.pg",
|
|
&fs::read_to_string(fixture("test.pg")).unwrap().replace(
|
|
" age: I32?\n}",
|
|
" age: I32?\n nickname: String?\n}",
|
|
),
|
|
);
|
|
|
|
let output = output_failure(
|
|
cli()
|
|
.arg("schema")
|
|
.arg("apply")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--schema")
|
|
.arg(&next_schema),
|
|
);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(stderr.contains("schema apply requires a graph with only main"));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_read_preserves_projection_order_in_json_and_csv() {
|
|
let graph = SystemGraph::loaded();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let ordered_query = graph.write_query(
|
|
"ordered-remote.gq",
|
|
r#"
|
|
query ordered_person($name: String) {
|
|
match {
|
|
$p: Person { name: $name }
|
|
}
|
|
return { $p.age, $p.name }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let json_payload = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(&ordered_query)
|
|
.arg("--name")
|
|
.arg("ordered_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--json"),
|
|
));
|
|
let columns = json_payload["columns"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|value| value.as_str().unwrap())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(columns, vec!["p.age", "p.name"]);
|
|
|
|
let csv = stdout_string(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(&ordered_query)
|
|
.arg("--name")
|
|
.arg("ordered_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Alice"}"#)
|
|
.arg("--format")
|
|
.arg("csv"),
|
|
));
|
|
let mut lines = csv.lines();
|
|
assert_eq!(lines.next().unwrap(), "p.age,p.name");
|
|
assert_eq!(lines.next().unwrap(), "30,Alice");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_branch_create_list_merge_flow() {
|
|
let graph = SystemGraph::loaded();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let mutation_file = graph.write_query(
|
|
"system-remote-branch-change.gq",
|
|
r#"
|
|
query insert_person($name: String, $age: I32) {
|
|
insert Person { name: $name, age: $age }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let initial = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("list")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(initial["branches"], json!(["main"]));
|
|
|
|
let created = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(created["from"], "main");
|
|
assert_eq!(created["name"], "feature");
|
|
|
|
let listed = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("list")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(listed["branches"], json!(["feature", "main"]));
|
|
|
|
let changed = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Zoe","age":33}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(changed["branch"], "feature");
|
|
assert_eq!(changed["affected_nodes"], 1);
|
|
|
|
let merged = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("merge")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("feature")
|
|
.arg("--into")
|
|
.arg("main")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(merged["source"], "feature");
|
|
assert_eq!(merged["target"], "main");
|
|
assert_eq!(merged["outcome"], "fast_forward");
|
|
|
|
let verify = parse_stdout_json(&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":"Zoe"}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(verify["row_count"], 1);
|
|
assert_eq!(verify["rows"][0]["p.name"], "Zoe");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_branch_delete_removes_branch() {
|
|
let graph = SystemGraph::loaded();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
|
|
parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature")
|
|
.arg("--json"),
|
|
));
|
|
|
|
let deleted = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("delete")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("feature")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(deleted["name"], "feature");
|
|
|
|
let listed = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("list")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(listed["branches"], json!(["main"]));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_export_round_trips_full_branch_graph() {
|
|
let graph = SystemGraph::loaded();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let mutation_file = graph.write_query(
|
|
"system-remote-export-change.gq",
|
|
r#"
|
|
query insert_person($name: String, $age: I32) {
|
|
insert Person { name: $name, age: $age }
|
|
}
|
|
|
|
query add_friend($from: String, $to: String) {
|
|
insert Knows { from: $from, to: $to }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature"),
|
|
);
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--name")
|
|
.arg("insert_person")
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Eve","age":29}"#)
|
|
.arg("--json"),
|
|
);
|
|
output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--name")
|
|
.arg("add_friend")
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--params")
|
|
.arg(r#"{"from":"Alice","to":"Eve"}"#)
|
|
.arg("--json"),
|
|
);
|
|
|
|
let exported = stdout_string(&output_success(
|
|
cli()
|
|
.arg("export")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--jsonl"),
|
|
));
|
|
let export_path = graph.write_jsonl("system-remote-exported.jsonl", &exported);
|
|
let imported_graph = graph
|
|
.path()
|
|
.parent()
|
|
.unwrap()
|
|
.join("imported-remote-export.omni");
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("init")
|
|
.arg("--schema")
|
|
.arg(fixture("test.pg"))
|
|
.arg(&imported_graph),
|
|
);
|
|
output_success(
|
|
cli()
|
|
.arg("load")
|
|
.arg("--data")
|
|
.arg(&export_path)
|
|
.arg(&imported_graph),
|
|
);
|
|
|
|
let snapshot = parse_stdout_json(&output_success(
|
|
cli().arg("snapshot").arg(&imported_graph).arg("--json"),
|
|
));
|
|
assert_eq!(
|
|
snapshot["tables"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|table| table["table_key"] == "node:Person")
|
|
.unwrap()["row_count"],
|
|
5
|
|
);
|
|
assert_eq!(
|
|
snapshot["tables"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|table| table["table_key"] == "edge:Knows")
|
|
.unwrap()["row_count"],
|
|
4
|
|
);
|
|
|
|
let eve = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg(&imported_graph)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Eve"}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(eve["row_count"], 1);
|
|
assert_eq!(eve["rows"][0]["p.name"], "Eve");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_ingest_creates_review_branch_and_keeps_it_readable() {
|
|
let graph = SystemGraph::loaded();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
let ingest_data = graph.write_jsonl(
|
|
"system-remote-ingest.jsonl",
|
|
r#"{"type":"Person","data":{"name":"Zoe","age":33}}
|
|
{"type":"Person","data":{"name":"Bob","age":26}}"#,
|
|
);
|
|
|
|
let ingest_payload = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("ingest")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--data")
|
|
.arg(&ingest_data)
|
|
.arg("--branch")
|
|
.arg("feature-ingest")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(ingest_payload["branch"], "feature-ingest");
|
|
assert_eq!(ingest_payload["base_branch"], "main");
|
|
assert_eq!(ingest_payload["branch_created"], true);
|
|
assert_eq!(ingest_payload["mode"], "merge");
|
|
assert_eq!(ingest_payload["tables"][0]["table_key"], "node:Person");
|
|
assert_eq!(ingest_payload["tables"][0]["rows_loaded"], 2);
|
|
|
|
let feature_snapshot = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("snapshot")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--branch")
|
|
.arg("feature-ingest")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(feature_snapshot["branch"], "feature-ingest");
|
|
|
|
let zoe = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--branch")
|
|
.arg("feature-ingest")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Zoe"}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(zoe["row_count"], 1);
|
|
assert_eq!(zoe["rows"][0]["p.name"], "Zoe");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_ingest_reuses_existing_branch_and_merges_updates() {
|
|
let graph = SystemGraph::loaded();
|
|
let server = graph.spawn_server();
|
|
let config = graph.write_config("omnigraph.yaml", &remote_yaml_config(&server.base_url));
|
|
|
|
output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature-ingest"),
|
|
);
|
|
|
|
let ingest_data = graph.write_jsonl(
|
|
"system-remote-ingest-merge.jsonl",
|
|
r#"{"type":"Person","data":{"name":"Bob","age":26}}
|
|
{"type":"Person","data":{"name":"Zoe","age":33}}"#,
|
|
);
|
|
|
|
let ingest_payload = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("ingest")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--data")
|
|
.arg(&ingest_data)
|
|
.arg("--branch")
|
|
.arg("feature-ingest")
|
|
.arg("--from")
|
|
.arg("missing-base")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(ingest_payload["branch"], "feature-ingest");
|
|
assert_eq!(ingest_payload["base_branch"], "missing-base");
|
|
assert_eq!(ingest_payload["branch_created"], false);
|
|
assert_eq!(ingest_payload["mode"], "merge");
|
|
assert_eq!(ingest_payload["tables"][0]["table_key"], "node:Person");
|
|
assert_eq!(ingest_payload["tables"][0]["rows_loaded"], 2);
|
|
|
|
let bob = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--branch")
|
|
.arg("feature-ingest")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Bob"}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(bob["row_count"], 1);
|
|
assert_eq!(bob["rows"][0]["p.age"], 26);
|
|
|
|
let zoe = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg("--config")
|
|
.arg(&config)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--branch")
|
|
.arg("feature-ingest")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"Zoe"}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(zoe["row_count"], 1);
|
|
assert_eq!(zoe["rows"][0]["p.name"], "Zoe");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn remote_policy_enforces_branch_first_cli_workflow() {
|
|
let graph = SystemGraph::loaded();
|
|
let server_config =
|
|
graph.write_config("server-policy.yaml", &remote_policy_server_config(&graph));
|
|
graph.write_config("policy.yaml", REMOTE_POLICY_E2E_YAML);
|
|
let server = graph.spawn_server_with_config_env(
|
|
&server_config,
|
|
&[(
|
|
"OMNIGRAPH_SERVER_BEARER_TOKENS_JSON",
|
|
r#"{"act-bruno":"team-token","act-ragnor":"admin-token"}"#,
|
|
)],
|
|
);
|
|
let client_config = graph.write_config(
|
|
"omnigraph-policy.yaml",
|
|
&remote_policy_client_config(&server.base_url),
|
|
);
|
|
graph.write_config(".env.omni", "POLICY_TEST_TOKEN=team-token\n");
|
|
let mutation_file = graph.write_query(
|
|
"system-remote-policy-change.gq",
|
|
r#"
|
|
query insert_person($name: String, $age: I32) {
|
|
insert Person { name: $name, age: $age }
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let snapshot = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("snapshot")
|
|
.arg("--config")
|
|
.arg(&client_config)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(snapshot["branch"], "main");
|
|
|
|
let denied_main_change = output_failure(
|
|
cli()
|
|
.arg("change")
|
|
.arg("--config")
|
|
.arg(&client_config)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--params")
|
|
.arg(r#"{"name":"PolicyRemote","age":41}"#)
|
|
.arg("--json"),
|
|
);
|
|
let denied_main_stderr = String::from_utf8(denied_main_change.stderr).unwrap();
|
|
assert!(denied_main_stderr.contains("policy denied action 'change' on branch 'main'"));
|
|
|
|
let created = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("create")
|
|
.arg("--config")
|
|
.arg(&client_config)
|
|
.arg("--from")
|
|
.arg("main")
|
|
.arg("feature")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(created["name"], "feature");
|
|
|
|
let changed = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("change")
|
|
.arg("--config")
|
|
.arg(&client_config)
|
|
.arg("--query")
|
|
.arg(&mutation_file)
|
|
.arg("--branch")
|
|
.arg("feature")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"PolicyRemote","age":41}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(changed["branch"], "feature");
|
|
assert_eq!(changed["affected_nodes"], 1);
|
|
|
|
let denied_merge = output_failure(
|
|
cli()
|
|
.arg("branch")
|
|
.arg("merge")
|
|
.arg("--config")
|
|
.arg(&client_config)
|
|
.arg("feature")
|
|
.arg("--into")
|
|
.arg("main")
|
|
.arg("--json"),
|
|
);
|
|
let denied_merge_stderr = String::from_utf8(denied_merge.stderr).unwrap();
|
|
assert!(denied_merge_stderr.contains("policy denied action 'branch_merge'"));
|
|
|
|
let merged = parse_stdout_json(&output_success(
|
|
cli()
|
|
.env("POLICY_TEST_TOKEN", "admin-token")
|
|
.arg("branch")
|
|
.arg("merge")
|
|
.arg("--config")
|
|
.arg(&client_config)
|
|
.arg("feature")
|
|
.arg("--into")
|
|
.arg("main")
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(merged["target"], "main");
|
|
|
|
let verify = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("read")
|
|
.arg("--config")
|
|
.arg(&client_config)
|
|
.arg("--query")
|
|
.arg(fixture("test.gq"))
|
|
.arg("--name")
|
|
.arg("get_person")
|
|
.arg("--params")
|
|
.arg(r#"{"name":"PolicyRemote"}"#)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(verify["row_count"], 1);
|
|
assert_eq!(verify["rows"][0]["p.name"], "PolicyRemote");
|
|
}
|
|
|
|
// ─── MR-668 PR 8 — omnigraph graphs end-to-end ──────────────────────────────
|
|
|
|
/// Multi-graph server + CLI `omnigraph graphs list` / `create` end-to-end.
|
|
///
|
|
/// Steps:
|
|
/// 1. Init a graph `alpha` on disk and write an `omnigraph.yaml`
|
|
/// whose `graphs:` map references it.
|
|
/// 2. Spawn the server with `--config <yaml>`.
|
|
/// 3. `omnigraph graphs list` — expect to see `alpha`.
|
|
/// 4. `omnigraph graphs create --graph-id beta --schema ...` —
|
|
/// expect 201 and stdout reflecting the new graph.
|
|
/// 5. `omnigraph graphs list` again — expect both `alpha` and `beta`.
|
|
///
|
|
/// Ignored by default — spawning servers needs loopback socket
|
|
/// permissions some sandboxes lack.
|
|
#[test]
|
|
#[ignore = "requires loopback socket permissions in sandboxed runners"]
|
|
fn graphs_list_and_create_against_multi_graph_server() {
|
|
let cfg_dir = tempfile::tempdir().unwrap();
|
|
let schema_path = fixture("test.pg");
|
|
|
|
// Init `alpha` on disk.
|
|
let alpha_uri = cfg_dir.path().join("alpha.omni");
|
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
|
Omnigraph::init(
|
|
alpha_uri.to_str().unwrap(),
|
|
&fs::read_to_string(&schema_path).unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
// Server config with `graphs:` map and no `server.graph` selector
|
|
// — multi mode (rule 4 of the inference matrix).
|
|
let server_config_path = cfg_dir.path().join("omnigraph.yaml");
|
|
fs::write(
|
|
&server_config_path,
|
|
format!(
|
|
"\
|
|
graphs:
|
|
alpha:
|
|
uri: {}
|
|
",
|
|
yaml_string(&alpha_uri.to_string_lossy())
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let server = spawn_server_with_config(&server_config_path);
|
|
|
|
// Client config — the CLI's `--target dev` resolves to `server.base_url`.
|
|
let client_config_path = cfg_dir.path().join("client.yaml");
|
|
fs::write(
|
|
&client_config_path,
|
|
format!(
|
|
"\
|
|
graphs:
|
|
dev:
|
|
uri: {}
|
|
cli:
|
|
graph: dev
|
|
",
|
|
yaml_string(&server.base_url)
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
// 1. `graphs list` lists `alpha`.
|
|
let payload = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("graphs")
|
|
.arg("list")
|
|
.arg("--config")
|
|
.arg(&client_config_path)
|
|
.arg("--json"),
|
|
));
|
|
let initial_ids: Vec<&str> = payload["graphs"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|g| g["graph_id"].as_str().unwrap())
|
|
.collect();
|
|
assert_eq!(initial_ids, vec!["alpha"]);
|
|
|
|
// 2. `graphs create` adds `beta`.
|
|
let beta_uri = cfg_dir.path().join("beta.omni");
|
|
let created = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("graphs")
|
|
.arg("create")
|
|
.arg("--config")
|
|
.arg(&client_config_path)
|
|
.arg("--graph-id")
|
|
.arg("beta")
|
|
.arg("--graph-uri")
|
|
.arg(beta_uri.to_str().unwrap())
|
|
.arg("--schema")
|
|
.arg(&schema_path)
|
|
.arg("--json"),
|
|
));
|
|
assert_eq!(created["graph_id"], "beta");
|
|
|
|
// 3. `graphs list` now lists both, sorted alphabetically.
|
|
let payload = parse_stdout_json(&output_success(
|
|
cli()
|
|
.arg("graphs")
|
|
.arg("list")
|
|
.arg("--config")
|
|
.arg(&client_config_path)
|
|
.arg("--json"),
|
|
));
|
|
let final_ids: Vec<&str> = payload["graphs"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|g| g["graph_id"].as_str().unwrap())
|
|
.collect();
|
|
assert_eq!(final_ids, vec!["alpha", "beta"]);
|
|
|
|
// 4. The new graph is reachable via its cluster snapshot route.
|
|
let client = Client::new();
|
|
let snap_status = client
|
|
.get(format!("{}/graphs/beta/snapshot?branch=main", server.base_url))
|
|
.send()
|
|
.unwrap()
|
|
.status();
|
|
assert_eq!(snap_status.as_u16(), 200);
|
|
|
|
drop(server);
|
|
}
|