Files
rtk-ai__rtk/tests/search_stdin_test.rs
Nicolas Le Cam 7931993da8 fix(git,search,dotnet): correct the flag classes the last round got wrong
`-W`/`--function-context` does not make `git log` emit a patch -- checked
against git 2.53, where it is byte-identical to plain log -- so routing it raw
turned `rtk git log -W` into a 1 MB unfiltered dump of the whole history. The
flags that *do* imply a patch there are `-U<n>` and the combined-diff forms
`--cc`/`-c`/`--remerge-diff`, whose patch the log compaction was discarding
with no tee to recover it from.

That raw route is `run_passthrough`, which streams straight to the terminal, so
RTK's default limit has to be in the args or it never applies at all: every one
of those spellings printed the entire history, 411k lines against 50 for plain
`rtk git log`. It now carries `-10` whenever the user named no limit of their
own -- the limit only, since `--no-merges` would gut `--cc`/`-c`, whose whole
purpose is the merge diff.

`--exit-code` and `--quiet` are `diff`'s, not `log`'s. Both leave `git log`'s
output byte-identical to plain log, so there is no shape for it to escape to,
and claiming them cost the whole history for nothing. In `diff` they do earn
the raw route: the body is replaced by a report and git exits non-zero, which
RTK's stat probe inherited, its early return on failure then throwing the
entire diff away. They join `--check`, whose handling already existed for the
same reason.

`--quiet` then splits `diff` from `show`, so each states its own raw-output
grammar rather than sharing one. In `show` it is a synonym of `-s` -- exit 0,
body suppressed -- so it belongs on the compact path with `-s`/`--no-patch`,
where the summary is exactly what it asks for; claiming it raw-passed the very
header its synonyms compact.

That split's short-flag rule holds for `-p`/`-u`/`-U`, which only restate
`diff`/`show`'s patch default, but not for `-c`: it is `--cc`'s combined-diff
form, and `compact_diff` reads a combined diff's two marker columns as one, so
`git show -c` on a merge came back as `+54 -8` against git's own 156
insertions and 0 deletions. It takes the raw route its long form already took.

`--diff-merges=<format>` names those same shapes a second way, so it routes on
the format rather than on the flag: every value but `off`/`none` emits a patch,
which `log` cannot compact, while only `c`/`cc`/`combined`/`dense-combined`
produce the two marker columns, which is all `show` needs the raw route for.
Both value spellings count -- git takes the format attached or as the next
token, and `git log --diff-merges c` is accepted.

`--unified=5` and `-U5` are one option and now take one path.

`run_show`'s format gate keeps `--oneline` out, unlike `run_log`'s: there the
gate routes the whole command to a raw passthrough, and `--oneline` only means
the user's one-line summary outranks RTK's own one-line summary. Claiming it
gave up compaction entirely to fix that -- 116 KB against 31 KB on a real
commit, on the one metric this tool exists for.

`git worktree`'s write actions compact to "ok" again unless the user asked for
a report with `--dry-run`/`-n` or `--verbose`/`-v`. Keeping every non-empty
report was meant for `prune --dry-run`, whose list is the point of the command,
but `add` also speaks on success: its two progress lines came back instead of
"ok", and in the wrong order, since one is on stdout and the other on stderr.

search: the `=`-unwrapping applies to the attached spelling only. Applied to a
separate-token value it ate the first character of the user's pattern, so
`rtk rg -e '=='` searched for `=` and matched lines the real command does not.
And the filename negation is engine-specific in both directions -- grep's `-h`
is `--no-filename` where rg's is `--help`, and rg's `-I` is `--no-filename`
where grep's is `--binary-files` -- so rg's spelling was forwarded, beat RTK's
`--with-filename`, and ran the whole search a second time. `-N` and
`--no-line-number` are ripgrep's alone for the same reason: grep 3.12 exits 2
on both, so recognising them there reported a match for a command the engine
refuses to run.

Recursion is not a filename request. `-r`/`-R`/`--recursive` shared the
last-spelling-wins slot with `-H`/`-h`, which made the result depend on typing
order -- `grep -rh` dropped the prefix and `grep -hr` kept it, where real grep
drops it either way, because `-r` only makes the search span several files.
It now feeds the fallback that the paths already feed, and only `-H`/`-h`
override.

