mod common;
use assert_cmd::Command;
use common::sqry_bin;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
fn build_indexed_workspace() -> TempDir {
let tmp = TempDir::new().expect("tempdir");
let root = tmp.path();
fs::create_dir_all(root.join("src")).expect("mkdir src");
fs::write(
root.join("src/lib.rs"),
r#"
pub fn func_alpha() -> u32 { 1 }
pub fn func_beta() -> u32 { 2 }
pub fn func_gamma() -> u32 { 3 }
"#,
)
.expect("write lib.rs");
fs::write(
root.join("src/extra.rs"),
r#"
pub fn other_function() -> u32 { 10 }
"#,
)
.expect("write extra.rs");
Command::new(sqry_bin())
.arg("index")
.arg(root)
.env("NO_COLOR", "1")
.assert()
.success();
tmp
}
#[test]
fn cli_query_uses_filesystem_acquirer_for_existing_graph() {
let tmp = build_indexed_workspace();
Command::new(sqry_bin())
.arg("--semantic")
.arg("query")
.arg("name:func_alpha")
.arg(tmp.path())
.env("NO_COLOR", "1")
.assert()
.success()
.stdout(predicate::str::contains("func_alpha"));
}
#[test]
fn cli_query_from_subdir_preserves_ancestor_scope_filter() {
let tmp = build_indexed_workspace();
let subdir = tmp.path().join("src");
Command::new(sqry_bin())
.arg("--semantic")
.arg("query")
.arg("name:func_alpha")
.arg(&subdir)
.env("NO_COLOR", "1")
.assert()
.success()
.stdout(predicate::str::contains("func_alpha"))
.stderr(predicate::str::contains("filtered to"));
}
#[test]
fn cli_query_file_scope_preserves_exact_file_filter() {
let tmp = build_indexed_workspace();
let file_path = tmp.path().join("src/lib.rs");
Command::new(sqry_bin())
.arg("--semantic")
.arg("query")
.arg("kind:function")
.arg(&file_path)
.env("NO_COLOR", "1")
.assert()
.success()
.stdout(predicate::str::contains("func_alpha"))
.stdout(predicate::str::contains("other_function").not());
}
#[test]
fn cli_invalid_path_rejected_before_graph_load() {
let tmp = TempDir::new().expect("tempdir");
let bogus = tmp.path().join("does/not/exist");
let output = Command::new(sqry_bin())
.arg("query")
.arg("kind:function")
.arg(&bogus)
.env("NO_COLOR", "1")
.output()
.expect("run sqry");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"non-existent path must fail (stderr={stderr})"
);
assert!(
stderr.contains("invalid path") || stderr.to_lowercase().contains("does not exist"),
"expected invalid-path diagnostic, got: {stderr}"
);
assert!(
!stderr.contains("Used index") && !stderr.contains("Using index from"),
"no `Used index` line should appear when path validation rejects the request: {stderr}"
);
}
#[test]
fn cli_text_mode_does_not_require_graph() {
let tmp = TempDir::new().expect("tempdir");
let root = tmp.path();
fs::create_dir_all(root.join("src")).expect("mkdir src");
fs::write(
root.join("src/lib.rs"),
"fn alpha() {}\nfn lookup_needle() {}\n",
)
.expect("write lib.rs");
let output = Command::new(sqry_bin())
.arg("--text")
.arg("query")
.arg("needle")
.arg(root)
.env("NO_COLOR", "1")
.output()
.expect("run sqry");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"--text on an unindexed directory must succeed; stderr={stderr}, stdout={stdout}"
);
assert!(
stdout.contains("lookup_needle") || stdout.contains("needle"),
"expected text match for `needle`, got stdout={stdout}"
);
assert!(
!stderr.contains("No graph found") && !stderr.contains("Run `sqry index"),
"text mode must not require a graph; stderr={stderr}"
);
}
#[test]
fn cli_hybrid_mode_executes_against_provider_acquired_graph() {
let tmp = build_indexed_workspace();
Command::new(sqry_bin())
.arg("query")
.arg("name:func_alpha")
.arg(tmp.path())
.env("NO_COLOR", "1")
.assert()
.success()
.stdout(predicate::str::contains("func_alpha"));
}
#[test]
fn cli_invalid_path_takes_precedence_over_invalid_query() {
let tmp = TempDir::new().expect("tempdir");
let bogus = tmp.path().join("does/not/exist");
let output = Command::new(sqry_bin())
.arg("query")
.arg("(kind:invalid_kind")
.arg(&bogus)
.env("NO_COLOR", "1")
.output()
.expect("run sqry");
let stderr = String::from_utf8_lossy(&output.stderr);
let code = output.status.code();
assert!(
!output.status.success(),
"invalid path + invalid query must fail (stderr={stderr})"
);
assert_eq!(
code,
Some(1),
"invalid path must exit 1 (path beats query); stderr={stderr}"
);
assert!(
stderr.contains("invalid path") || stderr.to_lowercase().contains("does not exist"),
"expected invalid-path diagnostic to win, got: {stderr}"
);
assert!(
!stderr.contains("sqry::parse") && !stderr.contains("sqry::validation"),
"no parse/validation diagnostic should appear when path is invalid: {stderr}"
);
}
#[test]
fn cli_invalid_query_reported_as_parse_error_when_path_is_unindexed() {
let tmp = TempDir::new().expect("tempdir");
fs::create_dir_all(tmp.path().join("src")).expect("mkdir src");
fs::write(tmp.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write");
let output = Command::new(sqry_bin())
.arg("query")
.arg("(kind:function") .arg(tmp.path())
.env("NO_COLOR", "1")
.output()
.expect("run sqry");
let stderr = String::from_utf8_lossy(&output.stderr);
let code = output.status.code();
assert!(
!output.status.success(),
"invalid query must fail (stderr={stderr})"
);
assert_eq!(
code,
Some(2),
"invalid query must exit 2 (parse error), not 1 (no-graph); stderr={stderr}"
);
assert!(
stderr.contains("sqry::parse") || stderr.contains("Unmatched"),
"expected parse-error diagnostic, got: {stderr}"
);
assert!(
!stderr.contains("No graph found") && !stderr.contains("Run `sqry index"),
"parse probe must short-circuit graph acquisition: {stderr}"
);
}
#[test]
fn cli_text_mode_succeeds_with_incompatible_graph_manifest() {
let workspace = TempDir::new().expect("tempdir");
let root = workspace.path();
fs::create_dir_all(root.join("src")).expect("mkdir src");
fs::write(root.join("src/lib.rs"), "pub fn func_alpha() {}\n").expect("write lib.rs");
let graph_dir = root.join(".sqry/graph");
fs::create_dir_all(&graph_dir).expect("mkdir .sqry/graph");
let manifest = serde_json::json!({
"schema_version": 1,
"snapshot_format_version": 2,
"built_at": "2026-05-08T00:00:00+00:00",
"root_path": root.to_string_lossy(),
"node_count": 0,
"edge_count": 0,
"snapshot_sha256": "0".repeat(64),
"build_provenance": {
"sqry_version": "13.0.0",
"build_timestamp": "2026-05-08T00:00:00+00:00",
"build_command": "cli:index",
},
"plugin_selection": {
"active_plugin_ids": ["nonexistent-lang-plugin"]
}
});
fs::write(
graph_dir.join("manifest.json"),
serde_json::to_string_pretty(&manifest).expect("manifest json"),
)
.expect("write manifest.json");
let output = Command::new(sqry_bin())
.arg("--text")
.arg("query")
.arg("func_alpha")
.arg(root)
.arg("--limit")
.arg("1")
.env("NO_COLOR", "1")
.output()
.expect("sqry query --text should run");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"text-only mode must succeed even when persisted plugin selection is incompatible. stderr={stderr} stdout={stdout}"
);
assert!(
stdout.contains("func_alpha"),
"expected text match for func_alpha; got stdout={stdout} stderr={stderr}"
);
assert!(
!stderr.contains("unknown plugin ids"),
"text mode must not resolve persisted plugin selection; stderr={stderr}"
);
}
#[test]
fn cli_invalid_path_rejected_before_pipeline_dispatch() {
let tmp = TempDir::new().expect("tempdir");
let bogus = tmp.path().join("does/not/exist");
let output = Command::new(sqry_bin())
.arg("query")
.arg("kind:function | count")
.arg(&bogus)
.env("NO_COLOR", "1")
.output()
.expect("run sqry");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"pipeline against non-existent path must fail; stderr={stderr}"
);
assert!(
stderr.contains("invalid path") || stderr.to_lowercase().contains("does not exist"),
"expected invalid-path diagnostic before pipeline dispatch, got: {stderr}"
);
}
#[test]
fn cli_query_unknown_plugin_id_returns_incompatible_graph() {
let tmp = build_indexed_workspace();
let manifest_path = tmp.path().join(".sqry/graph/manifest.json");
let manifest_bytes = fs::read(&manifest_path).expect("read manifest");
let mut manifest_json: serde_json::Value =
serde_json::from_slice(&manifest_bytes).expect("parse manifest");
let plugin_section = manifest_json
.get_mut("plugin_selection")
.expect("manifest must record plugin_selection after sqry index");
let active_ids = plugin_section
.get_mut("active_plugin_ids")
.and_then(|v| v.as_array_mut())
.expect("active_plugin_ids must be an array");
active_ids.push(serde_json::Value::String(
"sga07-fake-plugin-that-does-not-exist".to_string(),
));
fs::write(
&manifest_path,
serde_json::to_vec_pretty(&manifest_json).expect("serialize manifest"),
)
.expect("write manifest");
let output = Command::new(sqry_bin())
.arg("--semantic")
.arg("query")
.arg("name:func_alpha")
.arg(tmp.path())
.env("NO_COLOR", "1")
.output()
.expect("run sqry");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!output.status.success(),
"unknown plugin id manifest must reject the query; stdout={stdout} stderr={stderr}"
);
let lower_stderr = stderr.to_lowercase();
assert!(
lower_stderr.contains("incompatible graph")
|| lower_stderr.contains("unknown plugin")
|| stderr.contains("sga07-fake-plugin-that-does-not-exist"),
"expected IncompatibleGraph diagnostic surface, got stderr={stderr}"
);
}
#[test]
fn cli_query_json_output_schema_unchanged() {
let tmp = build_indexed_workspace();
let output = Command::new(sqry_bin())
.arg("--json")
.arg("--semantic")
.arg("query")
.arg("name:func_alpha")
.arg(tmp.path())
.env("NO_COLOR", "1")
.output()
.expect("run sqry --json query");
assert!(
output.status.success(),
"sqry --json query must succeed; stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("CLI --json output must be parseable: {e}; stdout={stdout}"));
assert!(
parsed.is_object(),
"CLI --json output must be a JSON object; got={parsed}"
);
let obj = parsed.as_object().unwrap();
for required_key in ["query", "results", "stats"] {
assert!(
obj.contains_key(required_key),
"CLI --json output MUST keep the `{required_key}` top-level key; got keys={:?}",
obj.keys().collect::<Vec<_>>()
);
}
assert!(
obj.get("results")
.map(serde_json::Value::is_array)
.unwrap_or(false),
"`results` must be an array",
);
assert!(
stdout.contains("func_alpha"),
"CLI --json query must still surface func_alpha; stdout={stdout}"
);
}