Skip to content

Commit 17540f9

Browse files
authored
Merge PR #8: PR-review test-detection + risk calibration (0.19.1)
fix(pr-review): test-detection + risk calibration — v0.19.1
2 parents 29ee815 + bec92b1 commit 17540f9

9 files changed

Lines changed: 100 additions & 24 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ members = [
6060
]
6161

6262
[workspace.package]
63-
version = "0.19.0"
63+
version = "0.19.1"
6464
edition = "2021"
6565
license = "Apache-2.0"
6666
repository = "https://github.com/codegraph-ai/codegraph"

crates/codegraph-rust/src/mapper.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ pub fn ir_to_graph(
7171
.with("line_end", func.line_end as i64)
7272
.with("is_async", func.is_async)
7373
.with("is_static", func.is_static)
74-
.with("is_abstract", func.is_abstract);
74+
.with("is_abstract", func.is_abstract)
75+
.with("is_test", func.is_test);
7576

7677
// Add complexity metrics if available
7778
if let Some(ref complexity) = func.complexity {

crates/codegraph-rust/src/visitor.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,33 @@ fn test_something() {}
13201320
assert!(visitor.functions[0].is_test);
13211321
}
13221322

1323+
#[test]
1324+
fn test_visitor_test_fn_inside_cfg_test_mod() {
1325+
// The idiomatic Rust unit-test shape: a #[test] fn with a descriptive
1326+
// (non-`test_`) name inside `#[cfg(test)] mod tests`. This is what the
1327+
// PR-review coverage analysis was missing.
1328+
let source = r#"
1329+
fn weighted_mean_l2() {}
1330+
1331+
#[cfg(test)]
1332+
mod tests {
1333+
use super::*;
1334+
1335+
#[test]
1336+
fn weighted_mean_l2_math() {
1337+
weighted_mean_l2();
1338+
}
1339+
}
1340+
"#;
1341+
let visitor = parse_and_visit(source);
1342+
let t = visitor
1343+
.functions
1344+
.iter()
1345+
.find(|f| f.name == "weighted_mean_l2_math")
1346+
.expect("nested test fn should be visited");
1347+
assert!(t.is_test, "#[test] fn inside `mod tests` must set is_test");
1348+
}
1349+
13231350
#[test]
13241351
fn test_visitor_visibility_modifiers() {
13251352
let source = r#"

crates/codegraph-server/src/domain/node_props.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,11 @@ pub(crate) fn is_public(node: &Node) -> bool {
109109
.or_else(|| node.properties.get_bool("exported"))
110110
.unwrap_or_else(|| matches!(visibility(node), "public" | "pub"))
111111
}
112+
113+
/// Whether the node is a test function, as recorded at index time from the
114+
/// language's test marker (`#[test]`/`#[cfg(test)]`, `@Test`, etc.). This is
115+
/// the structural signal; callers should prefer it over name heuristics, which
116+
/// miss idiomatic test names (Rust `#[cfg(test)] mod tests { fn descriptive() }`).
117+
pub(crate) fn is_test(node: &Node) -> bool {
118+
node.properties.get_bool("is_test").unwrap_or(false)
119+
}

crates/codegraph-server/src/mcp/server.rs

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4046,6 +4046,10 @@ impl McpServer {
40464046
let mut all_tests = Vec::new();
40474047
let mut untested_functions = Vec::new();
40484048
let mut total_direct = 0usize;
4049+
// Callers of signature-changed functions — the ones that can
4050+
// actually break. Body-only changes don't break callers, so
4051+
// they shouldn't drive the risk level or blast radius.
4052+
let mut breaking_callers = 0usize;
40494053
let mut all_affected_files = std::collections::HashSet::new();
40504054
let mut func_details = Vec::new();
40514055

@@ -4104,12 +4108,25 @@ impl McpServer {
41044108
if let Ok(caller) = graph.get_node(caller_id) {
41054109
let cname = crate::domain::node_props::name(caller);
41064110
let cfile = caller.properties.get_string("path").unwrap_or("");
4107-
let is_test = cname.to_lowercase().starts_with("test_")
4111+
// Prefer the structural is_test marker recorded at index time
4112+
// (#[test]/#[cfg(test)], @Test, …); fall back to name/path
4113+
// heuristics only for languages that don't populate it. The
4114+
// heuristics alone miss idiomatic Rust tests with descriptive
4115+
// names inside `#[cfg(test)] mod tests`.
4116+
let is_test = crate::domain::node_props::is_test(caller)
4117+
|| cname.to_lowercase().starts_with("test_")
41084118
|| cname.to_lowercase().contains("_test")
41094119
|| cfile.contains("/tests/")
41104120
|| cfile.contains("/test_");
4111-
4112-
if is_test {
4121+
// Callers under examples/ (and doctests) exercise the
4122+
// function at runtime — count them as coverage, not as
4123+
// breakable production callers. This is what covers code
4124+
// driven only by eval/example harnesses.
4125+
let is_exercising = !is_test
4126+
&& (cfile.contains("/examples/")
4127+
|| cfile.contains("/benches/"));
4128+
4129+
if is_test || is_exercising {
41134130
has_test_caller = true;
41144131
all_tests.push(serde_json::json!({
41154132
"test": cname, "file": cfile, "covers": func_name,
@@ -4128,11 +4145,18 @@ impl McpServer {
41284145
}
41294146
}
41304147
total_direct += caller_count as usize;
4148+
if change_type == "signature_changed" {
4149+
breaking_callers += caller_count as usize;
4150+
}
41314151

41324152
// #87: Test gap — function has no test callers.
41334153
// Skip functions that ARE tests (they don't need
41344154
// their own coverage) and trivial getters/setters.
4135-
let fn_is_test = func_name.to_lowercase().starts_with("test_")
4155+
let fn_is_test = graph
4156+
.get_node(*node_id)
4157+
.ok()
4158+
.is_some_and(|n| crate::domain::node_props::is_test(n))
4159+
|| func_name.to_lowercase().starts_with("test_")
41364160
|| func_name.to_lowercase().contains("_test")
41374161
|| changed_rel[idx].contains("/tests/")
41384162
|| changed_rel[idx].contains("_test.");
@@ -4282,10 +4306,29 @@ impl McpServer {
42824306

42834307
drop(graph);
42844308

4285-
// Risk level
4286-
let risk_level = if total_direct > 20 || untested_functions.len() > 5 {
4309+
// Functions actually touched by this PR (denominator for ratios).
4310+
let total_functions: u64 = file_impacts
4311+
.iter()
4312+
.map(|f| f["functions_changed"].as_u64().unwrap_or(0))
4313+
.sum();
4314+
4315+
// Risk is driven by what can actually break — callers of
4316+
// signature-changed functions — and by the share of touched
4317+
// functions left untested, NOT by the raw caller count (which
4318+
// body-only changes inflate, e.g. a widely-called helper whose
4319+
// body changed but signature didn't).
4320+
let untested_ratio = if total_functions > 0 {
4321+
untested_functions.len() as f64 / total_functions as f64
4322+
} else {
4323+
0.0
4324+
};
4325+
let risk_level = if breaking_callers > 25
4326+
|| (untested_functions.len() > 5 && untested_ratio > 0.5)
4327+
{
42874328
"high"
4288-
} else if total_direct > 5 || untested_functions.len() > 2 {
4329+
} else if breaking_callers > 8
4330+
|| (untested_functions.len() > 2 && untested_ratio > 0.25)
4331+
{
42894332
"medium"
42904333
} else {
42914334
"low"
@@ -4305,28 +4348,24 @@ impl McpServer {
43054348
commit_prefix, primary_module
43064349
);
43074350

4308-
let total_functions: u64 = file_impacts
4309-
.iter()
4310-
.map(|f| f["functions_changed"].as_u64().unwrap_or(0))
4311-
.sum();
4312-
43134351
let mut result = serde_json::json!({
43144352
"base_branch": base,
43154353
"changed_files": changed_rel.len(),
43164354
"lines_added": lines_added,
43174355
"lines_removed": lines_removed,
43184356
"functions_touched": total_functions,
43194357
"direct_callers": total_direct,
4358+
"breaking_callers": breaking_callers,
43204359
"related_tests": unique_tests.len(),
43214360
"untested_functions": untested_functions.len(),
43224361
"affected_modules": affected_modules,
43234362
"risk_level": risk_level,
43244363
"commit_hint": commit_hint,
43254364
"files": file_impacts,
43264365
"message": format!(
4327-
"PR changes {} files (+{}/-{}, {} functions). {} direct callers, {} tests, {} untested. Risk: {}.",
4366+
"PR changes {} files (+{}/-{}, {} functions). {} direct callers ({} breaking), {} tests, {} untested. Risk: {}.",
43284367
changed_rel.len(), lines_added, lines_removed, total_functions,
4329-
total_direct, unique_tests.len(), untested_functions.len(), risk_level,
4368+
total_direct, breaking_callers, unique_tests.len(), untested_functions.len(), risk_level,
43304369
),
43314370
});
43324371

@@ -4385,9 +4424,10 @@ impl McpServer {
43854424

43864425
if total_direct > 0 {
43874426
md.push_str(&format!(
4388-
"### Blast radius\n{} direct caller{} affected",
4427+
"### Blast radius\n{} direct caller{} affected ({} breaking)",
43894428
total_direct,
43904429
if total_direct == 1 { "" } else { "s" },
4430+
breaking_callers,
43914431
));
43924432
if !affected_modules.is_empty() {
43934433
let mods: Vec<String> = affected_modules

mcp-package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@astudioplus/codegraph-mcp",
3-
"version": "0.19.0",
3+
"version": "0.19.1",
44
"mcpName": "io.github.codegraph-ai/codegraph",
55
"description": "CodeGraph MCP server — cross-language code intelligence with 42 tools, 38 languages",
66
"author": "Andrey Vasilevsky <anvanster@gmail.com>",

mcp-package/server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"url": "https://github.com/codegraph-ai/CodeGraph",
77
"source": "github"
88
},
9-
"version": "0.19.0",
9+
"version": "0.19.1",
1010
"packages": [
1111
{
1212
"registryType": "npm",
1313
"identifier": "@astudioplus/codegraph-mcp",
14-
"version": "0.19.0",
14+
"version": "0.19.1",
1515
"transport": {
1616
"type": "stdio"
1717
},

vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "codegraph",
33
"displayName": "CodeGraph",
44
"description": "Cross-language code intelligence powered by graph analysis",
5-
"version": "0.19.0",
5+
"version": "0.19.1",
66
"publisher": "aStudioPlus",
77
"author": "Andrey Vasilevsky <anvanster@gmail.com>",
88
"license": "Apache-2.0",

0 commit comments

Comments
 (0)