Files
codebase-memory-mcp/tests/test_configlink.c
T
Andrew Hundt e02c6fb34f fix(pipeline): stabilize incremental graph parity
Make duplicate graph-buffer upserts and worker merges choose richer source spans for definition-like nodes, so full and containment rebuilds no longer depend on worker order for declaration-vs-definition locations.

Pass C/C++ macro extraction mode through extraction contexts instead of relying on process-global pipeline state, preventing MCP runs with mixed modes from leaking macro-node policy across pipelines.

Recompute configlink-owned CONFIGURES edges deterministically by clearing only key_symbol/dependency_import strategy edges, preserving direct CONFIGURES links, and sizing candidate arrays from discovered graph counts instead of fixed caps. Reuse cbm_path_base for manifest basename handling and make it recognize both POSIX and Windows separators.

Validation: git diff --check; ASan/UBSan test-runner rebuild /private/tmp/cbm-pan4-20260702T112332Z-build-test-runner-after-basename.log; str_util 65/65 and configlink 9/9 /private/tmp/cbm-pan4-20260702T112531Z-affected-strutil-configlink.log; product build /private/tmp/cbm-pan4-20260702T112545Z-cbm-build-after-configlink.log; MCP self-dogfood one-source canonical equality /private/tmp/cbm-pan4-20260702T112614Z-one-source-mcp-after-configlink.json; graph_buffer 63/63 and extraction 256/256 /private/tmp/cbm-pan4-20260702T112822Z-affected-graphbuffer-extraction.log; incremental 161/161 /private/tmp/cbm-pan4-20260702T112851Z-incremental-suite.log.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-07-02 07:37:19 -04:00

422 lines
16 KiB
C

