Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0ea7ac2
test(js): cover Vitest and Jest dispatch and spawning
yeahjack Oct 5, 2026
9f18e6c
test(js): cover literal argv and fallback reporter wiring
yeahjack Oct 5, 2026
0d19ed5
chore(js): integrate develop and align reporter regression coverage
yeahjack Oct 13, 2026
778a857
chore(sync): merge latest develop into test/vitest-jest-command-routing
yeahjack Oct 14, 2026
a805eee
chore(sync): merge latest develop into test/vitest-jest-command-routing
yeahjack Oct 14, 2026
79439e6
chore(sync): merge latest develop into test/vitest-jest-command-routing
yeahjack Oct 15, 2026
981f62d
chore(sync): merge latest develop into test/vitest-jest-command-routing
yeahjack Oct 16, 2026
6facb26
chore(sync): merge latest develop into test/vitest-jest-command-routing
yeahjack Oct 17, 2026
8753327
chore(sync): merge latest develop into test/vitest-jest-command-routing
yeahjack Oct 17, 2026
077effb
Merge commit '9a501c8d3b783b23a96073261708715381eb5ceb' into safe-syn…
yeahjack Oct 19, 2026
63eee9b
chore(sync): merge develop into test/vitest-jest-command-routing
yeahjack Oct 19, 2026
18f437c
Merge develop into test/vitest-jest-command-routing
yeahjack Oct 19, 2026
acbd90a
Merge develop into test/vitest-jest-command-routing
yeahjack Oct 19, 2026
92f1403
Merge develop into test/vitest-jest-command-routing
yeahjack Oct 19, 2026
c74a8be
Merge develop into test/vitest-jest-command-routing
yeahjack Oct 20, 2026
6311481
Merge develop into test/vitest-jest-command-routing
yeahjack Oct 20, 2026
408e246
Merge develop into test/vitest-jest-command-routing
yeahjack Oct 20, 2026
71f2969
Merge develop into test/vitest-jest-command-routing
yeahjack Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions tests/fixtures/fake_test_runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! Native fake for CLI routing tests: records the executable and argument vector
//! without requiring Node, a shell, a package registry, or installed JS runners.
use std::env;
use std::fs::OpenOptions;
use std::io::Write;

fn main() {
let executable = env::current_exe().expect("current executable");
let name = executable.file_stem().expect("executable name");
let mut trace = OpenOptions::new()
.create(true)
.append(true)
.open(env::var_os("RTK_TEST_TRACE").expect("trace path"))
.expect("open trace");
writeln!(trace, "{}", name.to_string_lossy()).expect("record executable");
for arg in env::args().skip(1) {
writeln!(trace, "{arg}").expect("record argument");
}
writeln!(trace, "END").expect("record invocation end");
std::io::stdout()
.write_all(env::var("RTK_TEST_STDOUT").unwrap_or_default().as_bytes())
.expect("write stdout");
std::io::stderr()
.write_all(env::var("RTK_TEST_STDERR").unwrap_or_default().as_bytes())
.expect("write stderr");
std::process::exit(
env::var("RTK_TEST_EXIT")
.expect("exit code")
.parse()
.expect("numeric exit code"),
);
}
245 changes: 245 additions & 0 deletions tests/framework_test_routing_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
//! Exercise the public CLI, package-manager selection, spawn and output wiring.
//! A native Rust fake runs on Unix and Windows; no JS tools or downloads are used.
use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use std::sync::LazyLock;

mod common;

const PASS_JSON: &str =
r#"{"numTotalTests":2,"numPassedTests":2,"numFailedTests":0,"testResults":[]}"#;
const FAIL_JSON: &str = r#"{"numTotalTests":1,"numPassedTests":0,"numFailedTests":1,"testResults":[{"name":"src/fail.test.ts","assertionResults":[{"fullName":"fails deliberately","status":"failed","failureMessages":["expected true, got false"]}]}]}"#;

// Compile once using the same Rust toolchain required to run the test suite.
// Keep bytes rather than a TempDir in a static so the compiler's scratch files
// are cleaned up, and every fixture owns its own executables.
static FAKE_RUNNER: LazyLock<Vec<u8>> = LazyLock::new(|| {
let dir = tempfile::tempdir().expect("compiler scratch directory");
let output = dir
.path()
.join(format!("fake{}", std::env::consts::EXE_SUFFIX));
let result = Command::new(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()))
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/fake_test_runner.rs"))
.args(["--edition=2024", "--deny", "warnings", "-o"])
.arg(&output)
.output()
.expect("compile native fake runner");
assert!(
result.status.success(),
"compile fake runner: {}",
String::from_utf8_lossy(&result.stderr)
);
fs::read(output).expect("read native fake runner")
});

