use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use regex::Regex;
use rustcodegraph::mcp::tools::{ToolHandler, ToolResult};
use rustcodegraph::{CodeGraph, IndexOptions};
use serde_json::{Map, Value, json};
const FILE_VIEW_STATUS: &str = "Rust CodeGraph MCP node file-view cases are active";
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TempProject {
root: PathBuf,
}
impl TempProject {
fn new() -> Self {
for attempt in 0..100 {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after Unix epoch")
.as_nanos();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"cg-fileview-{}-{unique}-{counter}-{attempt}",
std::process::id()
));
match fs::create_dir(&root) {
Ok(()) => {
fs::create_dir(root.join("src")).unwrap_or_else(|err| {
panic!("failed to create temp src dir {}: {err}", root.display())
});
return Self { root };
}
Err(err) if err.kind() == ErrorKind::AlreadyExists => continue,
Err(err) => panic!("failed to create temp dir {}: {err}", root.display()),
}
}
panic!("failed to allocate a unique cg-fileview temp directory")
}
fn path(&self) -> &Path {
&self.root
}
fn write_src(&self, name: &str, contents: &str) {
fs::write(self.root.join("src").join(name), contents)
.unwrap_or_else(|err| panic!("failed to write fixture file {name}: {err}"));
}
}
impl Drop for TempProject {
fn drop(&mut self) {
if self.root.exists() {
let _ = fs::remove_dir_all(&self.root);
}
}
}
struct Fixture {
_temp: TempProject,
cg: CodeGraph,
handler: ToolHandler,
}
impl Fixture {
fn new() -> Self {
let temp = TempProject::new();
temp.write_src(
"a.ts",
"export function helper(x: number) {\n return x + 1;\n}\nexport class Widget {\n build() { return helper(1); }\n}\n",
);
temp.write_src(
"b.ts",
"import { helper } from './a';\n\n// a comment between symbols\nconst SETTING = 7;\nexport function useHelper() { return helper(2) + SETTING; }\n",
);
temp.write_src(
"application.properties",
"spring.datasource.password=SUPERSECRET123\nserver.port=8080\n",
);
let big_body = (0..2000)
.map(|i| format!(" const v{i} = {i};"))
.collect::<Vec<_>>()
.join("\n");
temp.write_src(
"big.ts",
&format!("export function big() {{\n{big_body}\n return 0;\n}}\n"),
);
let mut cg = CodeGraph::init_sync(temp.path()).expect("failed to initialize CodeGraph");
let _ = cg.index_all(IndexOptions::default());
let mut handler = ToolHandler::new(true);
handler.set_default_code_graph(&cg);
Self {
_temp: temp,
cg,
handler,
}
}
fn text(&mut self, args: Map<String, Value>) -> String {
text(&self.handler.execute("rustcodegraph_node", &args))
}
}
impl Drop for Fixture {
fn drop(&mut self) {
self.handler.close_all();
self.cg.close();
}
}
fn text(result: &ToolResult) -> String {
result
.content
.iter()
.map(|content| content.text.as_str())
.collect::<Vec<_>>()
.join("\n")
}
fn file_args(file: &str) -> Map<String, Value> {
let mut args = Map::new();
args.insert("file".to_string(), json!(file));
args
}
fn file_args_with_offset_limit(
file: &str,
offset: usize,
limit: Option<usize>,
) -> Map<String, Value> {
let mut args = file_args(file);
args.insert("offset".to_string(), json!(offset));
if let Some(limit) = limit {
args.insert("limit".to_string(), json!(limit));
}
args
}
fn symbols_only_args(file: &str) -> Map<String, Value> {
let mut args = file_args(file);
args.insert("symbolsOnly".to_string(), json!(true));
args
}
fn symbol_args(symbol: &str, include_code: bool) -> Map<String, Value> {
let mut args = Map::new();
args.insert("symbol".to_string(), json!(symbol));
args.insert("includeCode".to_string(), json!(include_code));
args
}
mod codegraph_node_file_view_read_replacement {
use super::*;
#[test]
fn reads_a_whole_file_like_read_by_default_numbered_tab_lines_no_pad_imports_and_gaps_included()
{
let mut fixture = Fixture::new();
let out = fixture.text(file_args("b.ts"));
assert!(
Regex::new(r"(?m)^1\timport \{ helper \} from '\./a';$")
.expect("regex should compile")
.is_match(&out),
"{out}"
);
assert!(
out.contains("// a comment between symbols"),
"inter-symbol gap should be preserved:\n{out}"
);
assert!(
out.contains("const SETTING = 7"),
"top-level statement should be preserved:\n{out}"
);
assert!(
out.contains("useHelper"),
"symbol body should be present:\n{out}"
);
assert!(
!out.contains("```"),
"Read has no code fence; file-view should not either:\n{out}"
);
}
#[test]
fn leads_with_a_one_line_blast_radius_header_the_value_add_over_read() {
let mut fixture = Fixture::new();
let out = fixture.text(file_args("a.ts"));
assert!(
Regex::new(r"used by 1 file: src/b\.ts")
.expect("regex should compile")
.is_match(&out),
"a.ts should report b.ts as a dependent:\n{out}"
);
assert!(
out.contains("return x + 1"),
"file source should still be returned:\n{out}"
);
}
#[test]
fn offset_limit_narrow_the_window_exactly_like_read() {
let mut fixture = Fixture::new();
let out = fixture.text(file_args_with_offset_limit("big.ts", 1000, Some(3)));
assert!(
Regex::new(r"(?m)^1000\t {2}const v998 = 998;$")
.expect("regex should compile")
.is_match(&out),
"{out}"
);
assert!(
!Regex::new(r"(?m)^1\t")
.expect("regex should compile")
.is_match(&out),
"line 1 should not be shown:\n{out}"
);
assert!(
Regex::new(r"lines 1000[–-]1002 of \d+")
.expect("regex should compile")
.is_match(&out),
"pagination note should report the exact window:\n{out}"
);
}
#[test]
fn an_offset_past_eof_is_reported_not_a_crash() {
let mut fixture = Fixture::new();
let out = fixture.text(file_args_with_offset_limit("a.ts", 9999, None));
assert!(
Regex::new("(?i)past the end")
.expect("regex should compile")
.is_match(&out),
"{out}"
);
}
#[test]
fn paginates_a_large_file_honestly_by_default_lines_1_to_n_of_total_never_silent_truncate() {
let mut fixture = Fixture::new();
let out = fixture.text(file_args("big.ts"));
assert!(
Regex::new(r"lines 1[–-]\d+ of \d+")
.expect("regex should compile")
.is_match(&out),
"large file should include an explicit window note:\n{out}"
);
assert!(
!out.contains("(output truncated)"),
"large file should avoid the generic output truncation marker:\n{out}"
);
assert!(
Regex::new(r"(?m)^1\texport function big")
.expect("regex should compile")
.is_match(&out),
"window head should be real source:\n{out}"
);
}
#[test]
fn does_not_dump_a_config_data_file_yaml_properties_issue_383_secret_safety() {
let mut fixture = Fixture::new();
let out = fixture.text(file_args("application.properties"));
assert!(
!out.contains("SUPERSECRET123"),
"secret value must not reach the agent:\n{out}"
);
assert!(
Regex::new("(?i)config|values withheld")
.expect("regex should compile")
.is_match(&out),
"config/data output should explain that values are withheld:\n{out}"
);
}
#[test]
fn symbols_only_returns_the_structural_map_not_the_source() {
let mut fixture = Fixture::new();
let out = fixture.text(symbols_only_args("a.ts"));
assert!(out.contains("### Symbols"), "{out}");
assert!(out.contains("helper"), "{out}");
assert!(out.contains("Widget"), "{out}");
assert!(
!out.contains("return x + 1"),
"bodies should not be included in the map:\n{out}"
);
}
#[test]
fn still_works_as_a_normal_symbol_lookup_no_regression() {
let mut fixture = Fixture::new();
let out = fixture.text(symbol_args("helper", true));
assert!(out.contains("helper"), "{out}");
assert!(out.contains("return x + 1"), "{out}");
}
#[test]
fn symbol_lookup_includes_a_caller_callee_trail() {
let mut fixture = Fixture::new();
let out = fixture.text(symbol_args("helper", true));
assert!(out.contains("### Trail"), "{out}");
assert!(out.contains("Called by"), "{out}");
assert!(out.contains("useHelper"), "{out}");
assert!(!out.contains("Source output is not available"), "{out}");
}
#[test]
fn a_miss_returns_a_helpful_message_not_a_crash() {
let mut fixture = Fixture::new();
let out = fixture.text(file_args("does-not-exist.ts"));
assert!(
Regex::new("(?i)no indexed file matches")
.expect("regex should compile")
.is_match(&out),
"{out}"
);
}
#[test]
fn file_view_cases_are_active_for_this_port() {
assert_eq!(
FILE_VIEW_STATUS,
"Rust CodeGraph MCP node file-view cases are active"
);
}
}