jmove/tests/cli_refs.rs
loki5512344 1ddf79b508
feat: warn about non-import references to moved files
The import graph only rewrites import/export-from/require. Files are
also named from markdown links, package.json fields, jest.mock strings
and tsconfig file lists, and a move that dangles those must not pass
silently. mv now scans text formats plus source strings for the moved
paths, module names and old specifier forms (word-boundary matched,
lockfiles/hidden dirs skipped) and reports each occurrence once per
line on stderr and as non_import_refs[] in --json (dry-run included).
References are never edited; exit codes are unchanged.

Also moves normalize_scope into Index (its only consumer) and the
Change/ChangedFile JSON structs into output where they are built, to
stay inside the 250-line-per-file budget.
2026-09-15 19:48:18 +02:00

68 lines
2.2 KiB
Rust

//! `mv` warns (never edits) about non-import references: markdown links,
//! `package.json` fields and `jest.mock` strings survive silent unless the
//! user sees them. JSON payload carries the same list for agents.
mod common;
use common::{copy_fixture, in_root, jmove, read};
use predicates::prelude::*;
fn dry_run_refs() -> String {
let tmp = copy_fixture("typescript", "refs");
let out = jmove(
&tmp,
&["mv", "lib/sum.ts", "lib/total.ts", "--dry-run", "--json"],
)
.success()
.stdout(predicate::str::contains("\"non_import_refs\""))
.get_output()
.stdout
.clone();
String::from_utf8(out).expect("json is utf-8")
}
#[test]
fn json_dry_run_lists_hidden_references_and_skips_the_noise() {
let out = dry_run_refs();
for expected in [
"\"file\": \"README.md\"",
"\"file\": \"package.json\"",
"\"file\": \"__tests__/sum.test.ts\"",
] {
assert!(out.contains(expected), "missing {expected} in {out}");
}
for absent in [
"\"file\": \"app.ts\"",
"\"file\": \"package-lock.json\"",
"\"file\": \".notes/refs.md\"",
"\"file\": \"lib/summary.ts\"",
] {
assert!(!out.contains(absent), "leaked {absent} in {out}");
}
}
#[test]
fn human_dry_run_warns_on_stderr_and_exits_zero() {
let tmp = copy_fixture("typescript", "refs");
jmove(&tmp, &["mv", "lib/sum.ts", "lib/total.ts", "--dry-run"])
.success()
.stderr(
predicate::str::contains("non-import reference")
.and(predicate::str::contains("README.md:1"))
.and(predicate::str::contains("__tests__/sum.test.ts:1")),
);
}
#[test]
fn real_move_still_succeeds_and_still_warns() {
let tmp = copy_fixture("typescript", "refs");
jmove(&tmp, &["mv", "lib/sum.ts", "lib/total.ts"])
.success()
.stderr(predicate::str::contains("may need manual fixing"));
assert!(in_root(tmp.path(), "lib/total.ts").exists());
let app = read(&in_root(tmp.path(), "app.ts"));
assert!(app.contains("from './lib/total'"), "{app}");
// The scanner warns; only the import graph is rewritten.
let readme = read(&in_root(tmp.path(), "README.md"));
assert!(readme.contains("./lib/sum.ts"), "{readme}");
}