mod common;
use assert_cmd::Command;
use common::sqry_bin;
use serde_json::Value;
use std::fs;
use tempfile::TempDir;
fn build_ambiguous_go_fixture() -> TempDir {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("main.go"),
r#"package main
type SelectorSource struct {
NeedTags bool
}
var NeedTags = "package-scope shadow"
func useSelector(selector SelectorSource) bool {
if selector.NeedTags {
return true
}
return false
}
func unrelated() {
_ = NeedTags
}
"#,
)
.unwrap();
Command::new(sqry_bin())
.current_dir(&dir)
.args(["index", "."])
.assert()
.success();
dir
}
#[test]
fn impact_returns_ambiguous_envelope_for_bare_collision() {
let dir = build_ambiguous_go_fixture();
let assert = Command::new(sqry_bin())
.current_dir(&dir)
.args(["impact", "--json", "NeedTags"])
.assert()
.code(4);
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let envelope: Value =
serde_json::from_str(&stdout).expect("ambiguous envelope must be valid JSON");
let error = envelope
.get("error")
.expect("envelope wraps the ambiguous payload under `error`");
assert_eq!(error["code"], "sqry::ambiguous_symbol");
let message = error["message"].as_str().expect("message is a string");
assert!(
message.contains("NeedTags") && message.contains("ambiguous"),
"message must name the symbol and the ambiguity, got {message:?}"
);
assert_eq!(error["truncated"], Value::Bool(false));
let candidates = error["candidates"]
.as_array()
.expect("candidates is an array");
assert!(
candidates.len() >= 2,
"expected at least two candidates, got {}",
candidates.len()
);
for candidate in candidates {
assert!(candidate.get("qualified_name").is_some());
assert!(candidate.get("kind").is_some());
assert!(candidate.get("file_path").is_some());
assert!(candidate.get("start_line").is_some());
assert!(candidate.get("start_column").is_some());
}
}
#[test]
fn impact_resolves_qualified_name_unambiguously() {
let dir = build_ambiguous_go_fixture();
let assert = Command::new(sqry_bin())
.current_dir(&dir)
.args(["impact", "--json", "main.SelectorSource.NeedTags"])
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let payload: Value = serde_json::from_str(&stdout).expect("success payload must be valid JSON");
assert_eq!(payload["symbol"], "main.SelectorSource.NeedTags");
assert!(payload.get("direct").is_some(), "direct[] must be present");
assert!(payload.get("stats").is_some(), "stats must be present");
}
#[test]
fn impact_surfaces_symbol_not_found_envelope() {
let dir = build_ambiguous_go_fixture();
let assert = Command::new(sqry_bin())
.current_dir(&dir)
.args(["impact", "--json", "DefinitelyNotASymbol_xyz"])
.assert()
.code(2);
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let envelope: Value = serde_json::from_str(&stdout).expect("not-found must be valid JSON");
assert_eq!(envelope["error"]["code"], "sqry::symbol_not_found");
}
#[test]
fn impact_human_output_lists_candidates_on_stderr() {
let dir = build_ambiguous_go_fixture();
let assert = Command::new(sqry_bin())
.current_dir(&dir)
.args(["impact", "NeedTags"])
.assert()
.code(4);
let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
assert!(
stderr.contains("ambiguous"),
"stderr must mention ambiguity, got {stderr:?}"
);
assert!(
stderr.contains("Candidates:"),
"stderr must list candidates, got {stderr:?}"
);
}