struct Fixture {
dir: tempfile::TempDir,
}

impl Fixture {
fn new(direct_tools: bool, lockfile: Option<&str>) -> Self {
let dir = tempfile::tempdir().expect("fixture directory");
let bin = dir.path().join("bin");
fs::create_dir(&bin).expect("fixture bin");
// Install all runner fakes so choosing the wrong one is observable too.
let mut names = vec!["pnpm", "yarn", "npx", "bunx"];
if direct_tools {
names.extend(["vitest", "jest"]);
}
for name in names {
let path = bin.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
fs::write(&path, &*FAKE_RUNNER).expect("write fake executable");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o755))
.expect("make fake executable");
}
}
if let Some(lockfile) = lockfile {
fs::write(dir.path().join(lockfile), "").expect("write lockfile");
}
Self { dir }
}

fn run(&self, args: &[&str], stdout: &str, stderr: &str, exit: i32) -> Output {
common::rtk_command()
.args(args)
.current_dir(self.dir.path())
// No inherited PATH: an installed runner must never defeat the fake
// or cause these tests to execute/download real JavaScript packages.
.env("PATH", self.dir.path().join("bin"))
.env("PATHEXT", ".EXE")
.env("NO_COLOR", "1")
.env("RTK_TEST_TRACE", self.dir.path().join("trace"))
.env("RTK_TEST_STDOUT", stdout)
.env("RTK_TEST_STDERR", stderr)
.env("RTK_TEST_EXIT", exit.to_string())
.output()
.expect("run rtk")
}

fn assert_invocation(&self, expected: &[&str]) {
let trace = fs::read_to_string(self.dir.path().join("trace")).expect("read spawn trace");
let expected: Vec<_> = expected.iter().copied().chain(["END"]).collect();
assert_eq!(trace.lines().collect::<Vec<_>>(), expected);
}
}

fn framework_args(framework: &str) -> Vec<&str> {
match framework {
"vitest" => vec!["run", "--reporter=json"],
"jest" => vec!["--no-watch", "--json"],
_ => panic!("unknown test framework"),
}
}

#[test]
fn cli_dispatches_each_framework_to_its_own_executable_and_arguments() {
for framework in ["vitest", "jest"] {
// A lockfile must not displace a runner already available on PATH.
let fixture = Fixture::new(true, Some("pnpm-lock.yaml"));
let user_args = ["src/has space.test.ts", "-t", "works with spaces"];
let args: Vec<_> = [framework].into_iter().chain(user_args).collect();
let out = fixture.run(&args, PASS_JSON, "", 0);
let mut expected = vec![framework];
expected.extend(framework_args(framework));
expected.extend(user_args);
fixture.assert_invocation(&expected);
assert_eq!(out.status.code(), Some(0));
assert_eq!(out.stdout, b"PASS (2) FAIL (0)\n");
assert!(out.stderr.is_empty());
}
}

#[test]
fn missing_framework_uses_the_projects_package_manager_without_fetching() {
for (lockfile, prefix) in [
(Some("pnpm-lock.yaml"), vec!["pnpm", "exec", "--"]),
(Some("yarn.lock"), vec!["yarn", "exec", "--"]),
(Some("bun.lock"), vec!["npx", "--no-install", "--"]),
(None, vec!["npx", "--no-install", "--"]),
] {
for framework in ["vitest", "jest"] {
let fixture = Fixture::new(false, lockfile);
let out = fixture.run(&[framework, "src/a.test.ts"], PASS_JSON, "", 0);
let mut expected = prefix.clone();
expected.push(framework);
expected.extend(framework_args(framework));
expected.push("src/a.test.ts");
fixture.assert_invocation(&expected);
assert_eq!(out.status.code(), Some(0));
assert_eq!(out.stdout, b"PASS (2) FAIL (0)\n");
assert!(out.stderr.is_empty());
}
}
}

