use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn scratch(label: &str) -> (PathBuf, PathBuf) {
let base = std::env::temp_dir();
let dir = base.join(format!("rq-e2e-{}-{label}", std::process::id()));
let db = base.join(format!("rq-e2e-{}-{label}.db", std::process::id()));
let _ = fs::remove_dir_all(&dir);
for suffix in ["", "-wal", "-shm"] {
let _ = fs::remove_file(format!("{}{suffix}", db.display()));
}
fs::create_dir_all(&dir).unwrap();
(dir, db)
}
fn rq(db: &Path, cwd: &Path, args: &[&str]) -> (bool, String) {
let out = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(args)
.current_dir(cwd)
.env("RQ_DB", db)
.env("RQ_WARM_DETACH", "0")
.output()
.expect("run rq");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
)
}
fn first_line(s: &str) -> &str {
s.lines().next().unwrap_or("")
}
fn warmed(db: &Path, cwd: &Path, query: &str) -> bool {
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["-v", query, "--no-record"])
.current_dir(cwd)
.env("RQ_DB", db)
.output()
.expect("run rq");
String::from_utf8_lossy(&run.stderr).contains("background warm")
}
fn git_init(dir: &Path) {
let _ = Command::new("git")
.arg("init")
.arg("-q")
.current_dir(dir)
.output();
}
fn git_init_commit(dir: &Path) {
git_init(dir);
let git = |args: &[&str]| {
let _ = Command::new("git").args(args).current_dir(dir).output();
};
git(&["add", "-A"]);
git(&[
"-c",
"user.email=t@e.st",
"-c",
"user.name=test",
"commit",
"-qm",
"init",
]);
}
#[test]
fn index_search_and_learn_through_the_cli() {
let (dir, db) = scratch("learn");
fs::write(dir.join("alpha.rb"), "class HandlerA\nend\n").unwrap();
fs::write(dir.join("beta.rb"), "class HandlerB\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index"]);
assert!(ok, "index failed: {out}");
assert!(out.contains("symbol"), "index output: {out}");
let (ok, out) = rq(&db, &dir, &["handler"]);
assert!(ok, "search failed: {out}");
assert!(
first_line(&out).contains("HandlerA"),
"search output: {out}"
);
let (ok, _) = rq(
&db,
&dir,
&["--record", "--file", "beta.rb", "--line", "1", "handler"],
);
assert!(ok, "record failed");
let (ok, out) = rq(&db, &dir, &["handler"]);
assert!(ok, "second search failed: {out}");
assert!(
first_line(&out).contains("HandlerB"),
"after learning, expected HandlerB first: {out}"
);
let (ok, out) = rq(&db, &dir, &["--status"]);
assert!(ok, "status failed: {out}");
assert!(out.contains("local:"), "status output: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_strong_match_suppresses_the_scattered_tail() {
let (dir, db) = scratch("gate");
fs::write(
dir.join("employees_controller.rb"),
"class EmployeesController\nend\n",
)
.unwrap();
fs::write(
dir.join("employee_status_controller.rb"),
"class EmployeeStatusController\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(
&db,
&dir,
&["employeescontroller", "--no-record", "--ndjson"],
);
assert!(out.contains("EmployeesController"), "exact kept: {out}");
assert!(
!out.contains("EmployeeStatusController"),
"scattered fuzzy dropped: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_wildcard_bridges_an_explicit_gap() {
let (dir, db) = scratch("wildcard");
fs::write(
dir.join("widget_alpha_bravo_controller.rb"),
"class WidgetAlphaBravoController\nend\n",
)
.unwrap();
fs::write(dir.join("gadget_service.rb"), "class GadgetService\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["widget*controller", "--no-record", "--ndjson"]);
assert!(ok, "wildcard search should match: {out}");
assert!(
out.contains("WidgetAlphaBravoController"),
"star bridges the gap: {out}"
);
assert!(!out.contains("GadgetService"), "non-match excluded: {out}");
let (matched, _) = rq(&db, &dir, &["widgetcontroller", "--no-record"]);
assert!(!matched, "plain fuzzy won't skip whole words");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn cold_index_builds_a_working_fuzzy_index() {
let (dir, db) = scratch("coldfts");
fs::write(dir.join("a.rb"), "class AlphaWidgetController\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index"]);
assert!(ok, "index failed: {out}");
let (ok, out) = rq(&db, &dir, &["widget", "--no-record"]);
assert!(ok, "fuzzy recall should find it: {out}");
assert!(out.contains("AlphaWidgetController"), "fts recall: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_compact_namespaced_class_is_found_by_its_leaf_name() {
let (dir, db) = scratch("namespaced");
fs::write(
dir.join("a.rb"),
"class My::Module::EmployeesController\n def index; end\nend\n",
)
.unwrap();
fs::write(
dir.join("b.rb"),
"class EmployeesController\n def show; end\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(
&db,
&dir,
&["employeescontroller", "--no-record", "--ndjson"],
);
assert!(ok, "search failed: {out}");
assert!(out.contains("a.rb"), "namespaced class kept: {out}");
assert!(out.contains("b.rb"), "top-level class kept: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_qualified_query_resolves_to_the_method_in_the_named_scope() {
let (dir, db) = scratch("qualified");
fs::write(
dir.join("a.rb"),
"module Foo\n class Bar\n def baz; end\n end\nend\n",
)
.unwrap();
fs::write(
dir.join("b.rb"),
"module Other\n class Bar\n def baz; end\n end\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["Foo::Bar#baz", "--no-record", "--ndjson"]);
assert!(ok, "search failed: {out}");
assert!(out.contains("a.rb"), "in-scope baz surfaces: {out}");
assert!(
!out.contains("b.rb"),
"out-of-scope baz is gated out: {out}"
);
let (ok, out) = rq(&db, &dir, &["Nope::Bar#baz", "--no-record", "--ndjson"]);
assert!(ok, "fallback search failed: {out}");
assert!(
out.contains("a.rb") && out.contains("b.rb"),
"no scope match falls back to all candidates: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn json_results_carry_the_definition_span() {
let (dir, db) = scratch("endline");
fs::write(
dir.join("a.rb"),
"class Widget\n def go\n 1\n end\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["Widget", "-l", "1", "--ndjson"]);
assert!(ok, "search failed: {out}");
assert!(out.contains("\"line\":1"), "start line present: {out}");
assert!(out.contains("\"end_line\":5"), "end line present: {out}");
assert!(
!out.contains("\"parent\":null"),
"no null fields in hit JSON: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn show_prints_the_body_when_confident_and_lists_when_not() {
let (dir, db) = scratch("show");
fs::write(
dir.join("a.rb"),
"class Widget\n def go\n 42\n end\nend\n",
)
.unwrap();
fs::write(dir.join("b.rb"), "class Thing\nend\nclass Thang\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["--show", "Widget", "-j", "--no-record"]);
assert!(ok, "show failed: {out}");
assert!(
out.contains("\"body\""),
"confident show carries body: {out}"
);
assert!(
out.contains("def go"),
"body has the definition source: {out}"
);
assert!(!out.contains("\"score\""), "score dropped from JSON: {out}");
assert!(out.contains("\"confidence\""), "confidence present: {out}");
let (_ok, out) = rq(&db, &dir, &["--show", "Th", "-j", "--no-record"]);
assert!(
!out.contains("\"body\""),
"ambiguous show prints no body: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_leading_kind_keyword_filters_like_dash_k() {
let (dir, db) = scratch("kindkw");
fs::write(
dir.join("a.rb"),
"class Widget\n def go; end\nend\nmodule Widget\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["class", "Widget", "--no-record", "--ndjson"]);
assert!(ok, "keyword search failed: {out}");
assert!(out.contains("\"kind\":\"class\""), "class kept: {out}");
assert!(
!out.contains("\"kind\":\"module\""),
"module filtered: {out}"
);
let (ok, out) = rq(&db, &dir, &["method", "go", "--no-record", "--ndjson"]);
assert!(ok, "method keyword failed: {out}");
assert!(out.contains("\"kind\":\"method\""), "method found: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_search_does_not_leak_another_indexed_repo() {
let (dir_a, db) = scratch("repo-a");
let dir_b = dir_a.with_file_name(format!("rq-e2e-{}-repo-b", std::process::id()));
let _ = fs::remove_dir_all(&dir_b);
fs::create_dir_all(&dir_b).unwrap();
fs::write(dir_a.join("a.rb"), "class Alpha\nend\n").unwrap();
fs::write(dir_b.join("b.rb"), "class Gadget\nend\n").unwrap();
rq(&db, &dir_a, &["--index"]);
rq(&db, &dir_b, &["--index"]);
let (ok, out) = rq(&db, &dir_a, &["Gadget", "--no-record", "--ndjson"]);
assert!(!ok, "no Gadget in repo A — should miss");
assert!(!out.contains("b.rb"), "must not leak repo B: {out}");
assert!(
out.contains("\"status\":\"no_match\""),
"reports no_match: {out}"
);
let (ok, out) = rq(
&db,
&dir_a,
&["Gadget", "--all-repos", "--no-record", "--ndjson"],
);
assert!(ok, "--all-repos should find Gadget in repo B: {out}");
assert!(out.contains("b.rb"), "cross-repo hit surfaces: {out}");
let (ok, out) = rq(&db, &dir_a, &["Gadget", "-a", "--no-record", "--ndjson"]);
assert!(ok, "-a should find Gadget in repo B: {out}");
assert!(out.contains("b.rb"), "cross-repo hit surfaces: {out}");
let _ = fs::remove_dir_all(&dir_a);
let _ = fs::remove_dir_all(&dir_b);
}
#[test]
fn a_clean_complete_repo_does_not_re_warm_on_search() {
let (dir, db) = scratch("nowarm");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
rq(&db, &dir, &["--index"]);
assert!(
!warmed(&db, &dir, "widget"),
"clean complete repo should not re-warm"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_tracked_edit_warms_but_a_new_untracked_file_does_not() {
let (dir, db) = scratch("dirty-check");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
rq(&db, &dir, &["--index"]);
fs::write(dir.join("a.rb"), "class Widget\n def go; end\nend\n").unwrap();
assert!(warmed(&db, &dir, "widget"), "tracked edit triggers a warm");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
fs::write(dir.join("b.rb"), "class Gadget\nend\n").unwrap();
assert!(
!warmed(&db, &dir, "widget"),
"a new untracked file does not trigger a warm (accepted tradeoff)"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn indexing_a_subdir_scopes_to_it_but_keeps_root_relative_paths() {
let (dir, db) = scratch("index-subdir");
fs::create_dir_all(dir.join("sub")).unwrap();
fs::create_dir_all(dir.join("other")).unwrap();
fs::write(dir.join("sub/a.rb"), "class InScope\nend\n").unwrap();
fs::write(dir.join("other/b.rb"), "class OutOfScope\nend\n").unwrap();
git_init_commit(&dir);
let (ok, out) = rq(&db, &dir, &["--index", "sub"]);
assert!(ok, "scoped index failed: {out}");
assert!(out.contains("subtree"), "subdir index is a seed: {out}");
assert!(
out.contains("1 files"),
"this run indexed exactly the one in-scope file: {out}"
);
let (ok, out) = rq(&db, &dir, &["inscope", "--no-record", "--ndjson"]);
assert!(ok, "search failed: {out}");
assert!(out.contains("InScope"), "in-scope class indexed: {out}");
assert!(
out.contains("\"file\":\"sub/a.rb\""),
"path is root-relative, not subdir-relative: {out}"
);
let (found, out) = rq(&db, &dir, &["outofscope", "--no-record", "--ndjson"]);
assert!(found, "warming finds the out-of-scope class: {out}");
assert!(out.contains("\"file\":\"other/b.rb\""), "warm hit: {out}");
let (_, status) = rq(&db, &dir, &["--status", "--ndjson"]);
assert!(
status.contains("\"files\":2"),
"warming persisted the rest of the repo: {status}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn empty_status_points_at_the_real_index_flag() {
let (dir, db) = scratch("empty-status");
let (ok, out) = rq(&db, &dir, &["--status"]);
assert!(ok, "status on an empty db should succeed: {out}");
assert!(out.contains("rq --index"), "hint names the flag: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn open_launches_and_records_the_pick() {
let (dir, db) = scratch("open");
fs::write(dir.join("user.rb"), "class User\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["--open", "user"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env("RQ_OPEN", "true")
.output()
.expect("run rq");
assert!(run.status.success(), "open should exit 0 via the launcher");
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["--open", "user", "--no-record"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env_remove("EDITOR")
.env_remove("VISUAL")
.env("PATH", "/nonexistent") .output()
.expect("run rq");
let printed = String::from_utf8_lossy(&run.stdout);
assert!(
printed.trim().ends_with("user.rb:1"),
"prints resolved path:line: {printed}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn drop_honors_json_output() {
let (dir, db) = scratch("drop-json");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["--drop", "--json"]);
assert!(ok, "drop --json failed: {out}");
assert!(out.trim_start().starts_with('{'), "json object: {out}");
assert!(out.contains("\"dropped\": true"), "reports dropped: {out}");
let (ok, out) = rq(&db, &dir, &["--drop", "--ndjson"]);
assert!(ok, "second drop should not error: {out}");
let line = out.lines().next().unwrap_or("");
assert!(
line.starts_with('{') && line.contains("\"dropped\":false"),
"ndjson dropped:false: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn drop_removes_a_repos_index() {
let (dir, db) = scratch("drop");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, before) = rq(&db, &dir, &["--status"]);
assert!(before.contains("symbol"), "indexed before drop: {before}");
let (ok, out) = rq(&db, &dir, &["--drop"]);
assert!(ok, "drop failed: {out}");
assert!(out.contains("dropped"), "drop confirms: {out}");
let (ok2, after) = rq(&db, &dir, &["--status"]);
assert!(ok2, "status after drop: {after}");
assert!(
!after.contains("symbol"),
"coverage gone after drop: {after}"
);
let (ok3, again) = rq(&db, &dir, &["--drop"]);
assert!(ok3, "second drop should not error: {again}");
assert!(again.contains("not indexed"), "idempotent message: {again}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn record_is_a_searchable_word_not_a_subcommand() {
let (dir, db) = scratch("disambig");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["record"]);
assert!(!ok, "no-match search should exit non-zero");
assert!(out.is_empty(), "expected no result lines, got: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn status_and_index_honor_json_output() {
let (dir, db) = scratch("ops-json");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index", "--json"]);
assert!(ok, "index --json failed: {out}");
assert!(
out.trim_start().starts_with('{'),
"index json object: {out}"
);
assert!(out.contains("\"files_added\""), "index run counts: {out}");
assert!(out.contains("\"repo\""), "index repo field: {out}");
let (ok, out) = rq(&db, &dir, &["--status", "--json"]);
assert!(ok, "status --json failed: {out}");
assert!(
out.trim_start().starts_with('['),
"status json array: {out}"
);
assert!(
out.contains("\"status\": \"complete\""),
"status field: {out}"
);
let (ok, out) = rq(&db, &dir, &["--status", "--ndjson"]);
assert!(ok, "status -J failed: {out}");
let line = out.lines().next().unwrap_or("");
assert!(
line.starts_with('{') && line.ends_with('}') && line.contains("\"repo\""),
"ndjson object per line: {out}"
);
rq(&db, &dir, &["--drop"]);
let (ok, out) = rq(&db, &dir, &["--status", "--json"]);
assert!(ok && out.trim() == "[]", "empty status json is []: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn json_and_ndjson_output() {
let (dir, db) = scratch("json");
fs::write(dir.join("alpha.rb"), "class HandlerA\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["handler", "--json"]);
assert!(ok, "json search failed: {out}");
assert!(
out.trim_start().starts_with('['),
"expected a JSON array: {out}"
);
assert!(out.contains("\"name\": \"HandlerA\""), "name field: {out}");
assert!(out.contains("\"file\": \"alpha.rb\""), "file field: {out}");
assert!(out.contains("\"repo\":"), "repo field: {out}");
assert!(
out.contains("\"signature\": \"class HandlerA\""),
"signature: {out}"
);
let (ok, out) = rq(&db, &dir, &["handler", "--ndjson"]);
assert!(ok, "ndjson search failed: {out}");
let first = out.lines().next().unwrap_or("");
assert!(
first.starts_with('{') && first.ends_with('}'),
"object per line: {out}"
);
assert!(
first.contains("\"name\":\"HandlerA\""),
"compact name: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn path_filter_restricts_results() {
let (dir, db) = scratch("path");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(
dir.join("app/services/widget.rb"),
"class WidgetService\nend\n",
)
.unwrap();
fs::write(dir.join("app/models/widget.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(
out.contains("app/models/widget.rb"),
"expected models hit: {out}"
);
assert!(
out.contains("app/services/widget.rb"),
"expected services hit: {out}"
);
let (ok, out) = rq(&db, &dir, &["widget", "--path", "app/services", "--ndjson"]);
assert!(ok, "path search failed: {out}");
assert!(
out.contains("app/services/widget.rb"),
"services hit kept: {out}"
);
assert!(
!out.contains("app/models/widget.rb"),
"models hit filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn path_filter_accepts_absolute_and_relative_paths() {
let (dir, db) = scratch("path-abs");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(
dir.join("app/services/widget.rb"),
"class WidgetService\nend\n",
)
.unwrap();
fs::write(dir.join("app/models/widget.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let abs = dir.join("app/services");
let abs = abs.to_str().unwrap();
for spec in [abs, "./app/services", "app/services"] {
let (ok, out) = rq(&db, &dir, &["widget", "--path", spec, "--ndjson"]);
assert!(ok, "path search failed for {spec:?}: {out}");
assert!(
out.contains("app/services/widget.rb"),
"services kept for {spec:?}: {out}"
);
assert!(
!out.contains("app/models/widget.rb"),
"models filtered for {spec:?}: {out}"
);
}
let (_, out) = rq(
&db,
&dir,
&["widget", "--path", "/nonexistent/elsewhere", "--ndjson"],
);
assert!(
!out.contains("\"file\":"),
"outside path yields no hits: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn limit_caps_the_number_of_results() {
let (dir, db) = scratch("limit");
let body: String = (0..12)
.map(|i| format!("class Handler{i}\nend\n"))
.collect();
fs::write(dir.join("a.rb"), body).unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["handler", "--limit", "1", "--ndjson"]);
assert!(ok, "limited search failed: {out}");
assert_eq!(out.lines().count(), 1, "expected exactly one result: {out}");
let (ok, out) = rq(&db, &dir, &["handler", "--ndjson"]);
assert!(ok, "default search failed: {out}");
assert_eq!(out.lines().count(), 10, "default caps at 10: {out}");
let (ok, out) = rq(&db, &dir, &["handler", "--limit", "0", "--ndjson"]);
assert!(ok, "unlimited search failed: {out}");
assert_eq!(out.lines().count(), 12, "expected every match: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn index_a_subset_of_a_repo() {
let (dir, db) = scratch("subset");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(dir.join("app/services/charge.rb"), "class Charge\nend\n").unwrap();
fs::write(dir.join("app/models/account.rb"), "class Account\nend\n").unwrap();
git_init(&dir);
let (ok, out) = rq(&db, &dir, &["--index", "--path", "app/services", "-J"]);
assert!(ok, "subset index failed: {out}");
assert!(out.contains("\"scope\":\"subtree\""), "seed marker: {out}");
assert!(out.contains("\"files\":1"), "seeded one file: {out}");
let (_, status) = rq(&db, &dir, &["--status", "--ndjson"]);
assert!(
status.contains("\"status\":\"warming\""),
"a seed leaves coverage warming: {status}"
);
let (ok, out) = rq(&db, &dir, &["charge", "--ndjson"]);
assert!(ok, "charge search failed: {out}");
assert!(
out.contains("\"file\":\"app/services/charge.rb\""),
"subset hit: {out}"
);
let (ok, out) = rq(&db, &dir, &["account", "--ndjson"]);
assert!(ok, "warming finds the unseeded symbol: {out}");
assert!(
out.contains("\"file\":\"app/models/account.rb\""),
"warm hit: {out}"
);
let (_, status) = rq(&db, &dir, &["--status", "--ndjson"]);
assert!(
status.contains("\"files\":2"),
"warming persisted the rest: {status}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn positional_paths_filter_like_rg() {
let (dir, db) = scratch("pospath");
fs::create_dir_all(dir.join("app/services")).unwrap();
fs::create_dir_all(dir.join("app/models")).unwrap();
fs::write(
dir.join("app/services/widget.rb"),
"class WidgetService\nend\n",
)
.unwrap();
fs::write(dir.join("app/models/widget.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["widget", "app/services", "--ndjson"]);
assert!(ok, "positional-path search failed: {out}");
assert!(
out.contains("app/services/widget.rb"),
"services hit kept: {out}"
);
assert!(
!out.contains("app/models/widget.rb"),
"models hit filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn no_record_still_returns_results() {
let (dir, db) = scratch("norec");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["widget", "--no-record", "--ndjson"]);
assert!(ok, "no-record search failed: {out}");
assert!(out.contains("\"name\":\"Widget\""), "result present: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn show_records_the_definition_it_printed() {
let (dir, db) = scratch("show-learn");
fs::write(
dir.join("beta.rb"),
"class HandlerB\n def go\n 1\n end\nend\n",
)
.unwrap();
rq(&db, &dir, &["--index"]);
let (ok, out) = rq(&db, &dir, &["--show", "handlerb", "--no-record"]);
assert!(ok && out.contains("def go"), "show printed a body: {out}");
let (_, out) = rq(&db, &dir, &["handlerb", "-e", "--no-record"]);
assert!(
!out.contains("learned"),
"--no-record taught nothing: {out}"
);
let (ok, out) = rq(&db, &dir, &["--show", "handlerb"]);
assert!(ok && out.contains("def go"), "show printed a body: {out}");
let (_, out) = rq(&db, &dir, &["handlerb", "-e", "--no-record"]);
assert!(out.contains("learned"), "show taught ranking: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn kind_filter_scopes_by_symbol_kind() {
let (dir, db) = scratch("kind");
fs::write(dir.join("a.rb"), "class Charge\n def charge\n end\nend\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(&db, &dir, &["charge", "--ndjson"]);
assert!(out.contains("\"kind\":\"class\""), "class present: {out}");
assert!(out.contains("\"kind\":\"method\""), "method present: {out}");
let (ok, out) = rq(&db, &dir, &["charge", "-k", "m", "--ndjson"]);
assert!(ok, "kind search failed: {out}");
assert!(out.contains("\"kind\":\"method\""), "method kept: {out}");
assert!(
!out.contains("\"kind\":\"class\""),
"class filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn first_query_warms_the_index_without_an_explicit_reindex() {
let (dir, db) = scratch("warm");
git_init(&dir);
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(ok, "cold search failed: {out}");
assert!(out.contains("\"name\":\"Widget\""), "result present: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn explicitly_indexes_and_recognizes_a_non_git_directory() {
let (dir, db) = scratch("nongit");
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
let (ok, out) = rq(&db, &dir, &["--index"]);
assert!(ok, "index of a non-git dir failed: {out}");
let (ok, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(ok, "search failed: {out}");
assert!(out.contains("\"name\":\"Widget\""), "result present: {out}");
assert!(
out.contains("current_repo"),
"current-repo boost applied: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn lang_filter_scopes_by_language() {
let (dir, db) = scratch("lang");
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
fs::write(dir.join("widget.rs"), "pub struct Widget {}\n").unwrap();
rq(&db, &dir, &["--index"]);
let (_, out) = rq(&db, &dir, &["widget", "--ndjson"]);
assert!(out.contains("\"language\":\"ruby\""), "ruby present: {out}");
assert!(out.contains("\"language\":\"rust\""), "rust present: {out}");
let (ok, out) = rq(&db, &dir, &["widget", "-x", "rs", "--ndjson"]);
assert!(ok, "lang search failed: {out}");
assert!(out.contains("\"language\":\"rust\""), "rust kept: {out}");
assert!(
!out.contains("\"language\":\"ruby\""),
"ruby filtered: {out}"
);
let (_, out) = rq(&db, &dir, &["widget", "-x", "r", "--ndjson"]);
assert!(
out.contains("\"language\":\"ruby\"") && out.contains("\"language\":\"rust\""),
"both kept for -x r: {out}"
);
let (_, out) = rq(&db, &dir, &["widget", "-x", "ruby", "--ndjson"]);
assert!(out.contains("\"language\":\"ruby\""), "ruby kept: {out}");
assert!(
!out.contains("\"language\":\"rust\""),
"rust filtered: {out}"
);
let (ok, out) = rq(&db, &dir, &["widget", "-x", "py", "--ndjson"]);
assert!(!ok, "no python here, should exit non-zero");
assert!(
out.contains("\"status\":\"no_match\""),
"a definitive miss reports no_match: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn searching_from_a_subdirectory_uses_the_repo_root() {
let (dir, db) = scratch("subdir");
git_init(&dir);
let sub = dir.join("nested");
fs::create_dir_all(&sub).unwrap();
fs::write(dir.join("top.rb"), "class TopWidget\nend\n").unwrap();
fs::write(sub.join("deep.rb"), "class DeepWidget\nend\n").unwrap();
let (ok, _) = rq(&db, &dir, &["--index"]);
assert!(ok);
let (_, out) = rq(&db, &dir, &["DeepWidget", "--no-record"]);
assert!(out.contains("nested/deep.rb"), "root-relative path: {out}");
let (_, out) = rq(&db, &sub, &["TopWidget", "--no-record"]);
assert!(
out.contains("TopWidget"),
"top symbol found from subdir: {out}"
);
let (_, status) = rq(&db, &dir, &["--status"]);
assert_eq!(
status.lines().count(),
1,
"one repo, not re-keyed: {status}"
);
assert!(status.contains("2 files"), "both files retained: {status}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn warming_a_committed_repo_indexes_tracked_source() {
let (dir, db) = scratch("gitwarm");
fs::create_dir_all(dir.join("lib")).unwrap();
fs::write(dir.join("lib/widget.rb"), "class Widget\nend\n").unwrap();
fs::write(dir.join("README.md"), "# docs, not source\n").unwrap();
git_init_commit(&dir);
let (ok, out) = rq(&db, &dir, &["Widget", "--no-record"]);
assert!(ok, "warmed search failed: {out}");
assert!(
out.contains("lib/widget.rb"),
"tracked source warmed: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn a_cold_repo_blocks_to_an_answer_instead_of_a_false_miss() {
let (dir, db) = scratch("block-json");
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["Widget", "--no-record", "--json"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env("RQ_ANSWER_BUDGET_MS", "1") .output()
.expect("run rq");
let out = String::from_utf8_lossy(&run.stdout);
assert!(
run.status.success(),
"a programmatic query should block until it finds the symbol; \
stdout={out:?} stderr={:?}",
String::from_utf8_lossy(&run.stderr)
);
assert!(out.contains("widget.rb"), "found in the right file: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn an_interactive_cold_repo_shows_progress_and_finds_the_answer() {
let (dir, db) = scratch("block-tty");
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["Widget", "--no-record"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env("RQ_ANSWER_BUDGET_MS", "1")
.env("RQ_ASSUME_INTERACTIVE", "1") .output()
.expect("run rq");
let out = String::from_utf8_lossy(&run.stdout);
assert!(run.status.success(), "should find the symbol: {out:?}");
assert!(out.contains("widget.rb"), "found in the right file: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn an_incomplete_index_reports_an_indeterminate_miss_not_a_definitive_one() {
let (dir, db) = scratch("indeterminate");
for i in 0..20 {
fs::write(
dir.join(format!("m{i:02}.rb")),
format!("class Widget{i}\nend\n"),
)
.unwrap();
}
git_init_commit(&dir);
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["Nonexistent", "--no-record", "--json"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env("RQ_COLLECT_CAP", "5") .output()
.expect("run rq");
let out = String::from_utf8_lossy(&run.stdout);
assert!(
out.contains("\"status\": \"warming\""),
"an incomplete-index miss reports warming in JSON: {out:?}"
);
assert_eq!(
run.status.code(),
Some(2),
"an incomplete-index miss is indeterminate (exit 2), not definitive"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn no_wait_returns_without_blocking_on_a_rebuild() {
let (dir, db) = scratch("no-wait");
fs::write(dir.join("widget.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
for flags in [["--no-wait"].as_slice(), ["--wait", "0"].as_slice()] {
let mut args = vec!["Nonexistent", "--no-record", "--json"];
args.extend_from_slice(flags);
let run = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(&args)
.current_dir(&dir)
.env("RQ_DB", &db)
.env("RQ_WARM_DETACH", "0") .env("RQ_WAIT_BUDGET_MS", "600000") .output()
.expect("run rq");
let out = String::from_utf8_lossy(&run.stdout);
assert!(
out.contains("\"status\": \"warming\""),
"{flags:?} miss on a not-yet-complete index reports warming (retry), \
not a block or a false absence: {out:?}"
);
assert_eq!(
run.status.code(),
Some(2),
"{flags:?} miss on an incomplete index is indeterminate (exit 2)"
);
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn symbols_outlines_a_file_in_line_order() {
let (dir, db) = scratch("symbols");
fs::write(
dir.join("widget.rb"),
"class Widget\n def build\n end\n def render\n end\nend\n",
)
.unwrap();
fs::write(dir.join("other.rb"), "class Other\nend\n").unwrap();
git_init_commit(&dir);
let (ok, out) = rq(&db, &dir, &["--symbols", "widget.rb", "--ndjson"]);
assert!(ok, "symbols failed: {out}");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 3, "class + two methods: {out}");
assert!(
first_line(&out).contains("\"name\":\"Widget\""),
"class first: {out}"
);
assert!(out.contains("\"name\":\"build\""), "build present: {out}");
assert!(
out.contains("\"parent\":\"Widget\""),
"method nests under class: {out}"
);
assert!(
out.contains("\"signature\":\"def build\""),
"signature read: {out}"
);
assert!(!out.contains("Other"), "other file excluded: {out}");
let (ok, out) = rq(
&db,
&dir,
&["--symbols", "widget.rb", "-k", "method", "--ndjson"],
);
assert!(ok, "filtered symbols failed: {out}");
assert_eq!(out.lines().count(), 2, "two methods only: {out}");
assert!(
!out.contains("\"name\":\"Widget\""),
"class filtered out: {out}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn bare_invocation_prints_help() {
let (dir, db) = scratch("help");
let (ok, out) = rq(&db, &dir, &[]);
assert!(ok, "bare rq should exit 0");
assert!(
out.contains("rq finds where a symbol is defined"),
"help banner: {out}"
);
assert!(out.contains("Usage:"), "usage in help: {out}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detached_warm_finishes_coverage_in_the_background() {
let (dir, db) = scratch("detach");
for i in 0..30 {
fs::write(
dir.join(format!("f{i:02}.rb")),
format!("class K{i:02}\nend\n"),
)
.unwrap();
}
git_init_commit(&dir);
let out = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(["K00", "--no-record"])
.current_dir(&dir)
.env("RQ_DB", &db)
.env("RQ_WARM_DETACH", "1")
.env("RQ_COLLECT_CAP", "5")
.output()
.expect("run rq");
assert!(
out.status.success(),
"search failed: {}",
String::from_utf8_lossy(&out.stdout)
);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
loop {
let (_, status) = rq(&db, &dir, &["--status", "--ndjson"]);
if status.contains("\"status\":\"complete\"") && status.contains("\"files\":30") {
break;
}
assert!(
std::time::Instant::now() < deadline,
"detached warm never completed: {status}"
);
std::thread::sleep(std::time::Duration::from_millis(200));
}
let _ = fs::remove_dir_all(&dir);
}
fn rq_stdin(db: &Path, cwd: &Path, args: &[&str], stdin: &str) -> (bool, String) {
use std::io::Write;
let mut child = Command::new(env!("CARGO_BIN_EXE_rq"))
.args(args)
.current_dir(cwd)
.env("RQ_DB", db)
.env("RQ_WARM_DETACH", "0")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.expect("run rq");
child
.stdin
.take()
.expect("stdin piped")
.write_all(stdin.as_bytes())
.expect("write queries");
let out = child.wait_with_output().expect("rq exits");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
)
}
#[test]
fn batch_answers_every_piped_query_and_says_which_is_which() {
let (dir, db) = scratch("batch");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
fs::write(dir.join("b.rb"), "class Gadget\nend\n").unwrap();
git_init_commit(&dir);
rq(&db, &dir, &["--index"]);
let (ok, out) = rq_stdin(
&db,
&dir,
&["-J", "-l", "1"],
"widget\ngadget\nnosuchthing\n",
);
assert!(ok, "a batch that found something exits 0: {out}");
let rows: Vec<serde_json::Value> = out
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| serde_json::from_str(l).expect("ndjson line"))
.collect();
let for_query = |q: &str| {
rows.iter()
.find(|r| r["query"] == q)
.unwrap_or_else(|| panic!("no row for {q}: {out}"))
.clone()
};
assert_eq!(for_query("widget")["name"], "Widget");
assert_eq!(for_query("gadget")["name"], "Gadget");
assert_eq!(for_query("nosuchthing")["status"], "no_match");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn batch_refuses_the_output_and_flags_it_cannot_frame() {
let (dir, db) = scratch("batch-flags");
fs::write(dir.join("a.rb"), "class Widget\nend\n").unwrap();
git_init_commit(&dir);
rq(&db, &dir, &["--index"]);
let (ok, _) = rq_stdin(&db, &dir, &["--json"], "widget\n");
assert!(!ok, "--json is refused for a batch");
let (ok, _) = rq_stdin(&db, &dir, &["-J", "--show"], "widget\n");
assert!(!ok, "--show is refused for a batch");
let _ = fs::remove_dir_all(&dir);
}