//! Stored-query commands and alias resolution. //! Moved verbatim from tests/cli.rs in the modularization. use serde_json::Value; use tempfile::tempdir; mod support; use support::*; #[test] fn query_check_alias_matches_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 lint_output = output_success( cli() .arg("query") .arg("lint") .arg("--query") .arg(&query_path) .arg("--schema") .arg(&schema_path) .arg("--json"), ); let check_output = output_success( cli() .arg("query") .arg("check") .arg("--query") .arg(&query_path) .arg("--schema") .arg(&schema_path) .arg("--json"), ); assert_eq!(stdout_string(&lint_output), stdout_string(&check_output)); } #[test] fn read_alias_from_yaml_config_runs_with_kv_output() { let temp = tempdir().unwrap(); let graph = graph_path(temp.path()); let config = temp.path().join("omnigraph.yaml"); let query = temp.path().join("aliases.gq"); init_graph(&graph); load_fixture(&graph); write_query_file( &query, &std::fs::read_to_string(fixture("test.gq")).unwrap(), ); write_config( &config, &format!( "{}aliases:\n owner:\n command: read\n query: aliases.gq\n name: get_person\n args: [name]\n format: kv\n", local_yaml_config(&graph) ), ); let output = output_success( cli() .arg("read") .arg("--config") .arg(&config) .arg("--alias") .arg("owner") .arg("Alice"), ); let stdout = stdout_string(&output); assert!(stdout.contains("row 1")); assert!(stdout.contains("p.name: Alice")); } #[test] fn read_alias_uses_alias_target_without_cli_default_and_accepts_url_like_arg() { let temp = tempdir().unwrap(); let graph = graph_path(temp.path()); let config = temp.path().join("omnigraph.yaml"); let query = temp.path().join("aliases.gq"); let data = temp.path().join("url-like.jsonl"); init_graph(&graph); write_jsonl( &data, r#"{"type":"Person","data":{"name":"https://example.com","age":30}}"#, ); output_success( cli() .arg("load") .arg("--mode") .arg("overwrite") .arg("--data") .arg(&data) .arg(&graph), ); write_query_file( &query, &std::fs::read_to_string(fixture("test.gq")).unwrap(), ); write_config( &config, &format!( "graphs:\n local:\n uri: '{}'\nquery:\n roots:\n - .\npolicy: {{}}\naliases:\n owner:\n command: read\n query: aliases.gq\n name: get_person\n args: [name]\n graph: local\n format: kv\n", graph.to_string_lossy() ), ); let output = output_success( cli() .arg("read") .arg("--config") .arg(&config) .arg("--alias") .arg("owner") .arg("https://example.com"), ); let stdout = stdout_string(&output); assert!(stdout.contains("row 1")); assert!(stdout.contains("p.name: https://example.com")); } #[test] fn change_alias_from_yaml_config_persists_changes() { let temp = tempdir().unwrap(); let graph = graph_path(temp.path()); let config = temp.path().join("omnigraph.yaml"); let query = temp.path().join("mutations.gq"); init_graph(&graph); load_fixture(&graph); write_query_file( &query, r#" query insert_person($name: String, $age: I32) { insert Person { name: $name, age: $age } } "#, ); write_config( &config, &format!( "{}aliases:\n add_person:\n command: change\n query: mutations.gq\n name: insert_person\n args: [name, age]\n", local_yaml_config(&graph) ), ); let output = output_success( cli() .arg("change") .arg("--config") .arg(&config) .arg("--alias") .arg("add_person") .arg("Eve") .arg("29") .arg("--json"), ); let payload: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(payload["affected_nodes"], 1); 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); } #[test] fn queries_validate_exits_zero_on_clean_registry() { let graph = SystemGraph::loaded(); graph.write_query( "find_person.gq", "query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }", ); let config = graph.write_config( "omnigraph.yaml", &queries_test_config( &graph.path().to_string_lossy(), "find_person", "find_person.gq", ), ); let output = output_success( cli() .arg("queries") .arg("validate") .arg("--config") .arg(&config), ); let stdout = stdout_string(&output); assert!(stdout.contains("OK"), "stdout:\n{stdout}"); } #[test] fn queries_validate_exits_nonzero_on_type_broken_query() { let graph = SystemGraph::loaded(); // `Widget` is not in the fixture schema. graph.write_query( "ghost.gq", "query ghost() { match { $w: Widget } return { $w.name } }", ); let config = graph.write_config( "omnigraph.yaml", &queries_test_config(&graph.path().to_string_lossy(), "ghost", "ghost.gq"), ); let output = output_failure( cli() .arg("queries") .arg("validate") .arg("--config") .arg(&config), ); let stdout = stdout_string(&output); assert!( stdout.contains("ghost"), "validation should name the broken query; stdout:\n{stdout}" ); } #[test] fn queries_list_prints_registered_query() { let graph = SystemGraph::loaded(); graph.write_query( "find_person.gq", "query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }", ); // Exposed with an explicit tool name so the list shows the MCP suffix. let config = graph.write_config( "omnigraph.yaml", &format!( concat!( "graphs:\n", " local:\n", " uri: '{}'\n", " queries:\n", " find_person:\n", " file: ./find_person.gq\n", " mcp: {{ expose: true, tool_name: lookup_person }}\n", "cli:\n", " graph: local\n", "policy: {{}}\n", ), graph.path().to_string_lossy().replace('\'', "''") ), ); let output = output_success( cli() .arg("queries") .arg("list") .arg("--config") .arg(&config), ); let stdout = stdout_string(&output); assert!(stdout.contains("find_person"), "stdout:\n{stdout}"); assert!( stdout.contains("$name: String"), "list should show typed params; stdout:\n{stdout}" ); assert!( stdout.contains("[mcp: lookup_person]"), "list should show the MCP tool name for exposed queries; stdout:\n{stdout}" ); } #[test] fn queries_list_requires_graph_selection_for_per_graph_only_registries() { let graph = SystemGraph::loaded(); graph.write_query( "find_person.gq", "query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }", ); let config = graph.write_config( "omnigraph.yaml", &format!( concat!( "graphs:\n", " local:\n", " uri: '{}'\n", " queries:\n", " find_person:\n", " file: ./find_person.gq\n", "policy: {{}}\n", ), graph.path().to_string_lossy().replace('\'', "''") ), ); let output = output_failure( cli() .arg("queries") .arg("list") .arg("--config") .arg(&config), ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("local") && stderr.contains("--target local"), "error must name the graph and give a concrete selection hint; stderr:\n{stderr}" ); } #[test] fn queries_list_without_graph_selection_lists_top_level_registry() { let graph = SystemGraph::loaded(); graph.write_query( "top_find.gq", "query top_find($name: String) { match { $p: Person { name: $name } } return { $p.age } }", ); let config = graph.write_config( "omnigraph.yaml", concat!( "queries:\n", " top_find:\n", " file: ./top_find.gq\n", "policy: {}\n", ), ); let output = output_success( cli() .arg("queries") .arg("list") .arg("--config") .arg(&config), ); let stdout = stdout_string(&output); assert!(stdout.contains("top_find"), "stdout:\n{stdout}"); } #[test] fn queries_list_unknown_target_errors() { // `queries list` opens no graph URI, so unknown-graph validation can't ride // along on URI resolution the way it does for every other command. An // unknown `--target` must still error (naming the graph) instead of // silently falling back to the top-level registry and showing the wrong // (or empty) catalog. let graph = SystemGraph::loaded(); graph.write_query( "find_person.gq", "query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }", ); let config = graph.write_config( "omnigraph.yaml", &queries_test_config( &graph.path().to_string_lossy(), "find_person", "find_person.gq", ), ); let output = output_failure( cli() .arg("queries") .arg("list") .arg("--target") .arg("nonexistent") .arg("--config") .arg(&config), ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("nonexistent"), "error must name the unknown graph; stderr:\n{stderr}" ); } #[test] fn queries_commands_reject_named_graph_with_populated_top_level_block() { // A named graph (here via `cli.graph`) uses its own `graphs.` block, // so a populated top-level `queries:` block would be silently ignored — a // config the server REFUSES to boot. `queries validate`/`list` must reject // it too (matching boot) instead of validating/listing the per-graph block // and giving a false green. let graph = SystemGraph::loaded(); graph.write_query( "find_person.gq", "query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }", ); let config = graph.write_config( "omnigraph.yaml", &format!( concat!( "graphs:\n", " local:\n", " uri: '{}'\n", " queries:\n", " find_person:\n", " file: ./find_person.gq\n", "cli:\n", " graph: local\n", "queries:\n", // populated top-level block: the coherence violation " legacy:\n", " file: ./legacy.gq\n", "policy: {{}}\n", ), graph.path().to_string_lossy().replace('\'', "''") ), ); // Both resolve `local` from cli.graph (no positional URI), so both must // error and name the graph + the ignored block — like server boot does. for sub in ["validate", "list"] { let output = output_failure(cli().arg("queries").arg(sub).arg("--config").arg(&config)); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("local") && stderr.contains("queries"), "`queries {sub}` must reject a named graph with a populated top-level block; stderr:\n{stderr}" ); } } #[test] fn queries_validate_exits_nonzero_on_duplicate_tool_name() { // Two exposed queries claiming one MCP tool name is a load-time // collision — `queries validate` must fail (offline, before the engine // opens) and name both queries plus the contested tool. let graph = SystemGraph::loaded(); graph.write_query( "a.gq", "query a() { match { $p: Person } return { $p.name } }", ); graph.write_query( "b.gq", "query b() { match { $p: Person } return { $p.name } }", ); let config = graph.write_config( "omnigraph.yaml", &format!( concat!( "graphs:\n", " local:\n", " uri: '{}'\n", " queries:\n", " a:\n", " file: ./a.gq\n", " mcp: {{ expose: true, tool_name: dup }}\n", " b:\n", " file: ./b.gq\n", " mcp: {{ expose: true, tool_name: dup }}\n", "cli:\n", " graph: local\n", "policy: {{}}\n", ), graph.path().to_string_lossy().replace('\'', "''") ), ); let output = output_failure( cli() .arg("queries") .arg("validate") .arg("--config") .arg(&config), ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("dup") && stderr.contains("'a'") && stderr.contains("'b'"), "duplicate tool name should be reported naming both queries; stderr:\n{stderr}" ); } #[test] fn queries_validate_positional_uri_ignores_default_graph() { // A positional URI is anonymous → the schema AND the registry both come // from top-level, even when `cli.graph` names a graph whose per-graph // queries would fail. Pins that the URI and registry can't diverge. let graph = SystemGraph::loaded(); graph.write_query( "clean.gq", "query clean($name: String) { match { $p: Person { name: $name } } return { $p.age } }", ); // `Widget` is not in the fixture schema — the default graph's per-graph // query would break validate if it were (wrongly) selected. graph.write_query( "broken.gq", "query broken() { match { $w: Widget } return { $w.name } }", ); let config = graph.write_config( "omnigraph.yaml", concat!( "cli:\n graph: prod\n", "graphs:\n", " prod:\n", " uri: /nonexistent-prod.omni\n", " queries:\n", " broken:\n", " file: ./broken.gq\n", "queries:\n", " clean:\n", " file: ./clean.gq\n", "policy: {}\n", ), ); // Positional URI = the real loaded graph; selection is anonymous, so the // CLEAN top-level registry validates (not prod's broken one). let output = output_success( cli() .arg("queries") .arg("validate") .arg(graph.path()) .arg("--config") .arg(&config), ); let stdout = stdout_string(&output); assert!( stdout.contains("OK"), "positional URI must validate the top-level registry, not the cli.graph default; stdout:\n{stdout}" ); }