/*
* test_configlink.c — Tests for config ↔ code linking strategies.
*
* Ported from internal/pipeline/configlink_test.go (9 tests).
* Uses unit test approach: set up gbuf state directly, run strategies,
* check CONFIGURES edges.
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "graph_buffer/graph_buffer.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdatomic.h>
/* Helper: wrap old (gb, project, repo_path) call into ctx-based API */
static int run_configlink(cbm_gbuf_t *gb, const char *project, const char *repo_path) {
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {
.project_name = project,
.repo_path = repo_path,
.gbuf = gb,
.cancelled = &cancelled,
};
return cbm_pipeline_pass_configlink(&ctx);
}
#include <sys/stat.h>
#include <unistd.h>
/* ── Helpers ─────────────────────────────────────────────────────── */
/* Helper to check if any CONFIGURES edge has a given strategy (parsed from JSON) */
static bool has_strategy(const cbm_gbuf_edge_t **edges, int count, const char *strategy) {
char needle[64];
snprintf(needle, sizeof(needle), "\"strategy\":\"%s\"", strategy);
for (int i = 0; i < count; i++) {
if (edges[i]->properties_json && strstr(edges[i]->properties_json, needle))
return true;
}
return false;
}
/* Check if any CONFIGURES edge has given strategy AND confidence. */
static bool has_strategy_with_confidence(const cbm_gbuf_edge_t **edges, int count,
const char *strategy, double confidence) {
char strat_needle[64];
snprintf(strat_needle, sizeof(strat_needle), "\"strategy\":\"%s\"", strategy);
char conf_needle[64];
snprintf(conf_needle, sizeof(conf_needle), "\"confidence\":%.2f", confidence);
for (int i = 0; i < count; i++) {
if (edges[i]->properties_json && strstr(edges[i]->properties_json, strat_needle) &&
strstr(edges[i]->properties_json, conf_needle))
return true;
}
return false;
}
/* Check if any CONFIGURES edge has given strategy AND config_key. */
static bool has_strategy_with_key(const cbm_gbuf_edge_t **edges, int count, const char *strategy,
const char *config_key) {
char strat_needle[64];
snprintf(strat_needle, sizeof(strat_needle), "\"strategy\":\"%s\"", strategy);
char key_needle[128];
snprintf(key_needle, sizeof(key_needle), "\"config_key\":\"%s\"", config_key);
for (int i = 0; i < count; i++) {
if (edges[i]->properties_json && strstr(edges[i]->properties_json, strat_needle) &&
strstr(edges[i]->properties_json, key_needle))
return true;
}
return false;
}
static int count_strategy(const cbm_gbuf_edge_t **edges, int count, const char *strategy) {
char needle[64];
snprintf(needle, sizeof(needle), "\"strategy\":\"%s\"", strategy);
int found = 0;
for (int i = 0; i < count; i++) {
if (edges[i]->properties_json && strstr(edges[i]->properties_json, needle)) {
found++;
}
}
return found;
}
static int count_prop_substr(const cbm_gbuf_edge_t **edges, int count, const char *needle) {
int found = 0;
for (int i = 0; i < count; i++) {
if (edges[i]->properties_json && strstr(edges[i]->properties_json, needle)) {
found++;
}
}
return found;
}
/* Recursive remove */
static void rm_rf(const char *path) {
th_rmtree(path);
}
/* ── Strategy 1: Config Key → Code Symbol ───────────────────────── */
/* Go: TestConfigKeySymbol_ExactMatch
* config.toml has max_connections, main.go has getMaxConnections() */
TEST(configlink_key_symbol_exact_match) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
/* Config Variable node: max_connections from config.toml */
int64_t cfg_id = cbm_gbuf_upsert_node(gb, "Variable", "max_connections",
"test.config.max_connections", "config.toml", 0, 0, NULL);
ASSERT_GT(cfg_id, 0);
/* Code Function node: getMaxConnections from main.go */
int64_t func_id = cbm_gbuf_upsert_node(gb, "Function", "getMaxConnections",
"test.main.getMaxConnections", "main.go", 0, 0, NULL);
ASSERT_GT(func_id, 0);
/* Run configlink */
int n = run_configlink(gb, "test", NULL);
ASSERT_GT(n, 0);
/* Check CONFIGURES edges */
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
ASSERT_TRUE(has_strategy(edges, count, "key_symbol"));
cbm_gbuf_free(gb);
PASS();
}
/* Go: TestConfigKeySymbol_SubstringMatch
* config.toml has request_timeout, main.go has getRequestTimeoutSeconds() */
TEST(configlink_key_symbol_substring_match) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
cbm_gbuf_upsert_node(gb, "Variable", "request_timeout", "test.config.request_timeout",
"config.toml", 0, 0, NULL);
cbm_gbuf_upsert_node(gb, "Function", "getRequestTimeoutSeconds",
"test.main.getRequestTimeoutSeconds", "main.go", 0, 0, NULL);
run_configlink(gb, "test", NULL);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
ASSERT_TRUE(has_strategy_with_confidence(edges, count, "key_symbol", 0.75));
cbm_gbuf_free(gb);
PASS();
}
/* Go: TestConfigKeySymbol_ShortKeySkipped
* config.toml has port, host, name — all 1-token keys, should be skipped */
TEST(configlink_key_symbol_short_key_skipped) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
/* Short single-token config keys */
const char *short_keys[] = {"port", "host", "name"};
for (int i = 0; i < 3; i++) {
char qn[64];
snprintf(qn, sizeof(qn), "test.config.%s", short_keys[i]);
cbm_gbuf_upsert_node(gb, "Variable", short_keys[i], qn, "config.toml", 0, 0, NULL);
}
cbm_gbuf_upsert_node(gb, "Function", "getPort", "test.main.getPort", "main.go", 0, 0, NULL);
run_configlink(gb, "test", NULL);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
/* No key_symbol edges for 1-token keys */
ASSERT_FALSE(has_strategy(edges, count, "key_symbol"));
cbm_gbuf_free(gb);
PASS();
}
/* Go: TestConfigKeySymbol_CamelCaseNormalization
* config.json has maxRetryCount, handler.go has getMaxRetryCount() */
TEST(configlink_key_symbol_camel_case) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
cbm_gbuf_upsert_node(gb, "Variable", "maxRetryCount", "test.config.maxRetryCount",
"config.json", 0, 0, NULL);
cbm_gbuf_upsert_node(gb, "Function", "getMaxRetryCount", "test.handler.getMaxRetryCount",
"handler.go", 0, 0, NULL);
run_configlink(gb, "test", NULL);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
ASSERT_TRUE(has_strategy(edges, count, "key_symbol"));
cbm_gbuf_free(gb);
PASS();
}
/* Go: TestConfigKeySymbol_NoFalsePositive
* config.toml has url — 1-token key, should not match parseURL */
TEST(configlink_key_symbol_no_false_positive) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
cbm_gbuf_upsert_node(gb, "Variable", "url", "test.db.url", "config.toml", 0, 0, NULL);
cbm_gbuf_upsert_node(gb, "Function", "parseURL", "test.main.parseURL", "main.go", 0, 0, NULL);
run_configlink(gb, "test", NULL);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
/* url is 1 token → skipped by key_symbol strategy */
ASSERT_FALSE(has_strategy_with_key(edges, count, "key_symbol", "url"));
cbm_gbuf_free(gb);
PASS();
}
/* ── Strategy 2: Dependency → Import ────────────────────────────── */
/* Go: TestDependencyImport_PackageJson
* package.json has express dep, app.js imports express */
TEST(configlink_dep_import_package_json) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
/* Dependency Variable in package.json */
cbm_gbuf_upsert_node(gb, "Variable", "express", "test.package.dependencies.express",
"package.json", 0, 0, NULL);
/* Module node for app.js (source of import) */
int64_t app_id = cbm_gbuf_upsert_node(gb, "Module", "app", "test.app", "app.js", 0, 0, NULL);
/* Import target node (what 'express' resolves to) */
int64_t target_id = cbm_gbuf_upsert_node(gb, "Module", "express", "test.node_modules.express",
"node_modules/express/index.js", 0, 0, NULL);
/* IMPORTS edge: app.js → express module */
cbm_gbuf_insert_edge(gb, app_id, target_id, "IMPORTS", NULL);
run_configlink(gb, "test", NULL);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
/* Should find dependency_import edge linking app.js to package.json dep */
ASSERT_TRUE(has_strategy(edges, count, "dependency_import"));
cbm_gbuf_free(gb);
PASS();
}
TEST(configlink_recompute_replaces_stale_derived_edges) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
int64_t cfg_id = cbm_gbuf_upsert_node(gb, "Variable", "service_timeout",
"test.config.service_timeout", "config.toml", 0, 0,
NULL);
int64_t current_id = cbm_gbuf_upsert_node(gb, "Function", "getServiceTimeout",
"test.main.getServiceTimeout", "main.go", 0, 0,
NULL);
int64_t stale_key_id = cbm_gbuf_upsert_node(gb, "Function", "oldLineComment",
"test.old.oldLineComment", "old.c", 0, 0, NULL);
int64_t stale_dep_id = cbm_gbuf_upsert_node(gb, "Module", "old_dep_source",
"test.old.dep_source", "old.js", 0, 0, NULL);
int64_t direct_id = cbm_gbuf_upsert_node(gb, "Function", "readEnv",
"test.env.readEnv", "env.c", 0, 0, NULL);
ASSERT_GT(cfg_id, 0);
ASSERT_GT(current_id, 0);
ASSERT_GT(stale_key_id, 0);
ASSERT_GT(stale_dep_id, 0);
ASSERT_GT(direct_id, 0);
ASSERT_GT(cbm_gbuf_insert_edge(gb, stale_key_id, cfg_id, "CONFIGURES",
"{\"strategy\":\"key_symbol\",\"confidence\":0.75,"
"\"config_key\":\"line_comment\"}"),
0);
ASSERT_GT(cbm_gbuf_insert_edge(gb, stale_dep_id, cfg_id, "CONFIGURES",
"{\"strategy\":\"dependency_import\",\"confidence\":0.95,"
"\"dep_name\":\"old\"}"),
0);
ASSERT_GT(cbm_gbuf_insert_edge(gb, direct_id, cfg_id, "CONFIGURES",
"{\"source\":\"env_access\"}"),
0);
int n = run_configlink(gb, "test", NULL);
ASSERT_EQ(n, 1);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
ASSERT_EQ(count_strategy(edges, count, "key_symbol"), 1);
ASSERT_EQ(count_strategy(edges, count, "dependency_import"), 0);
ASSERT_TRUE(has_strategy_with_key(edges, count, "key_symbol", "service_timeout"));
ASSERT_EQ(count_prop_substr(edges, count, "\"source\":\"env_access\""), 1);
cbm_gbuf_free(gb);
PASS();
}
TEST(configlink_key_symbol_searches_beyond_former_code_cap) {
enum {
CONFIGLINK_FORMER_CODE_ENTRY_CAP = 8192,
CONFIGLINK_CAP_REGRESSION_FILLER_COUNT = CONFIGLINK_FORMER_CODE_ENTRY_CAP + CBM_SZ_8,
};
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
ASSERT_NOT_NULL(gb);
int64_t cfg_id = cbm_gbuf_upsert_node(gb, "Variable", "target_value",
"test.config.target_value", "config.toml", 0, 0,
NULL);
ASSERT_GT(cfg_id, 0);
for (int i = 0; i < CONFIGLINK_CAP_REGRESSION_FILLER_COUNT; i++) {
char name[CBM_SZ_64];
char qn[CBM_SZ_128];
int nn = snprintf(name, sizeof(name), "filler_%04d", i);
int qn_n = snprintf(qn, sizeof(qn), "test.main.filler_%04d", i);
ASSERT_GT(nn, 0);
ASSERT_LT((size_t)nn, sizeof(name));
ASSERT_GT(qn_n, 0);
ASSERT_LT((size_t)qn_n, sizeof(qn));
ASSERT_GT(cbm_gbuf_upsert_node(gb, "Function", name, qn, "main.go", 0, 0, NULL), 0);
}
ASSERT_GT(cbm_gbuf_upsert_node(gb, "Function", "getTargetValue",
"test.main.getTargetValue", "main.go", 0, 0, NULL),
0);
int n = run_configlink(gb, "test", NULL);
ASSERT_EQ(n, 1);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
ASSERT_TRUE(has_strategy_with_key(edges, count, "key_symbol", "target_value"));
cbm_gbuf_free(gb);
PASS();
}
/* ── Strategy 3: Config File Path → Code String Reference ───────── */
/* Go: TestConfigFileRef_ExactPath
* main.go references "config/database.toml" in source code */
/* Go: TestConfigFileRef_BasenameMatch
* main.go references "settings.yaml" by basename */
/* Go: TestConfigFileRef_NoFalsePositive
* main.go references "data.csv" — not a config extension */
TEST(configlink_file_ref_no_false_positive) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_cfglink_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmpdir));
/* Create data.csv */
char csv_path[512];
snprintf(csv_path, sizeof(csv_path), "%s/data.csv", tmpdir);
FILE *f = fopen(csv_path, "w");
fprintf(f, "a,b,c\n");
fclose(f);
/* Create main.go referencing data.csv */
char main_path[512];
snprintf(main_path, sizeof(main_path), "%s/main.go", tmpdir);
f = fopen(main_path, "w");
fprintf(f, "package main\n\nfunc loadData() {\n"
"\tf := readFile(\"data.csv\")\n"
"\t_ = f\n}\n");
fclose(f);
char *project = cbm_project_name_from_path(tmpdir);
cbm_gbuf_t *gb = cbm_gbuf_new(project, tmpdir);
/* data.csv Module (NOT a config extension) */
char *csv_qn = cbm_pipeline_fqn_module(project, "data.csv");
cbm_gbuf_upsert_node(gb, "Module", "data", csv_qn, "data.csv", 0, 0, NULL);
/* Source Module */
char *main_qn = cbm_pipeline_fqn_module(project, "main.go");
cbm_gbuf_upsert_node(gb, "Module", "main", main_qn, "main.go", 0, 0, NULL);
run_configlink(gb, project, tmpdir);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
/* .csv is not a config extension — no file_reference edges */
ASSERT_FALSE(has_strategy(edges, count, "file_reference"));
cbm_gbuf_free(gb);
free(csv_qn);
free(main_qn);
free(project);
rm_rf(tmpdir);
PASS();
}
/* ── Suite ───────────────────────────────────────────────────────── */
SUITE(configlink) {
/* Strategy 1: Key → Symbol */
RUN_TEST(configlink_key_symbol_exact_match);
RUN_TEST(configlink_key_symbol_substring_match);
RUN_TEST(configlink_key_symbol_short_key_skipped);
RUN_TEST(configlink_key_symbol_camel_case);
RUN_TEST(configlink_key_symbol_no_false_positive);
/* Strategy 2: Dependency → Import */
RUN_TEST(configlink_dep_import_package_json);
RUN_TEST(configlink_recompute_replaces_stale_derived_edges);
RUN_TEST(configlink_key_symbol_searches_beyond_former_code_cap);
/* Strategy 3: File Path → Reference */
RUN_TEST(configlink_file_ref_no_false_positive);
}