Summary
In C++ projects, multiple GoogleTest TEST(...) cases defined in the same file collapse into a single graph node. Only the last one survives, and the call edges of all the others are dropped.
The practical effect is that trace_path / caller analysis silently under-reports test callers: in a test-heavy C++ codebase, most of the tests that exercise a symbol become invisible.
Environment
codebase-memory-mcp 0.9.0
- macOS 15 (Darwin 25.6.0), arm64
- Indexed with
index_repository(mode="full")
Minimal reproduction
Four files, no build required (indexing is static; GoogleTest does not need to be installed).
src/widget.hpp
#pragma once
namespace demo {
int assembleWidget(int size);
} // namespace demo
src/widget.cpp
#include "widget.hpp"
namespace demo {
int assembleWidget(int size) { return size * 2; }
} // namespace demo
tests/direct_test.cpp — three test cases in one file, each calling the symbol directly
#include "widget.hpp"
#include <gtest/gtest.h>
TEST(WidgetSuite, DoublesSmallSize) { EXPECT_EQ(demo::assembleWidget(1), 2); }
TEST(WidgetSuite, DoublesZero) { EXPECT_EQ(demo::assembleWidget(0), 0); }
TEST(WidgetSuite, DoublesLargeSize) {
EXPECT_EQ(demo::assembleWidget(1000), 2000);
}
tests/indirect_test.cpp — one test reaching the symbol through a helper
#include "widget.hpp"
#include <gtest/gtest.h>
namespace {
int makeFixture() { return demo::assembleWidget(21); }
} // namespace
TEST(FixtureSuite, UsesHelper) { EXPECT_EQ(makeFixture(), 42); }
Steps
index_repository(repo_path="<repro>", name="gtest-caller-repro", mode="full")
search_graph(project="gtest-caller-repro", file_pattern="%direct_test%")
trace_path(project="gtest-caller-repro", function_name="assembleWidget",
direction="inbound", include_tests=true, depth=3)
Expected
tests/direct_test.cpp contributes three distinct function nodes (one per test case), each with a CALLS edge to assembleWidget.
Actual
search_graph returns exactly one node for the whole file:
{
"name": "TEST",
"qualified_name": "gtest-caller-repro.tests.direct_test.TEST",
"file_path": "tests/direct_test.cpp",
"signature": "(WidgetSuite, DoublesLargeSize)",
"lines": 3,
"out_degree": 1
}
signature is the last test in the file — the first two are gone.
out_degree is 1, so two of the three call sites were dropped.
trace_path correspondingly reports one caller for that file instead of three:
{"function":"assembleWidget","direction":"inbound","callers":[
{"name":"TEST","qualified_name":"gtest-caller-repro.tests.direct_test.TEST","hop":1},
{"name":"makeFixture","qualified_name":"gtest-caller-repro.tests.indirect_test.makeFixture","hop":1},
{"name":"TEST","qualified_name":"gtest-caller-repro.tests.indirect_test.TEST","hop":2}
]}
Why this happens (guess)
TEST(Suite, Case) parses as a function-like construct whose name is TEST, so every case in a file maps to the same qualified name <...>.<file>.TEST and the nodes overwrite each other. The same applies to TEST_F, TEST_P, TYPED_TEST, and to any macro-defined entry point that repeats a name within a file (e.g. Catch2's TEST_CASE, which is name-plus-string).
A fix could derive the qualified name from the macro arguments (...direct_test.TEST.WidgetSuite.DoublesZero) or disambiguate by start line.
Impact
Caller and impact analysis under-count test coverage of a symbol. For C++ repositories where GoogleTest is the norm, a file with N test cases reports 1 caller instead of N, and "which tests exercise this function" — a primary reason to consult the graph before editing — becomes unreliable.
Secondary observation (not minimally reproducible)
On a large C++ codebase (~129k nodes / 383k edges) I hit a stronger version of this: a free function in a nested namespace is called directly by five same-named TEST cases in one file, and trace_path(direction="inbound", depth=3, include_tests=true) returns no caller from that file at all — only callers from a different file. The collapsed node for that file does exist, is reachable via search_graph, has out_degree: 12, and its body tokens include the callee's name — yet no inbound edge is reported.
I could not reduce this to a minimal case: in the repro above the direct edge is found (including when the call goes through a using-declaration with an unqualified call). So it may be a separate resolution issue that only appears at scale, or an additional consequence of the collapsing above. Happy to gather more diagnostics if you can suggest what would be useful — I can query the graph but cannot share that codebase.
Summary
In C++ projects, multiple GoogleTest
TEST(...)cases defined in the same file collapse into a single graph node. Only the last one survives, and the call edges of all the others are dropped.The practical effect is that
trace_path/ caller analysis silently under-reports test callers: in a test-heavy C++ codebase, most of the tests that exercise a symbol become invisible.Environment
codebase-memory-mcp0.9.0index_repository(mode="full")Minimal reproduction
Four files, no build required (indexing is static; GoogleTest does not need to be installed).
src/widget.hppsrc/widget.cpptests/direct_test.cpp— three test cases in one file, each calling the symbol directlytests/indirect_test.cpp— one test reaching the symbol through a helperSteps
Expected
tests/direct_test.cppcontributes three distinct function nodes (one per test case), each with aCALLSedge toassembleWidget.Actual
search_graphreturns exactly one node for the whole file:{ "name": "TEST", "qualified_name": "gtest-caller-repro.tests.direct_test.TEST", "file_path": "tests/direct_test.cpp", "signature": "(WidgetSuite, DoublesLargeSize)", "lines": 3, "out_degree": 1 }signatureis the last test in the file — the first two are gone.out_degreeis 1, so two of the three call sites were dropped.trace_pathcorrespondingly reports one caller for that file instead of three:{"function":"assembleWidget","direction":"inbound","callers":[ {"name":"TEST","qualified_name":"gtest-caller-repro.tests.direct_test.TEST","hop":1}, {"name":"makeFixture","qualified_name":"gtest-caller-repro.tests.indirect_test.makeFixture","hop":1}, {"name":"TEST","qualified_name":"gtest-caller-repro.tests.indirect_test.TEST","hop":2} ]}Why this happens (guess)
TEST(Suite, Case)parses as a function-like construct whose name isTEST, so every case in a file maps to the same qualified name<...>.<file>.TESTand the nodes overwrite each other. The same applies toTEST_F,TEST_P,TYPED_TEST, and to any macro-defined entry point that repeats a name within a file (e.g. Catch2'sTEST_CASE, which is name-plus-string).A fix could derive the qualified name from the macro arguments (
...direct_test.TEST.WidgetSuite.DoublesZero) or disambiguate by start line.Impact
Caller and impact analysis under-count test coverage of a symbol. For C++ repositories where GoogleTest is the norm, a file with N test cases reports 1 caller instead of N, and "which tests exercise this function" — a primary reason to consult the graph before editing — becomes unreliable.
Secondary observation (not minimally reproducible)
On a large C++ codebase (~129k nodes / 383k edges) I hit a stronger version of this: a free function in a nested namespace is called directly by five same-named
TESTcases in one file, andtrace_path(direction="inbound", depth=3, include_tests=true)returns no caller from that file at all — only callers from a different file. The collapsed node for that file does exist, is reachable viasearch_graph, hasout_degree: 12, and its body tokens include the callee's name — yet no inbound edge is reported.I could not reduce this to a minimal case: in the repro above the direct edge is found (including when the call goes through a
using-declaration with an unqualified call). So it may be a separate resolution issue that only appears at scale, or an additional consequence of the collapsing above. Happy to gather more diagnostics if you can suggest what would be useful — I can query the graph but cannot share that codebase.