feat(cli): no-default-graph errors list candidate graphs (RFC-011 D7) (#245)

When a server/cluster scope resolves with no --graph and no default_graph, the CLI auto-uses a sole graph (cluster) or errors listing the candidate graph ids (cluster catalog; multi-graph server via best-effort GET /graphs), never a silent pick. GraphClient::resolve becomes async; flat/single-graph servers and happy paths are unaffected.
This commit is contained in:
Andrew Altshuler 2026-06-15 15:48:29 +03:00 committed by GitHub
parent b395757e21
commit 1bc0ea6b51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 262 additions and 62 deletions

View file

@ -1006,21 +1006,80 @@ fn optimize_unknown_cluster_graph_id_errors() {
}
#[test]
fn cluster_without_graph_demands_a_graph_selector() {
// A cluster holds many graphs; `--cluster` alone can't pick one. The scope
// resolver demands `--graph <id>` (replacing the old `--cluster-graph`
// requirement) before it ever touches cluster state.
fn optimize_auto_uses_the_sole_cluster_graph() {
// RFC-011 D7: a cluster with exactly one applied graph needs no --graph —
// the resolver enumerates the catalog and uses the only candidate.
let temp = applied_knowledge_cluster();
let out = output_success(
cli()
.arg("optimize")
.arg("--cluster")
.arg(temp.path())
.arg("--json"),
);
assert!(
parse_stdout_json(&out)["tables"].as_array().is_some(),
"optimize should auto-resolve the sole cluster graph"
);
}
/// Stand up an applied cluster with two graphs (`knowledge`, `archive`).
fn applied_two_graph_cluster() -> tempfile::TempDir {
let temp = tempdir().unwrap();
let root = temp.path();
fs::write(
root.join("people.pg"),
"node Person {\n name: String @key\n age: I32?\n}\n",
)
.unwrap();
fs::write(root.join("base.policy.yaml"), "rules: []\n").unwrap();
fs::write(
root.join("cluster.yaml"),
r#"
version: 1
metadata:
name: two-graph
state:
backend: cluster
lock: true
graphs:
knowledge:
schema: ./people.pg
archive:
schema: ./people.pg
policies:
base:
file: ./base.policy.yaml
applies_to: [knowledge, archive]
"#,
)
.unwrap();
init_named_cluster_graph(root, "knowledge", "people.pg");
init_named_cluster_graph(root, "archive", "people.pg");
assert_eq!(cluster_json(root, "import")["ok"], true);
assert_eq!(cluster_json(root, "apply")["converged"], true);
temp
}
#[test]
fn optimize_on_multi_graph_cluster_without_graph_lists_candidates() {
// RFC-011 D7: >1 graph and no --graph → error naming every candidate,
// never an auto-pick.
let temp = applied_two_graph_cluster();
let out = output_failure(
cli()
.arg("optimize")
.arg("--cluster")
.arg(".")
.arg(temp.path())
.arg("--json"),
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--graph <id>"),
"expected --cluster to demand --graph; got: {stderr}"
stderr.contains("2 graphs")
&& stderr.contains("archive")
&& stderr.contains("knowledge")
&& stderr.contains("--graph <id>"),
"expected a candidate-listing error; got: {stderr}"
);
}

View file

@ -1136,5 +1136,27 @@ auth:
.collect();
assert_eq!(ids, vec!["alpha"]);
// RFC-011 D7: addressing the multi-graph server via `--server <url>` with no
// `--graph` errors and lists the candidate graphs (the resolver probes
// GET /graphs; the default-env token authorizes it).
let no_graph = cli()
.env("OMNIGRAPH_BEARER_TOKEN", "admin-token")
.arg("query")
.arg("--server")
.arg(&server.base_url)
.arg("-e")
.arg("query q { match { $p: Person { name: \"x\" } } return { $p.name } }")
.output()
.unwrap();
assert!(
!no_graph.status.success(),
"multi-graph server with no --graph must error"
);
let stderr = String::from_utf8_lossy(&no_graph.stderr);
assert!(
stderr.contains("alpha") && stderr.contains("--graph <id>"),
"expected a candidate-listing error naming alpha; got: {stderr}"
);
drop(server);
}