#![allow(
clippy::expect_used,
reason = "integration tests use expect to keep fixture setup concise"
)]
#[path = "common/mod.rs"]
mod common;
use std::path::{Path, PathBuf};
use std::process::Command;
use common::fallow_bin;
const SIZE_LIMIT_MB: &str = "1";
fn write_skipped_importer_project(root: &Path) {
std::fs::create_dir_all(root.join("src")).expect("create src");
std::fs::write(
root.join("package.json"),
r#"{ "name": "caveat-surfaces", "version": "1.0.0", "main": "src/index.ts" }"#,
)
.expect("write manifest");
std::fs::write(root.join("src/lib.ts"), "export const needed = 1;\n").expect("write library");
let mut oversized = String::from("import { needed } from \"./lib\";\nexport const pad = [\n");
while oversized.len() < 2 * 1024 * 1024 {
oversized.push_str(" \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n");
}
oversized.push_str("];\n");
std::fs::write(root.join("src/huge.ts"), oversized).expect("write oversized importer");
std::fs::write(
root.join("src/index.ts"),
"import \"./huge\";\n\nexport const run = (): void => {};\n",
)
.expect("write entry module");
}
fn write_skipped_export_importer_project(root: &Path) {
std::fs::create_dir_all(root.join("src")).expect("create src");
std::fs::write(
root.join("package.json"),
r#"{ "name": "caveat-export-surfaces", "version": "1.0.0", "main": "src/index.ts" }"#,
)
.expect("write manifest");
std::fs::write(
root.join("src/lib.ts"),
"export const needed = 1;\nexport const alsoUsed = 2;\n",
)
.expect("write library");
let mut oversized = String::from("import { needed } from \"./lib\";\nexport const pad = [\n");
while oversized.len() < 2 * 1024 * 1024 {
oversized.push_str(" \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n");
}
oversized.push_str("];\n");
std::fs::write(root.join("src/huge.ts"), oversized).expect("write oversized importer");
std::fs::write(
root.join("src/index.ts"),
"import \"./huge\";\nimport { alsoUsed } from \"./lib\";\n\nexport const run = (): number => alsoUsed;\n",
)
.expect("write entry module");
}
fn run_format(root: &Path, format: &str) -> String {
let output = Command::new(fallow_bin())
.current_dir(root)
.env("NO_COLOR", "1")
.env("RUST_LOG", "")
.env("FALLOW_MAX_FILE_SIZE", SIZE_LIMIT_MB)
.args(["check", "--format", format, "--quiet", "--no-cache"])
.output()
.expect("run fallow check");
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn project() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("temporary project");
let root = dir.path().join("project");
write_skipped_importer_project(&root);
(dir, root)
}
#[test]
fn the_json_finding_is_reported_with_its_actions_and_a_caveat() {
let (_dir, root) = project();
let stdout = run_format(&root, "json");
let envelope: serde_json::Value = serde_json::from_str(&stdout).expect("json envelope");
let finding = &envelope["unused_files"][0];
assert_eq!(finding["path"], "src/lib.ts");
assert_eq!(
finding["reachability_caveats"],
serde_json::json!(["incomplete-import-graph"])
);
assert!(
!finding["actions"].as_array().expect("actions").is_empty(),
"a caveat is advisory provenance and never trims the actions: {finding}"
);
}
#[test]
fn the_surfaces_that_already_carried_the_caveat_still_do() {
let (_dir, root) = project();
assert!(
run_format(&root, "human").contains("(caveat: incomplete import graph)"),
"human report"
);
assert!(
run_format(&root, "sarif").contains("(caveat: incomplete import graph)"),
"sarif message"
);
}
#[test]
fn the_summary_view_states_the_caveat() {
let (_dir, root) = project();
let output = Command::new(fallow_bin())
.current_dir(&root)
.env("NO_COLOR", "1")
.env("RUST_LOG", "")
.env("FALLOW_MAX_FILE_SIZE", SIZE_LIMIT_MB)
.args(["check", "--summary", "--quiet", "--no-cache"])
.output()
.expect("run fallow check --summary");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("carry a caveat: incomplete import graph"),
"{stdout}"
);
}
const CAVEAT_BEARING_FORMATS: [&str; 11] = [
"human",
"compact",
"markdown",
"codeclimate",
"sarif",
"pr-comment-github",
"pr-comment-gitlab",
"review-github",
"review-gitlab",
"github-annotations",
"github-summary",
];
#[test]
fn every_machine_consumed_format_carries_the_caveat() {
let (_dir, root) = project();
for format in CAVEAT_BEARING_FORMATS {
let rendered = run_format(&root, format);
let names_caveat = rendered.contains("incomplete import graph")
|| rendered.contains("incomplete-import-graph");
assert!(
names_caveat,
"`--format {format}` recommends acting on a caveated finding without saying the evidence is incomplete:\n{rendered}"
);
}
}
#[test]
fn the_review_comment_body_hedges_next_to_its_fix_intent() {
let (_dir, root) = project();
let envelope: serde_json::Value =
serde_json::from_str(&run_format(&root, "review-github")).expect("review envelope");
let body = envelope["comments"][0]["body"]
.as_str()
.expect("inline comment body");
assert!(
body.contains("caveat: incomplete import graph"),
"the inline comment must hedge: {body}"
);
assert!(
body.contains("Fix intent:"),
"the guard is only meaningful while the comment still offers the mutation: {body}"
);
assert!(
body.find("caveat:").expect("caveat position")
< body.find("Fix intent:").expect("fix intent position"),
"the hedge has to be read before the recommendation: {body}"
);
}
#[test]
fn a_clean_project_carries_no_caveat_on_any_surface() {
let dir = tempfile::tempdir().expect("temporary project");
let root = dir.path().join("project");
std::fs::create_dir_all(root.join("src")).expect("create src");
std::fs::write(
root.join("package.json"),
r#"{ "name": "clean", "version": "1.0.0", "main": "src/index.ts" }"#,
)
.expect("write manifest");
std::fs::write(root.join("src/lib.ts"), "export const needed = 1;\n").expect("write library");
std::fs::write(
root.join("src/index.ts"),
"export const run = (): void => {};\n",
)
.expect("write entry module");
for format in CAVEAT_BEARING_FORMATS {
let rendered = run_format(&root, format);
assert!(
!rendered.to_lowercase().contains("caveat"),
"`--format {format}` invented a caveat on a fully analyzed project:\n{rendered}"
);
}
}
#[test]
fn a_caveated_finding_ships_its_text_but_no_committable_edit() {
let dir = tempfile::tempdir().expect("temporary project");
let root = dir.path().join("project");
write_skipped_export_importer_project(&root);
for (format, fence) in [
("review-github", "```suggestion"),
("review-gitlab", "```suggestion:-0+0"),
] {
let envelope: serde_json::Value =
serde_json::from_str(&run_format(&root, format)).expect("review envelope");
let body = envelope["comments"][0]["body"]
.as_str()
.expect("inline comment body")
.to_owned();
assert!(
!body.contains(fence),
"`--format {format}` shipped a one-click edit for a mutation `fix` refuses: {body}"
);
assert!(
body.contains("caveat: incomplete import graph"),
"`--format {format}` dropped the caveat text along with the block: {body}"
);
assert!(
body.contains("No one-click fix offered"),
"`--format {format}` withheld the block without saying so: {body}"
);
}
}
#[test]
fn a_clean_run_keeps_its_committable_edit() {
let dir = tempfile::tempdir().expect("temporary project");
let root = dir.path().join("project");
std::fs::create_dir_all(root.join("src")).expect("create src");
std::fs::write(
root.join("package.json"),
r#"{ "name": "clean", "version": "1.0.0", "main": "src/index.ts" }"#,
)
.expect("write manifest");
std::fs::write(
root.join("src/lib.ts"),
"export const used = 1;\nexport const needed = 2;\n",
)
.expect("write library");
std::fs::write(
root.join("src/index.ts"),
"import { used } from './lib';\n\nexport const run = (): number => used;\n",
)
.expect("write entry module");
for (format, fence) in [
("review-github", "```suggestion"),
("review-gitlab", "```suggestion:-0+0"),
] {
let envelope: serde_json::Value =
serde_json::from_str(&run_format(&root, format)).expect("review envelope");
let bodies = envelope["comments"]
.as_array()
.expect("inline comments")
.iter()
.filter_map(|comment| comment["body"].as_str())
.collect::<Vec<_>>()
.join("\n");
assert!(
bodies.contains(fence),
"`--format {format}` must keep the edit block on a fully analyzed run: {bodies}"
);
assert!(
!bodies.contains("No one-click fix offered"),
"`--format {format}` withheld an edit the run has the evidence for: {bodies}"
);
}
}
#[test]
fn the_fix_path_withholds_the_write_the_review_comment_hedges() {
let dir = tempfile::tempdir().expect("temporary project");
let root = dir.path().join("project");
write_skipped_export_importer_project(&root);
let output = Command::new(fallow_bin())
.current_dir(&root)
.env("NO_COLOR", "1")
.env("RUST_LOG", "")
.env("FALLOW_MAX_FILE_SIZE", SIZE_LIMIT_MB)
.args([
"fix",
"--dry-run",
"--format",
"json",
"--quiet",
"--no-cache",
])
.output()
.expect("run fallow fix --dry-run");
let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).expect("fix envelope");
assert_eq!(
envelope["fixes"][0]["skip_reason"], "low_confidence_incomplete_analysis",
"{envelope}"
);
let review: serde_json::Value =
serde_json::from_str(&run_format(&root, "review-github")).expect("review envelope");
let body = review["comments"][0]["body"]
.as_str()
.expect("inline comment body");
assert!(
body.contains("caveat: incomplete import graph"),
"the surface that reports what `fix` withheld must hedge it: {body}"
);
assert!(
!body.contains("```suggestion"),
"the review comment must not offer a one-click edit for the write `fix` refused: {body}"
);
}
fn run_fallow(root: &Path, cwd: &Path, fallow_root: Option<&Path>, args: &[&str]) -> String {
let mut command = Command::new(fallow_bin());
command
.current_dir(cwd)
.env("NO_COLOR", "1")
.env("RUST_LOG", "")
.env_remove("FALLOW_ROOT")
.env("FALLOW_MAX_FILE_SIZE", SIZE_LIMIT_MB)
.args(args)
.args(["--quiet", "--no-cache", "-r"])
.arg(root);
if let Some(fallow_root) = fallow_root {
command.env("FALLOW_ROOT", fallow_root);
}
let output = command.output().expect("run fallow");
String::from_utf8_lossy(&output.stdout).into_owned()
}
#[test]
fn the_combined_envelope_carries_the_caveat_under_check() {
let (_dir, root) = project();
let envelope: serde_json::Value =
serde_json::from_str(&run_fallow(&root, &root, None, &["--format", "json"]))
.expect("combined envelope");
assert!(
envelope.get("dead_code").is_none(),
"the combined envelope names its dead-code block `check`: {}",
envelope
.as_object()
.map(|map| map.keys().cloned().collect::<Vec<_>>().join(", "))
.unwrap_or_default()
);
assert_eq!(
envelope["check"]["unused_files"][0]["reachability_caveats"][0], "incomplete-import-graph",
"{envelope}"
);
}
#[test]
fn the_annotation_appends_the_caveat_after_its_remediation_guidance() {
let (_dir, root) = project();
let rendered = run_format(&root, "github-annotations");
let annotation = rendered
.lines()
.find(|line| line.contains("::warning"))
.expect("an annotation for the caveated finding");
let caveat = annotation
.find("Caveat: incomplete import graph.")
.expect("the caveat sentence");
let guidance = annotation
.find("Consider removing it")
.expect("the remediation guidance");
assert!(
guidance < caveat,
"the caveat is appended after the guidance, not inserted before it: {annotation}"
);
assert!(
annotation.ends_with("so verify before removing."),
"the message ends with the explanation, not with the `Caveat:` sentence: {annotation}"
);
}
#[test]
fn only_the_review_formats_carry_a_fix_intent_line() {
let (_dir, root) = project();
for format in ["pr-comment-github", "pr-comment-gitlab"] {
let rendered = run_format(&root, format);
assert!(
rendered.contains("caveat: incomplete import graph"),
"`--format {format}` dropped the caveat: {rendered}"
);
assert!(
!rendered.contains("Fix intent:"),
"`--format {format}` is a summary table and has no fix-intent line: {rendered}"
);
}
for format in ["review-github", "review-gitlab"] {
let rendered = run_format(&root, format);
assert!(
rendered.contains("Fix intent:"),
"`--format {format}` is the surface that carries the fix-intent line: {rendered}"
);
}
}
#[test]
fn withheld_export_removals_are_counted_by_file() {
let dir = tempfile::tempdir().expect("temporary project");
let root = dir.path().join("project");
std::fs::create_dir_all(root.join("src")).expect("create src");
std::fs::write(
root.join("package.json"),
r#"{ "name": "caveat-counts", "version": "1.0.0", "main": "src/index.ts" }"#,
)
.expect("write manifest");
std::fs::write(
root.join("src/a.ts"),
"export const a1 = 1;\nexport const a2 = 2;\nexport const aKeep = 3;\n",
)
.expect("write first library");
std::fs::write(
root.join("src/b.ts"),
"export const b1 = 1;\nexport const bKeep = 2;\n",
)
.expect("write second library");
let mut oversized = String::from(
"import { a1, a2 } from \"./a\";\nimport { b1 } from \"./b\";\nexport const pad = [\n",
);
while oversized.len() < 2 * 1024 * 1024 {
oversized.push_str(" \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n");
}
oversized.push_str("];\n");
std::fs::write(root.join("src/huge.ts"), oversized).expect("write oversized importer");
std::fs::write(
root.join("src/index.ts"),
"import \"./huge\";\nimport { aKeep } from \"./a\";\nimport { bKeep } from \"./b\";\n\nexport const run = (): number => aKeep + bKeep;\n",
)
.expect("write entry module");
let envelope: serde_json::Value = serde_json::from_str(&run_fallow(
&root,
&root,
None,
&["fix", "--dry-run", "--format", "json"],
))
.expect("fix envelope");
assert_eq!(
envelope["skipped_low_confidence_exports"], 2,
"three withheld exports across two files count as two files: {envelope}"
);
let entries = envelope["fixes"].as_array().expect("fix entries");
assert_eq!(entries.len(), 2, "one entry per file: {envelope}");
for entry in entries {
assert_eq!(entry["type"], "skipped", "{entry}");
assert_eq!(
entry["skip_reason"], "low_confidence_incomplete_analysis",
"{entry}"
);
assert!(
entry.get("name").is_none() && entry.get("line").is_none(),
"a withheld export removal names no export and no line: {entry}"
);
}
assert_eq!(envelope["skipped_low_confidence_dependencies"], 0);
assert_eq!(envelope["skipped_low_confidence_members"], 0);
}
#[test]
fn the_withheld_fix_note_needs_fallow_root_under_an_external_root() {
let dir = tempfile::tempdir().expect("temporary project");
let root = dir.path().join("project");
write_skipped_export_importer_project(&root);
let elsewhere = dir.path();
let without: serde_json::Value = serde_json::from_str(&run_fallow(
&root,
elsewhere,
None,
&["check", "--format", "review-github"],
))
.expect("review envelope");
let body = without["comments"][0]["body"]
.as_str()
.expect("inline comment body");
assert!(
body.contains("caveat: incomplete import graph"),
"the caveat text does not depend on reading the source: {body}"
);
assert!(
!body.contains("```suggestion") && !body.contains("No one-click fix offered"),
"neither the block nor its replacement renders without `FALLOW_ROOT`: {body}"
);
let with: serde_json::Value = serde_json::from_str(&run_fallow(
&root,
elsewhere,
Some(&root),
&["check", "--format", "review-github"],
))
.expect("review envelope");
let body = with["comments"][0]["body"]
.as_str()
.expect("inline comment body");
assert!(
body.contains("No one-click fix offered") && !body.contains("```suggestion"),
"with `FALLOW_ROOT` the note replaces the withheld block: {body}"
);
}