dotnet: the `-l` alias is single-dash only. `--l` is not a spelling dotnet
accepts, so treating it as the logger suppressed RTK's injection for a command
that then wrote no TRX at all.

Two tests were asserting less than they appeared to: the stdin fixture lived
inside the directory being searched, so an engine that ignored stdin and walked
the cwd still produced the asserted line, and the blob-show test built its
tokens with log's grammar where `run_show` uses diff's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 12:14:09 +02:00

106 lines
4.0 KiB
Rust

#![cfg(unix)]
//! Which stdin the engines actually read. These run in CI as integration tests rather than
//! `#[ignore]`d unit tests: `CARGO_BIN_EXE_rtk` guarantees a built binary, so the fix they
//! cover is exercised by `cargo test --all` instead of only by a flag nobody passes.
use std::process::{Command, Stdio};
fn rtk() -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_rtk"));
cmd.env("LC_ALL", "C");
cmd
}
/// Each test guards on the engine it actually drives -- guarding an rg test on grep (or the
/// reverse) turns a missing tool into a silent pass rather than a skip.
fn engine_available(engine: &str) -> bool {
Command::new(engine)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[test]
fn rg_shows_filenames_when_stdin_is_not_a_pipe() {
if !engine_available("rg") {
return;
}
// `rtk rg -z foo < /dev/null` in a multi-file dir used to drop filenames: stdin being a
// non-terminal, non-pipe redirect was misread as "the engine reads stdin," routing into the
// streaming path, which can't discover "multiple files" the way the buffered path does.
// Real rg searches the cwd here, not stdin.
let dir = tempfile::tempdir().expect("test setup");
std::fs::write(dir.path().join("a.txt"), "foo one\n").expect("test setup");
std::fs::write(dir.path().join("b.txt"), "foo two\n").expect("test setup");
let output = rtk()
.args(["rg", "-z", "foo"])
.current_dir(dir.path())
.stdin(Stdio::null())
.output()
.expect("failed to run rtk rg");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("a.txt"), "filename missing: {stdout}");
assert!(stdout.contains("b.txt"), "filename missing: {stdout}");
}
#[test]
fn a_single_matching_file_still_gets_its_name() {
if !engine_available("rg") {
return;
}
// The engine walked the cwd itself, so the filename is the only way to place the match --
// real rg prints it even when exactly one file matched.
let dir = tempfile::tempdir().expect("test setup");
std::fs::write(dir.path().join("only.txt"), "foo here\n").expect("test setup");
std::fs::write(dir.path().join("other.txt"), "nothing\n").expect("test setup");
let output = rtk()
.args(["rg", "foo"])
.current_dir(dir.path())
.stdin(Stdio::null())
.output()
.expect("failed to run rtk rg");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("only.txt"), "filename missing: {stdout}");
}
#[test]
fn both_engines_read_a_redirected_file_on_stdin() {
// A regular file on stdin *is* read by both engines (rg's own is_readable_stdin counts
// files and sockets, not just FIFOs), so the search must return that file's match rather
// than the cwd's.
let dir = tempfile::tempdir().expect("test setup");
std::fs::write(dir.path().join("a.txt"), "foo from the cwd\n").expect("test setup");
// The fixture lives outside the searched directory: inside it, an engine that ignored
// stdin and walked the cwd would still print "foo from stdin" and the test would pass.
let elsewhere = tempfile::tempdir().expect("test setup");
let piped = elsewhere.path().join("piped.log");
std::fs::write(&piped, "foo from stdin\n").expect("test setup");
for engine in ["grep", "rg"] {
if !engine_available(engine) {
continue;
}
let output = rtk()
.args([engine, "foo"])
.current_dir(dir.path())
.stdin(std::fs::File::open(&piped).expect("test setup"))
.output()
.expect("failed to run rtk");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("foo from stdin"),
"{engine} did not read stdin: {stdout}"
);
assert!(
!stdout.contains("foo from the cwd"),
"{engine} walked the cwd instead of reading stdin: {stdout}"
);
}
}