#[test]
fn explicit_vitest_reporter_reaches_the_spawn_and_bypasses_parsing() {
let verbose =
" ✓ src/a.test.ts > keeps this test name\n\n Tests 2 passed (2)\n Duration 10ms\n";
let json_line = format!("{PASS_JSON}\n");
for (reporter, raw, expected_stdout) in [
(vec!["--reporter=verbose"], verbose, verbose),
(vec!["--reporter", "verbose"], verbose, verbose),
// An explicitly requested JSON report must also remain JSON.
(vec!["--reporter=json"], PASS_JSON, json_line.as_str()),
] {
let fixture = Fixture::new(true, None);
let mut args = vec!["vitest", "src/a.test.ts"];
args.extend(&reporter);
let out = fixture.run(&args, raw, "", 0);
let mut expected = vec!["vitest", "run", "src/a.test.ts"];
expected.extend(&reporter);
fixture.assert_invocation(&expected);
assert_eq!(out.status.code(), Some(0));
// Terminated reports stay byte-exact; only a missing final newline is added.
assert_eq!(out.stdout, expected_stdout.as_bytes());
assert!(out.stderr.is_empty());
}
}

#[test]
fn framework_failures_keep_the_child_exit_status() {
for framework in ["vitest", "jest"] {
let fixture = Fixture::new(true, None);
let out = fixture.run(&[framework], FAIL_JSON, "", 7);
let mut expected = vec![framework];
expected.extend(framework_args(framework));
fixture.assert_invocation(&expected);
assert_eq!(out.status.code(), Some(7));
let shown = String::from_utf8_lossy(&out.stdout);
assert!(shown.contains("FAIL (1)"), "{shown}");
assert!(shown.contains("fails deliberately"), "{shown}");
assert!(shown.contains("expected true, got false"), "{shown}");
assert!(out.stderr.is_empty());
}
}

#[test]
fn explicit_reporter_keeps_stderr_diagnostics_and_failure_status() {
let fixture = Fixture::new(true, None);
let stdout = " Tests 2 passed (2)\n";
let stderr = "reporter teardown failed\n";
let out = fixture.run(&["vitest", "--reporter=verbose"], stdout, stderr, 3);
fixture.assert_invocation(&["vitest", "run", "--reporter=verbose"]);
assert_eq!(out.status.code(), Some(3));
assert_eq!(out.stdout, format!("{stdout}{stderr}").as_bytes());
assert!(out.stderr.is_empty());
}

#[test]
fn native_spawn_preserves_argument_boundaries_and_metacharacters() {
for framework in ["vitest", "jest"] {
let fixture = Fixture::new(true, None);
let user_args = [
"src/a.test.ts",
"--",
"-literal",
"a;$(echo unsafe)&b",
"quote\"and\\slash",
];
let args: Vec<_> = [framework].into_iter().chain(user_args).collect();
let out = fixture.run(&args, PASS_JSON, "", 0);
let mut expected = vec![framework];
expected.extend(framework_args(framework));
expected.extend(user_args);
fixture.assert_invocation(&expected);
assert_eq!(out.status.code(), Some(0));
assert_eq!(out.stdout, b"PASS (2) FAIL (0)\n");
assert!(out.stderr.is_empty());
}
}

#[test]
fn reporter_passthrough_also_reaches_the_package_manager() {
let fixture = Fixture::new(false, Some("pnpm-lock.yaml"));
let out = fixture.run(&["vitest", "--reporter", "json"], PASS_JSON, "", 0);
fixture.assert_invocation(&["pnpm", "exec", "--", "vitest", "run", "--reporter", "json"]);
assert_eq!(out.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&out.stdout),
format!("{PASS_JSON}\n")
);
assert!(out.stderr.is_empty());
}

#[test]
fn explicit_reporter_preserves_empty_and_terminated_output() {
for (raw, expected) in [
("", ""),
("report", "report\n"),
("report\n", "report\n"),
("report\n\n", "report\n\n"),
("report\r\n", "report\r\n"),
] {
let fixture = Fixture::new(true, None);
let out = fixture.run(&["vitest", "--reporter=verbose"], raw, "", 0);
fixture.assert_invocation(&["vitest", "run", "--reporter=verbose"]);
assert_eq!(out.status.code(), Some(0));
assert_eq!(out.stdout, expected.as_bytes(), "report input: {raw:?}");
assert!(out.stderr.is_empty());
}
}
Loading