mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
Merge upstream/main 7d6cdb23 into api-consolidation 0fcb7e10 as a tested capability superset.
Retain the branch's streamlined, classic, and hidden-reveal MCP surfaces; JSON/TOON output; read-only Cypher execution; semantic search; dependency indexing; PageRank; incremental indexing; and modular harness installer. Integrate upstream ObjectScript routine/UDL extraction, macro and return-type call resolution, Python alias and C++ header resolution, atomic SQLite publication, SCC/cycle reporting, Cypher deadlines, and bounded multi-source impact traversal.
Fix merge-specific defects in handle_detect_changes project validation, cbm_pipeline_release_objectscript_tables ownership, no-sanitizer test defines, focused-suite registration, portable config publication, and installed streamlined guidance. The guidance keeps auto-index/get_code/query_graph discovery while adding coverage tiers and removes the stale fixed row ceiling.
Verification: ASan/UBSan full runner 7,155 passed with 27 sandbox-loopback cases separately passing in the escalated HTTP suite (44 passed, 1 Windows-only skip); TSan 449 passed; incremental leak gate 514 passed with 0 leaks; MCP 261; CLI 232; extraction 313; tool consolidation 113; Cypher 179; SQLite writer 16; configuration editors 127; integration 28; language contracts 38; PageRank 60. scripts/check-source-safety.sh and git diff --cached --check pass.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
3755 lines
128 KiB
C
3755 lines
128 KiB
C
/*
|
||
* test_incremental.c — Comprehensive integration tests for indexing pipeline.
|
||
*
|
||
* Clones a real open-source repo (FastAPI 0.99.1, ~1100 Python files), indexes
|
||
* it through the full pipeline, then tests incremental re-indexing with file
|
||
* modifications, additions, deletions, and adversarial inputs.
|
||
*
|
||
* Includes performance metrics: wall-clock time and peak RSS on every index,
|
||
* edge-type-specific assertions, and regression guards.
|
||
*
|
||
* Requires network access for initial clone. Skips gracefully if offline.
|
||
*/
|
||
#include "../src/foundation/compat.h"
|
||
#include "../src/foundation/compat_fs.h"
|
||
#include "../src/foundation/constants.h"
|
||
#include "test_framework.h"
|
||
#include "test_graph_diff.h"
|
||
#include "test_helpers.h"
|
||
#include <cli/cli.h>
|
||
#include <mcp/mcp.h>
|
||
#include <store/store.h>
|
||
#include <pipeline/pipeline.h>
|
||
#include <foundation/log.h>
|
||
#include <foundation/mem.h>
|
||
#include <foundation/platform.h>
|
||
#include <foundation/str_util.h>
|
||
/* Forward decl (foundation/platform.c): honors CBM_CACHE_DIR so the test reads
|
||
* the index from the same dir the pipeline writes it (needed for isolation). */
|
||
const char *cbm_resolve_cache_dir(void);
|
||
|
||
#include <stdarg.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
#include <unistd.h>
|
||
#include <sys/stat.h>
|
||
#include <time.h>
|
||
|
||
/* ── Globals ──────────────────────────────────────────────────────── */
|
||
|
||
static char g_tmpdir[256];
|
||
static char g_repodir[512];
|
||
static char g_dbpath[512];
|
||
static cbm_mcp_server_t *g_srv = NULL;
|
||
static cbm_config_t *g_cfg = NULL;
|
||
static char *g_project = NULL;
|
||
|
||
/* Baseline counts after full index */
|
||
static int g_full_nodes = 0;
|
||
static int g_full_edges = 0;
|
||
static int g_full_calls = 0;
|
||
static int g_full_imports = 0;
|
||
|
||
/* Performance: track peak RSS and timing per test phase */
|
||
static size_t g_rss_before_full = 0;
|
||
static double g_full_index_ms = 0;
|
||
|
||
enum {
|
||
INCR_ACCURACY_NODE_TOLERANCE = 2,
|
||
INCR_ACCURACY_EDGE_TOLERANCE = 50,
|
||
INCR_ACCURACY_CALL_TOLERANCE = 2,
|
||
INCR_FORMATTER_MAX_FILES = 50,
|
||
/* Measured FastAPI 0.99.1 production-build budgets: x86 peaks around
|
||
* 2050-2072 MiB; ARM/16 KiB-page runners peak around 2385 MiB. */
|
||
INCR_FULL_INDEX_BASE_MAX_RSS_DELTA_MB = 2304,
|
||
INCR_FULL_INDEX_LARGE_PAGE_MAX_RSS_DELTA_MB = 2816,
|
||
INCR_LARGE_PAGE_MIN_BYTES = 16384,
|
||
INCR_INSTRUMENTED_TIMEOUT_MULTIPLIER = 4,
|
||
};
|
||
|
||
static const char *INCR_TEST_ARTIFACT_ENV = "CBM_TEST_ARTIFACT_DIR";
|
||
static const char *INCR_TEST_FASTAPI_REPO_ENV = "CBM_TEST_FASTAPI_REPO";
|
||
static const char *INCR_TEST_FASTAPI_CACHE_ENV = "CBM_TEST_FASTAPI_CACHE";
|
||
static const char *INCR_TEST_FASTAPI_URL = "https://github.com/fastapi/fastapi.git";
|
||
static const char *INCR_TEST_FASTAPI_TAG = "0.99.1";
|
||
static const char *INCR_TEST_FASTAPI_COMMIT = "dd4e78ca7b09abdf0d4646fe4697316c021a8b2e";
|
||
static const char *INCR_TEST_FASTAPI_DEFAULT_CACHE_NAME = "cbm-test-fastapi-0.99.1-cache";
|
||
|
||
#ifndef __has_feature
|
||
#define __has_feature(x) 0
|
||
#endif
|
||
|
||
static bool incr_memory_instrumentation_active(void) {
|
||
#if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__) || \
|
||
__has_feature(address_sanitizer) || __has_feature(thread_sanitizer)
|
||
return true;
|
||
#else
|
||
const char *scribble = getenv("MallocScribble");
|
||
const char *pre_scribble = getenv("MallocPreScribble");
|
||
const char *guard_malloc = getenv("DYLD_INSERT_LIBRARIES");
|
||
return (scribble && strcmp(scribble, "1") == 0) ||
|
||
(pre_scribble && strcmp(pre_scribble, "1") == 0) ||
|
||
(guard_malloc && strstr(guard_malloc, "libgmalloc") != NULL);
|
||
#endif
|
||
}
|
||
|
||
static int incr_full_index_rss_limit_mb(void) {
|
||
int rss_limit_mb = INCR_FULL_INDEX_BASE_MAX_RSS_DELTA_MB;
|
||
#ifndef _WIN32
|
||
if (sysconf(_SC_PAGESIZE) >= INCR_LARGE_PAGE_MIN_BYTES) {
|
||
rss_limit_mb = INCR_FULL_INDEX_LARGE_PAGE_MAX_RSS_DELTA_MB;
|
||
}
|
||
#endif
|
||
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__)
|
||
rss_limit_mb = INCR_FULL_INDEX_LARGE_PAGE_MAX_RSS_DELTA_MB;
|
||
#endif
|
||
return rss_limit_mb;
|
||
}
|
||
|
||
/* ── Helpers ──────────────────────────────────────────────────────── */
|
||
|
||
static double now_ms(void) {
|
||
struct timespec ts;
|
||
cbm_clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
||
}
|
||
|
||
static void write_file_at(const char *rel_path, const char *content) {
|
||
char path[512];
|
||
snprintf(path, sizeof(path), "%s/%s", g_repodir, rel_path);
|
||
char dir[512];
|
||
snprintf(dir, sizeof(dir), "%s", path);
|
||
char *slash = strrchr(dir, '/');
|
||
if (slash) {
|
||
*slash = '\0';
|
||
cbm_mkdir(dir);
|
||
}
|
||
FILE *f = fopen(path, "w");
|
||
if (f) {
|
||
fputs(content, f);
|
||
fclose(f);
|
||
}
|
||
}
|
||
|
||
static void delete_file_at(const char *rel_path) {
|
||
char path[512];
|
||
snprintf(path, sizeof(path), "%s/%s", g_repodir, rel_path);
|
||
unlink(path);
|
||
}
|
||
|
||
/* Append "# reformatted" to up to max_files .py files in subdir (cross-platform). */
|
||
static int reformat_files(const char *subdir, int max_files) {
|
||
char dir[512];
|
||
snprintf(dir, sizeof(dir), "%s/%s", g_repodir, subdir);
|
||
cbm_dir_t *d = cbm_opendir(dir);
|
||
if (!d) {
|
||
return -1;
|
||
}
|
||
cbm_dirent_t *entry;
|
||
int count = 0;
|
||
while ((entry = cbm_readdir(d)) != NULL && count < max_files) {
|
||
size_t nlen = strlen(entry->name);
|
||
if (nlen < 4 || strcmp(entry->name + nlen - 3, ".py") != 0) {
|
||
continue;
|
||
}
|
||
if (entry->is_dir) {
|
||
continue;
|
||
}
|
||
char path[1024];
|
||
snprintf(path, sizeof(path), "%s/%s", dir, entry->name);
|
||
th_append_file(path, "# reformatted\n");
|
||
count++;
|
||
}
|
||
cbm_closedir(d);
|
||
return 0;
|
||
}
|
||
|
||
static char *index_repo(void) {
|
||
char args[512];
|
||
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", g_repodir);
|
||
return cbm_mcp_handle_tool(g_srv, "index_repository", args);
|
||
}
|
||
|
||
/* Timed index: returns response, sets *elapsed_ms and *peak_rss_mb */
|
||
static char *index_repo_timed(double *elapsed_ms, size_t *peak_rss_mb) {
|
||
double t0 = now_ms();
|
||
char *resp = index_repo();
|
||
*elapsed_ms = now_ms() - t0;
|
||
*peak_rss_mb = cbm_mem_peak_rss() / (1024 * 1024);
|
||
return resp;
|
||
}
|
||
|
||
static char *call_tool(const char *tool, const char *args_fmt, ...) {
|
||
char args[1024];
|
||
va_list ap;
|
||
va_start(ap, args_fmt);
|
||
vsnprintf(args, sizeof(args), args_fmt, ap);
|
||
va_end(ap);
|
||
return cbm_mcp_handle_tool(g_srv, tool, args);
|
||
}
|
||
|
||
/* Parse integer from a tool response (handles the nested MCP envelope).
|
||
* Matches the JSON forms "key":N / \"key\":N and the TOON scalar `key: N`
|
||
* (search_graph default output since the compact-output change). */
|
||
static int count_in_response(const char *resp, const char *key) {
|
||
if (!resp)
|
||
return -1;
|
||
char pattern[64];
|
||
snprintf(pattern, sizeof(pattern), "\"%s\":", key);
|
||
const char *p = strstr(resp, pattern);
|
||
if (p)
|
||
return atoi(p + strlen(pattern));
|
||
snprintf(pattern, sizeof(pattern), "\\\"%s\\\":", key);
|
||
p = strstr(resp, pattern);
|
||
if (p)
|
||
return atoi(p + strlen(pattern));
|
||
snprintf(pattern, sizeof(pattern), "%s: ", key);
|
||
p = strstr(resp, pattern);
|
||
if (p)
|
||
return atoi(p + strlen(pattern));
|
||
return -1;
|
||
}
|
||
|
||
/* ── Direct store queries (more reliable than MCP for tests) ────── */
|
||
|
||
static cbm_store_t *open_store(void) {
|
||
return cbm_store_open_path(g_dbpath);
|
||
}
|
||
|
||
static int dump_current_store_to_file(const char *dest_path) {
|
||
cbm_store_t *s = open_store();
|
||
if (!s) {
|
||
return CBM_STORE_ERR;
|
||
}
|
||
int rc = cbm_store_dump_to_file(s, dest_path);
|
||
cbm_store_close(s);
|
||
return rc;
|
||
}
|
||
|
||
static int dump_store_file_to_file(const char *src_path, const char *dest_path) {
|
||
cbm_store_t *s = cbm_store_open_path(src_path);
|
||
if (!s) {
|
||
return CBM_STORE_ERR;
|
||
}
|
||
int rc = cbm_store_dump_to_file(s, dest_path);
|
||
cbm_store_close(s);
|
||
return rc;
|
||
}
|
||
|
||
static void preserve_accuracy_artifacts(const char *incr_snapshot_path, const char *reason) {
|
||
char artifact_dir[CBM_SZ_512];
|
||
const char *dir =
|
||
cbm_safe_getenv(INCR_TEST_ARTIFACT_ENV, artifact_dir, sizeof(artifact_dir), cbm_tmpdir());
|
||
if (!dir || dir[0] == '\0') {
|
||
dir = cbm_tmpdir();
|
||
}
|
||
|
||
char incr_out[CBM_SZ_1K];
|
||
char full_out[CBM_SZ_1K];
|
||
int pid = (int)getpid();
|
||
int n1 = snprintf(incr_out, sizeof(incr_out), "%s/cbm-incr-accuracy-%d-incremental.db", dir,
|
||
pid);
|
||
int n2 = snprintf(full_out, sizeof(full_out), "%s/cbm-incr-accuracy-%d-full.db", dir, pid);
|
||
if (n1 <= 0 || n2 <= 0 || (size_t)n1 >= sizeof(incr_out) ||
|
||
(size_t)n2 >= sizeof(full_out)) {
|
||
printf(" [accuracy:artifacts] skipped: artifact path too long\n");
|
||
return;
|
||
}
|
||
|
||
int incr_rc = dump_store_file_to_file(incr_snapshot_path, incr_out);
|
||
int full_rc = dump_current_store_to_file(full_out);
|
||
printf(" [accuracy:artifacts] reason=%s incremental=%s rc=%d full=%s rc=%d\n",
|
||
reason ? reason : "canonical-diff", incr_out, incr_rc, full_out, full_rc);
|
||
}
|
||
|
||
static int get_node_count(void) {
|
||
cbm_store_t *s = open_store();
|
||
if (!s)
|
||
return -1;
|
||
int c = cbm_store_count_nodes(s, g_project);
|
||
cbm_store_close(s);
|
||
return c;
|
||
}
|
||
|
||
static int get_edge_count(void) {
|
||
cbm_store_t *s = open_store();
|
||
if (!s)
|
||
return -1;
|
||
int c = cbm_store_count_edges(s, g_project);
|
||
cbm_store_close(s);
|
||
return c;
|
||
}
|
||
|
||
static int get_edge_count_by_type(const char *type) {
|
||
cbm_store_t *s = open_store();
|
||
if (!s)
|
||
return -1;
|
||
int c = cbm_store_count_edges_by_type(s, g_project, type);
|
||
cbm_store_close(s);
|
||
return c;
|
||
}
|
||
|
||
static const char *const k_accuracy_edge_types[] = {
|
||
"CALLS",
|
||
"IMPORTS",
|
||
"DEFINES",
|
||
"CONTAINS_FILE",
|
||
"CONTAINS_FOLDER",
|
||
"HAS_BRANCH",
|
||
"DEFINES_METHOD",
|
||
"MEMBER_OF",
|
||
"HAS_FIELD",
|
||
"HANDLES",
|
||
"HTTP_CALLS",
|
||
"ASYNC_CALLS",
|
||
"DATA_FLOWS",
|
||
"INFRA_MAPS",
|
||
"CONFIGURES",
|
||
"DEPENDS_ON",
|
||
"FILE_CHANGES_WITH",
|
||
"SIMILAR_TO",
|
||
"SEMANTICALLY_RELATED",
|
||
"TESTS",
|
||
"TESTS_FILE",
|
||
"USAGE",
|
||
"THROWS",
|
||
"RAISES",
|
||
"WRITES",
|
||
"READS",
|
||
"INHERITS",
|
||
"DECORATES",
|
||
"IMPLEMENTS",
|
||
"EMITS",
|
||
"LISTENS_ON",
|
||
"GRPC_CALLS",
|
||
"GRAPHQL_CALLS",
|
||
"TRPC_CALLS",
|
||
};
|
||
|
||
enum { ACCURACY_EDGE_TYPE_COUNT = sizeof(k_accuracy_edge_types) / sizeof(k_accuracy_edge_types[0]) };
|
||
|
||
static int accuracy_edge_type_count(void) {
|
||
return ACCURACY_EDGE_TYPE_COUNT;
|
||
}
|
||
|
||
static void capture_accuracy_edge_counts(int counts[ACCURACY_EDGE_TYPE_COUNT]) {
|
||
int n = accuracy_edge_type_count();
|
||
for (int i = 0; i < n; i++) {
|
||
counts[i] = get_edge_count_by_type(k_accuracy_edge_types[i]);
|
||
}
|
||
}
|
||
|
||
static void print_accuracy_edge_diff(const int incr_counts[ACCURACY_EDGE_TYPE_COUNT],
|
||
const int full_counts[ACCURACY_EDGE_TYPE_COUNT]) {
|
||
int n = accuracy_edge_type_count();
|
||
printf(" [accuracy:edge-types] type incr full delta\n");
|
||
for (int i = 0; i < n; i++) {
|
||
int delta = incr_counts[i] - full_counts[i];
|
||
if (delta != 0) {
|
||
printf(" [accuracy:edge-types] %s %d %d %+d\n", k_accuracy_edge_types[i],
|
||
incr_counts[i], full_counts[i], delta);
|
||
}
|
||
}
|
||
}
|
||
|
||
typedef struct {
|
||
int total;
|
||
int handler;
|
||
int prefix_bridge;
|
||
int infra_match;
|
||
int empty;
|
||
int other;
|
||
} handle_breakdown_t;
|
||
|
||
static handle_breakdown_t capture_handle_breakdown(void) {
|
||
handle_breakdown_t b = {0};
|
||
cbm_store_t *s = open_store();
|
||
if (!s) {
|
||
return b;
|
||
}
|
||
cbm_edge_t *edges = NULL;
|
||
int count = 0;
|
||
if (cbm_store_find_edges_by_type(s, g_project, "HANDLES", &edges, &count) == CBM_STORE_OK) {
|
||
b.total = count;
|
||
for (int i = 0; i < count; i++) {
|
||
const char *props = edges[i].properties_json ? edges[i].properties_json : "{}";
|
||
if (strstr(props, "\"source\":\"prefix_decorator_bridge\"")) {
|
||
b.prefix_bridge++;
|
||
} else if (strstr(props, "\"source\":\"infra_match\"")) {
|
||
b.infra_match++;
|
||
} else if (strstr(props, "\"handler\"")) {
|
||
b.handler++;
|
||
} else if (strcmp(props, "{}") == 0) {
|
||
b.empty++;
|
||
} else {
|
||
b.other++;
|
||
}
|
||
}
|
||
cbm_store_free_edges(edges, count);
|
||
}
|
||
cbm_store_close(s);
|
||
return b;
|
||
}
|
||
|
||
static void print_handle_breakdown(const char *label, handle_breakdown_t b) {
|
||
printf(" [accuracy:handles:%s] total=%d handler=%d prefix_bridge=%d infra_match=%d "
|
||
"empty=%d other=%d\n",
|
||
label, b.total, b.handler, b.prefix_bridge, b.infra_match, b.empty, b.other);
|
||
}
|
||
|
||
static int has_function(const char *name_pattern) {
|
||
char *resp = call_tool("search_graph",
|
||
"{\"project\":\"%s\",\"label\":\"Function\",\"name_pattern\":\"%s\"}",
|
||
g_project, name_pattern);
|
||
int total = count_in_response(resp, "total");
|
||
free(resp);
|
||
return total > 0;
|
||
}
|
||
|
||
static int count_by_label(const char *label) {
|
||
char *resp =
|
||
call_tool("search_graph", "{\"project\":\"%s\",\"label\":\"%s\"}", g_project, label);
|
||
int total = count_in_response(resp, "total");
|
||
free(resp);
|
||
return total;
|
||
}
|
||
|
||
static int incr_join_path(char *out, size_t out_sz, const char *base, const char *rel) {
|
||
if (!out || out_sz == 0 || !base || !base[0] || !rel || !rel[0]) {
|
||
return -1;
|
||
}
|
||
int n = snprintf(out, out_sz, "%s/%s", base, rel);
|
||
return (n >= 0 && (size_t)n < out_sz) ? 0 : -1;
|
||
}
|
||
|
||
static bool incr_shell_path_ok(const char *path) {
|
||
return path && path[0] && cbm_validate_shell_arg(path);
|
||
}
|
||
|
||
static bool incr_fastapi_fixture_has_required_files(const char *repo) {
|
||
char path[CBM_SZ_1K];
|
||
if (incr_join_path(path, sizeof(path), repo, "fastapi/applications.py") != 0 ||
|
||
!cbm_file_exists(path)) {
|
||
return false;
|
||
}
|
||
if (incr_join_path(path, sizeof(path), repo, "tests/test_application.py") != 0 ||
|
||
!cbm_file_exists(path)) {
|
||
return false;
|
||
}
|
||
if (incr_join_path(path, sizeof(path), repo, "docs/en/docs/release-notes.md") != 0 ||
|
||
!cbm_file_exists(path)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
static bool incr_fastapi_fixture_at_expected_commit(const char *repo) {
|
||
if (!incr_shell_path_ok(repo)) {
|
||
return false;
|
||
}
|
||
char cmd[CBM_SZ_1K];
|
||
int n = snprintf(cmd, sizeof(cmd), "git -C '%s' rev-parse --verify HEAD 2>/dev/null", repo);
|
||
if (n < 0 || (size_t)n >= sizeof(cmd)) {
|
||
return false;
|
||
}
|
||
FILE *fp = cbm_popen(cmd, "r");
|
||
if (!fp) {
|
||
return false;
|
||
}
|
||
char head[CBM_SZ_128] = {0};
|
||
bool ok = fgets(head, sizeof(head), fp) != NULL;
|
||
(void)cbm_pclose(fp);
|
||
if (!ok) {
|
||
return false;
|
||
}
|
||
head[strcspn(head, "\r\n")] = '\0';
|
||
return strcmp(head, INCR_TEST_FASTAPI_COMMIT) == 0;
|
||
}
|
||
|
||
static bool incr_fastapi_fixture_valid(const char *repo) {
|
||
return incr_fastapi_fixture_has_required_files(repo) &&
|
||
incr_fastapi_fixture_at_expected_commit(repo);
|
||
}
|
||
|
||
static int incr_clone_fastapi_fixture_from(const char *source) {
|
||
if (!incr_shell_path_ok(source) || !incr_shell_path_ok(g_repodir)) {
|
||
return -1;
|
||
}
|
||
char cmd[CBM_SZ_2K];
|
||
int n = snprintf(cmd, sizeof(cmd),
|
||
"git clone --quiet --no-hardlinks '%s' '%s' 2>&1", source,
|
||
g_repodir);
|
||
if (n < 0 || (size_t)n >= sizeof(cmd)) {
|
||
return -1;
|
||
}
|
||
int rc = system(cmd);
|
||
if (rc != 0) {
|
||
return rc;
|
||
}
|
||
if (getenv("CI")) {
|
||
n = snprintf(cmd, sizeof(cmd),
|
||
"cd '%s' && git sparse-checkout set --no-cone '/*' '!/docs' '!/tests' "
|
||
"2>&1",
|
||
g_repodir);
|
||
if (n < 0 || (size_t)n >= sizeof(cmd)) {
|
||
return -1;
|
||
}
|
||
rc = system(cmd);
|
||
}
|
||
return rc;
|
||
}
|
||
|
||
static int incr_clone_fastapi_fixture_from_network(const char *dest, bool sparse_on_ci) {
|
||
if (!incr_shell_path_ok(dest)) {
|
||
return -1;
|
||
}
|
||
char cmd[CBM_SZ_2K];
|
||
int n = 0;
|
||
if (sparse_on_ci && getenv("CI")) {
|
||
n = snprintf(cmd, sizeof(cmd),
|
||
"git clone --depth=1 --branch %s --quiet --filter=blob:none --sparse "
|
||
"%s '%s' 2>&1 && cd '%s' && git sparse-checkout set --no-cone '/*' "
|
||
"'!/docs' '!/tests' 2>&1",
|
||
INCR_TEST_FASTAPI_TAG, INCR_TEST_FASTAPI_URL, dest, dest);
|
||
} else {
|
||
n = snprintf(cmd, sizeof(cmd), "git clone --depth=1 --branch %s --quiet %s '%s' 2>&1",
|
||
INCR_TEST_FASTAPI_TAG, INCR_TEST_FASTAPI_URL, dest);
|
||
}
|
||
if (n < 0 || (size_t)n >= sizeof(cmd)) {
|
||
return -1;
|
||
}
|
||
return system(cmd);
|
||
}
|
||
|
||
static const char *incr_fastapi_cache_path(char *buf, size_t buf_sz) {
|
||
const char *cache = cbm_safe_getenv(INCR_TEST_FASTAPI_CACHE_ENV, buf, buf_sz, NULL);
|
||
if (cache && cache[0]) {
|
||
return cache;
|
||
}
|
||
int n = snprintf(buf, buf_sz, "%s/%s", cbm_tmpdir(), INCR_TEST_FASTAPI_DEFAULT_CACHE_NAME);
|
||
return (n >= 0 && (size_t)n < buf_sz) ? buf : NULL;
|
||
}
|
||
|
||
static int incr_prepare_managed_fastapi_cache(const char *cache) {
|
||
if (!incr_shell_path_ok(cache)) {
|
||
return -1;
|
||
}
|
||
if (incr_fastapi_fixture_valid(cache)) {
|
||
return 0;
|
||
}
|
||
|
||
th_rmtree(cache);
|
||
int rc = incr_clone_fastapi_fixture_from_network(cache, false);
|
||
if (rc != 0) {
|
||
th_rmtree(cache);
|
||
return rc;
|
||
}
|
||
if (!incr_fastapi_fixture_valid(cache)) {
|
||
th_rmtree(cache);
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int incr_clone_fastapi_fixture(void) {
|
||
char source_buf[CBM_SZ_1K];
|
||
const char *source = cbm_safe_getenv(INCR_TEST_FASTAPI_REPO_ENV, source_buf,
|
||
sizeof(source_buf), NULL);
|
||
if (source && source[0] && incr_fastapi_fixture_valid(source)) {
|
||
printf(" using FastAPI fixture source: %s\n", source);
|
||
return incr_clone_fastapi_fixture_from(source);
|
||
}
|
||
|
||
char cache_buf[CBM_SZ_1K];
|
||
const char *cache = incr_fastapi_cache_path(cache_buf, sizeof(cache_buf));
|
||
int rc = incr_prepare_managed_fastapi_cache(cache);
|
||
if (rc == 0) {
|
||
printf(" using FastAPI fixture cache: %s\n", cache);
|
||
return incr_clone_fastapi_fixture_from(cache);
|
||
}
|
||
|
||
return incr_clone_fastapi_fixture_from_network(g_repodir, true);
|
||
}
|
||
|
||
/* ── Setup / Teardown ─────────────────────────────────────────────── */
|
||
|
||
static int incremental_setup(void) {
|
||
snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/cbm_incr_XXXXXX");
|
||
if (!cbm_mkdtemp(g_tmpdir))
|
||
return -1;
|
||
|
||
snprintf(g_repodir, sizeof(g_repodir), "%s/fastapi", g_tmpdir);
|
||
|
||
int rc = incr_clone_fastapi_fixture();
|
||
if (rc != 0) {
|
||
printf(" FastAPI fixture setup failed (rc=%d) — cache invalid and network offline?\n", rc);
|
||
return -1;
|
||
}
|
||
|
||
g_project = cbm_project_name_from_path(g_repodir);
|
||
if (!g_project)
|
||
return -1;
|
||
|
||
/* Resolve the cache dir via cbm_resolve_cache_dir() so it honors CBM_CACHE_DIR
|
||
* and matches the index WRITE path (pipeline.c). Hardcoding ~/.cache here
|
||
* made get_node_count read from a different dir than the index wrote under
|
||
* CBM_TEST_ISOLATE, yielding 0-node indexes (and a div-by-zero). */
|
||
const char *cdir = cbm_resolve_cache_dir();
|
||
if (!cdir) {
|
||
cdir = "/tmp";
|
||
}
|
||
snprintf(g_dbpath, sizeof(g_dbpath), "%s/%s.db", cdir, g_project);
|
||
|
||
char cache_dir[512];
|
||
snprintf(cache_dir, sizeof(cache_dir), "%s", cdir);
|
||
cbm_mkdir(cache_dir);
|
||
|
||
cbm_unlink(g_dbpath);
|
||
|
||
g_srv = cbm_mcp_server_new(NULL);
|
||
if (!g_srv)
|
||
return -1;
|
||
g_cfg = cbm_config_open(cdir);
|
||
if (!g_cfg) {
|
||
cbm_mcp_server_free(g_srv);
|
||
g_srv = NULL;
|
||
return -1;
|
||
}
|
||
cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_REINDEX, "always");
|
||
if (incr_memory_instrumentation_active()) {
|
||
char timeout_ms[CBM_SZ_32];
|
||
snprintf(timeout_ms, sizeof(timeout_ms), "%d",
|
||
CBM_CONFIG_EXTRACT_TIMEOUT_DEFAULT_MS * INCR_INSTRUMENTED_TIMEOUT_MULTIPLIER);
|
||
cbm_config_set(g_cfg, CBM_CONFIG_EXTRACT_TIMEOUT_MS, timeout_ms);
|
||
}
|
||
cbm_mcp_server_set_config(g_srv, g_cfg);
|
||
|
||
g_rss_before_full = cbm_mem_rss();
|
||
|
||
return 0;
|
||
}
|
||
|
||
static void incremental_teardown(void) {
|
||
if (g_srv) {
|
||
cbm_mcp_server_free(g_srv);
|
||
g_srv = NULL;
|
||
}
|
||
cbm_config_close(g_cfg);
|
||
g_cfg = NULL;
|
||
if (g_project) {
|
||
unlink(g_dbpath);
|
||
char wal[520], shm[520];
|
||
snprintf(wal, sizeof(wal), "%s-wal", g_dbpath);
|
||
snprintf(shm, sizeof(shm), "%s-shm", g_dbpath);
|
||
unlink(wal);
|
||
unlink(shm);
|
||
}
|
||
free(g_project);
|
||
g_project = NULL;
|
||
|
||
th_rmtree(g_tmpdir);
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 1: Full index — baseline + performance
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
TEST(incr_full_index) {
|
||
double ms = 0;
|
||
size_t peak_mb = 0;
|
||
char *resp = index_repo_timed(&ms, &peak_mb);
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
g_full_nodes = get_node_count();
|
||
g_full_edges = get_edge_count();
|
||
g_full_calls = get_edge_count_by_type("CALLS");
|
||
g_full_imports = get_edge_count_by_type("IMPORTS");
|
||
g_full_index_ms = ms;
|
||
|
||
/* FastAPI: expect substantial graph.
|
||
* CI uses sparse checkout (~7K files), local has full repo (~18K files).
|
||
* Thresholds set for sparse checkout (smaller graph). */
|
||
ASSERT_GT(g_full_nodes, 2000);
|
||
ASSERT_GT(g_full_edges, 500);
|
||
ASSERT_GT(g_full_calls, 200);
|
||
ASSERT_GT(g_full_imports, 50);
|
||
|
||
/* Performance: full index — warn if slow, don't block */
|
||
if ((int)ms > 30000) {
|
||
printf(" [PERF WARNING] full index: %.0fms (>30s)\n", ms);
|
||
}
|
||
|
||
/* Memory: use the measured architecture/page-size budget. Diagnostic
|
||
* allocators intentionally inflate RSS, so they report instead of failing
|
||
* this production-build resource guard. */
|
||
size_t rss_delta_mb = peak_mb - (g_rss_before_full / (1024 * 1024));
|
||
int rss_limit_mb = incr_full_index_rss_limit_mb();
|
||
if (incr_memory_instrumentation_active()) {
|
||
printf(" [perf note] full index rss_delta=%zuMB under memory instrumentation "
|
||
"(normal limit=%dMB)\n",
|
||
rss_delta_mb, rss_limit_mb);
|
||
} else {
|
||
ASSERT_LT((int)rss_delta_mb, rss_limit_mb);
|
||
}
|
||
|
||
printf(" [perf] full: %d nodes, %d edges (%d CALLS, %d IMPORTS) "
|
||
"in %.0fms, peak=%zuMB\n",
|
||
g_full_nodes, g_full_edges, g_full_calls, g_full_imports, ms, peak_mb);
|
||
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_full_has_functions) {
|
||
int funcs = count_by_label("Function");
|
||
ASSERT_GT(funcs, 500);
|
||
|
||
int methods = count_by_label("Method");
|
||
ASSERT_GT(methods, 100);
|
||
|
||
int modules = count_by_label("Module");
|
||
ASSERT_GT(modules, 200);
|
||
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_full_edge_types) {
|
||
/* Verify edge type distribution is sane */
|
||
int calls = get_edge_count_by_type("CALLS");
|
||
int imports = get_edge_count_by_type("IMPORTS");
|
||
int defines = get_edge_count_by_type("DEFINES");
|
||
int contains = get_edge_count_by_type("CONTAINS_FILE");
|
||
|
||
ASSERT_GT(calls, 0);
|
||
ASSERT_GT(imports, 0);
|
||
ASSERT_GT(defines, 0);
|
||
ASSERT_GT(contains, 0);
|
||
|
||
/* CALLS should be the most common edge type */
|
||
ASSERT_GT(calls, imports);
|
||
|
||
printf(" [edges] CALLS=%d IMPORTS=%d DEFINES=%d CONTAINS_FILE=%d\n", calls, imports, defines,
|
||
contains);
|
||
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 2: Noop reindex — must be fast and exact
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
TEST(incr_noop_reindex) {
|
||
double ms = 0;
|
||
size_t peak_mb = 0;
|
||
char *resp = index_repo_timed(&ms, &peak_mb);
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
/* Exact same counts */
|
||
ASSERT_EQ(get_node_count(), g_full_nodes);
|
||
ASSERT_EQ(get_edge_count(), g_full_edges);
|
||
|
||
/* Noop should be fast (just file classification, no parsing) */
|
||
if ((int)ms > 5000) {
|
||
printf(" [PERF WARNING] noop reindex: %.0fms (>5s)\n", ms);
|
||
}
|
||
|
||
printf(" [perf] noop: %.0fms\n", ms);
|
||
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 3: Incremental delta changes
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
TEST(incr_modify_file) {
|
||
int nodes_before = get_node_count();
|
||
|
||
/* Append functions to an existing source file */
|
||
char path[512];
|
||
snprintf(path, sizeof(path), "%s/fastapi/applications.py", g_repodir);
|
||
FILE *f = fopen(path, "a");
|
||
ASSERT(f != NULL);
|
||
fprintf(f, "\n\ndef incr_modify_extra_a(x: int) -> int:\n"
|
||
" return x + 1\n"
|
||
"\n"
|
||
"def incr_modify_extra_b(y: str) -> str:\n"
|
||
" return y.strip()\n");
|
||
fclose(f);
|
||
|
||
double ms = 0;
|
||
size_t peak_mb = 0;
|
||
char *resp = index_repo_timed(&ms, &peak_mb);
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(has_function("incr_modify_extra_a"));
|
||
ASSERT(has_function("incr_modify_extra_b"));
|
||
ASSERT_GT(get_node_count(), nodes_before);
|
||
|
||
/* Single-file incremental should be faster than full */
|
||
if ((int)ms > (int)(g_full_index_ms * 1.5)) {
|
||
printf(" [PERF WARNING] incremental slower than 1.5x full: %.0fms vs %.0fms\n", ms,
|
||
g_full_index_ms);
|
||
}
|
||
|
||
printf(" [perf] modify 1 file: %.0fms (full was %.0fms)\n", ms, g_full_index_ms);
|
||
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_formatter_run) {
|
||
/* Baseline */
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
int nodes_before = get_node_count();
|
||
int edges_before = get_edge_count();
|
||
int calls_before = get_edge_count_by_type("CALLS");
|
||
|
||
/* Simulate a semantics-preserving formatter batch. */
|
||
ASSERT_EQ(cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
|
||
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_EAGER),
|
||
0);
|
||
int reformat_rc = reformat_files("fastapi", INCR_FORMATTER_MAX_FILES);
|
||
|
||
double ms = 0;
|
||
size_t peak_mb = 0;
|
||
resp = index_repo_timed(&ms, &peak_mb);
|
||
int incremental_response_ok = resp != NULL && strstr(resp, "indexed") != NULL;
|
||
free(resp);
|
||
|
||
/* Counts diagnose drift, but canonical incremental/full identity below is
|
||
* the pass/fail contract. Appending comments does not change semantics. */
|
||
int node_diff = abs(get_node_count() - nodes_before);
|
||
int edge_diff = abs(get_edge_count() - edges_before);
|
||
int calls_diff = abs(get_edge_count_by_type("CALLS") - calls_before);
|
||
|
||
char incremental_snapshot_path[CBM_SZ_512];
|
||
int snapshot_path_len = snprintf(incremental_snapshot_path, sizeof(incremental_snapshot_path),
|
||
"%s/incr_formatter_incremental.db", g_tmpdir);
|
||
int snapshot_path_ok =
|
||
snapshot_path_len > 0 && (size_t)snapshot_path_len < sizeof(incremental_snapshot_path);
|
||
int snapshot_rc = CBM_STORE_ERR;
|
||
int full_response_ok = 0;
|
||
int canonical_graph_diff_rc = CBM_NOT_FOUND;
|
||
char canonical_graph_diff_error[CBM_SZ_8K] = {0};
|
||
|
||
if (incremental_response_ok && snapshot_path_ok) {
|
||
cbm_unlink(incremental_snapshot_path);
|
||
snapshot_rc = dump_current_store_to_file(incremental_snapshot_path);
|
||
}
|
||
if (snapshot_rc == CBM_STORE_OK) {
|
||
cbm_unlink(g_dbpath);
|
||
resp = index_repo();
|
||
full_response_ok = resp != NULL && strstr(resp, "indexed") != NULL;
|
||
free(resp);
|
||
}
|
||
if (full_response_ok) {
|
||
canonical_graph_diff_rc = cbm_test_compare_canonical_graphs(
|
||
incremental_snapshot_path, g_dbpath, g_project, canonical_graph_diff_error,
|
||
sizeof(canonical_graph_diff_error));
|
||
}
|
||
if (canonical_graph_diff_rc != 0 && snapshot_rc == CBM_STORE_OK && full_response_ok) {
|
||
printf(" [formatter:canonical-diff] %s\n", canonical_graph_diff_error);
|
||
preserve_accuracy_artifacts(incremental_snapshot_path, "formatter-canonical-diff");
|
||
}
|
||
|
||
cbm_unlink(incremental_snapshot_path);
|
||
int restore_config_rc = cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
|
||
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_DEFAULT);
|
||
|
||
printf(" [perf] reformat up to %d files: %.0fms, node_diff=%d edge_diff=%d "
|
||
"calls_diff=%d\n",
|
||
INCR_FORMATTER_MAX_FILES, ms, node_diff, edge_diff, calls_diff);
|
||
|
||
ASSERT_EQ(reformat_rc, 0);
|
||
ASSERT(incremental_response_ok);
|
||
ASSERT(snapshot_path_ok);
|
||
ASSERT_EQ(snapshot_rc, CBM_STORE_OK);
|
||
ASSERT(full_response_ok);
|
||
ASSERT_EQ(restore_config_rc, 0);
|
||
ASSERT_EQ(canonical_graph_diff_rc, 0);
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_add_file) {
|
||
int nodes_before = get_node_count();
|
||
|
||
write_file_at("fastapi/incr_test_new.py", "\"\"\"New module.\"\"\"\n"
|
||
"from fastapi import FastAPI\n"
|
||
"\n"
|
||
"def incr_new_entry(app: FastAPI) -> None:\n"
|
||
" setup(app)\n"
|
||
"\n"
|
||
"def setup(app: FastAPI) -> None:\n"
|
||
" pass\n"
|
||
"\n"
|
||
"def incr_new_validate(data: dict) -> bool:\n"
|
||
" return bool(data)\n"
|
||
"\n"
|
||
"class IncrNewHandler:\n"
|
||
" def handle(self, req):\n"
|
||
" return incr_new_validate(req)\n"
|
||
"\n"
|
||
" def cleanup(self):\n"
|
||
" pass\n");
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(has_function("incr_new_entry"));
|
||
ASSERT(has_function("incr_new_validate"));
|
||
ASSERT_GT(get_node_count(), nodes_before);
|
||
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_delete_file) {
|
||
int nodes_before = get_node_count();
|
||
|
||
delete_file_at("fastapi/incr_test_new.py");
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(!has_function("incr_new_entry"));
|
||
ASSERT(!has_function("incr_new_validate"));
|
||
ASSERT(has_function("incr_test_injected")); /* others survive */
|
||
ASSERT_LT(get_node_count(), nodes_before);
|
||
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_simultaneous_changes) {
|
||
/* Add */
|
||
write_file_at("tests/incr_simul_add.py", "def incr_simul_added():\n return 'added'\n");
|
||
|
||
/* Modify */
|
||
char path[512];
|
||
snprintf(path, sizeof(path), "%s/fastapi/routing.py", g_repodir);
|
||
FILE *f = fopen(path, "a");
|
||
if (f) {
|
||
fprintf(f, "\ndef incr_simul_modified():\n return 'modified'\n");
|
||
fclose(f);
|
||
}
|
||
|
||
/* Delete */
|
||
delete_file_at("fastapi/incr_test_new.py"); /* already gone, noop */
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(has_function("incr_simul_added"));
|
||
ASSERT(has_function("incr_simul_modified"));
|
||
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 4: Adversarial inputs — attempt to break the pipeline
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
TEST(incr_empty_file) {
|
||
write_file_at("fastapi/incr_empty.py", "");
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
ASSERT(has_function("incr_test_injected"));
|
||
delete_file_at("fastapi/incr_empty.py");
|
||
free(resp);
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_syntax_error) {
|
||
int nodes_before = get_node_count();
|
||
|
||
write_file_at("fastapi/incr_broken.py", "def broken(\n"
|
||
" # deliberately broken\n"
|
||
"class @#$% {\n"
|
||
" async def nested() -> None\n"
|
||
" yield from broken(\n"
|
||
"))))\n");
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
ASSERT(has_function("incr_test_injected")); /* others survive */
|
||
|
||
int diff = abs(get_node_count() - nodes_before);
|
||
ASSERT_LT(diff, 20);
|
||
|
||
delete_file_at("fastapi/incr_broken.py");
|
||
free(resp);
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_huge_single_function) {
|
||
/* A single function with 5000 lines — stress test extraction budget */
|
||
size_t sz = 5000 * 30 + 100;
|
||
char *content = malloc(sz);
|
||
ASSERT(content != NULL);
|
||
int pos = 0;
|
||
pos += snprintf(content + pos, sz - (size_t)pos, "def huge_func(x):\n");
|
||
for (int i = 0; i < 5000; i++) {
|
||
pos += snprintf(content + pos, sz - (size_t)pos, " x = x + %d\n", i);
|
||
}
|
||
pos += snprintf(content + pos, sz - (size_t)pos, " return x\n");
|
||
(void)pos;
|
||
|
||
write_file_at("fastapi/incr_huge.py", content);
|
||
free(content);
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
/* Should extract the function (may be truncated by budget, that's ok) */
|
||
/* Key: must not crash or corrupt graph */
|
||
ASSERT(has_function("incr_test_injected"));
|
||
|
||
delete_file_at("fastapi/incr_huge.py");
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_binary_content) {
|
||
/* Write binary garbage to a .py file */
|
||
char path[512];
|
||
snprintf(path, sizeof(path), "%s/fastapi/incr_binary.py", g_repodir);
|
||
FILE *f = fopen(path, "wb");
|
||
if (f) {
|
||
unsigned char bin[256];
|
||
for (int i = 0; i < 256; i++)
|
||
bin[i] = (unsigned char)i;
|
||
fwrite(bin, 1, sizeof(bin), f);
|
||
fclose(f);
|
||
}
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
ASSERT(has_function("incr_test_injected"));
|
||
|
||
delete_file_at("fastapi/incr_binary.py");
|
||
free(resp);
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_large_generated) {
|
||
char *content = malloc(300 * 80);
|
||
ASSERT(content != NULL);
|
||
content[0] = '\0';
|
||
for (int i = 0; i < 300; i++) {
|
||
char line[80];
|
||
snprintf(line, sizeof(line), "def incr_gen_%d(x):\n return x + %d\n\n", i, i);
|
||
strcat(content, line);
|
||
}
|
||
|
||
write_file_at("fastapi/incr_generated.py", content);
|
||
free(content);
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
ASSERT(has_function("incr_gen_0"));
|
||
ASSERT(has_function("incr_gen_299"));
|
||
|
||
delete_file_at("fastapi/incr_generated.py");
|
||
free(resp);
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_new_subdir) {
|
||
/* File in a brand new subdirectory */
|
||
write_file_at("fastapi/newpkg/__init__.py", "");
|
||
write_file_at("fastapi/newpkg/handler.py", "def newpkg_handler():\n"
|
||
" return 'from_new_package'\n");
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
ASSERT(has_function("newpkg_handler"));
|
||
|
||
delete_file_at("fastapi/newpkg/handler.py");
|
||
delete_file_at("fastapi/newpkg/__init__.py");
|
||
free(resp);
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 5: Stress + complex scenarios
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
TEST(incr_rapid_reindex) {
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
int baseline_nodes = get_node_count();
|
||
int baseline_edges = get_edge_count();
|
||
|
||
for (int i = 0; i < 3; i++) {
|
||
resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
}
|
||
|
||
ASSERT_EQ(get_node_count(), baseline_nodes);
|
||
ASSERT_EQ(get_edge_count(), baseline_edges);
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_replace_file_content) {
|
||
write_file_at("fastapi/incr_replace.py", "def replace_original_a():\n return 'a'\n"
|
||
"def replace_original_b():\n return 'b'\n");
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(has_function("replace_original_a"));
|
||
ASSERT(has_function("replace_original_b"));
|
||
|
||
/* Replace entirely */
|
||
write_file_at("fastapi/incr_replace.py", "def replace_new_x():\n return 'x'\n"
|
||
"def replace_new_y():\n return replace_new_x()\n");
|
||
|
||
resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(!has_function("replace_original_a"));
|
||
ASSERT(!has_function("replace_original_b"));
|
||
ASSERT(has_function("replace_new_x"));
|
||
ASSERT(has_function("replace_new_y"));
|
||
|
||
delete_file_at("fastapi/incr_replace.py");
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_batch_add_delete) {
|
||
/* Add 20 files */
|
||
for (int i = 0; i < 20; i++) {
|
||
char path[128], content[256];
|
||
snprintf(path, sizeof(path), "tests/incr_batch_%d.py", i);
|
||
snprintf(content, sizeof(content),
|
||
"def batch_%d(x):\n return x + %d\n"
|
||
"def batch_h_%d(y):\n return batch_%d(y) * 2\n",
|
||
i, i, i, i);
|
||
write_file_at(path, content);
|
||
}
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(has_function("batch_0"));
|
||
ASSERT(has_function("batch_19"));
|
||
ASSERT(has_function("batch_h_10"));
|
||
|
||
/* Delete all 20 */
|
||
for (int i = 0; i < 20; i++) {
|
||
char path[128];
|
||
snprintf(path, sizeof(path), "tests/incr_batch_%d.py", i);
|
||
delete_file_at(path);
|
||
}
|
||
|
||
resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
ASSERT(!has_function("batch_0"));
|
||
ASSERT(!has_function("batch_19"));
|
||
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 6: Recovery + accuracy
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
TEST(incr_db_deleted_recovery) {
|
||
/* Recovery is an exact graph oracle, so establish an eager-derived
|
||
* baseline instead of comparing the configured stale-on-incremental view
|
||
* with a clean rebuild that necessarily refreshes global semantic edges. */
|
||
ASSERT_EQ(cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
|
||
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_EAGER),
|
||
0);
|
||
write_file_at("tests/incr_recovery_refresh.py",
|
||
"def incr_recovery_refresh():\n return 'recovery'\n");
|
||
char *baseline_response = index_repo();
|
||
ASSERT(baseline_response != NULL);
|
||
ASSERT(strstr(baseline_response, "indexed") != NULL);
|
||
free(baseline_response);
|
||
|
||
char recovery_baseline_path[CBM_SZ_512];
|
||
int recovery_baseline_path_len =
|
||
snprintf(recovery_baseline_path, sizeof(recovery_baseline_path),
|
||
"%s/incr_db_deleted_recovery_baseline.db", g_tmpdir);
|
||
ASSERT_GT(recovery_baseline_path_len, 0);
|
||
ASSERT_LT((size_t)recovery_baseline_path_len, sizeof(recovery_baseline_path));
|
||
cbm_unlink(recovery_baseline_path);
|
||
ASSERT_EQ(dump_current_store_to_file(recovery_baseline_path), CBM_STORE_OK);
|
||
|
||
ASSERT_EQ(cbm_unlink(g_dbpath), 0);
|
||
|
||
double ms = 0;
|
||
size_t peak_mb = 0;
|
||
char *resp = index_repo_timed(&ms, &peak_mb);
|
||
ASSERT(resp != NULL);
|
||
ASSERT(strstr(resp, "indexed") != NULL);
|
||
free(resp);
|
||
|
||
char canonical_graph_diff_error[CBM_SZ_8K] = {0};
|
||
int canonical_graph_diff_rc = cbm_test_compare_canonical_graphs(
|
||
recovery_baseline_path, g_dbpath, g_project, canonical_graph_diff_error,
|
||
sizeof(canonical_graph_diff_error));
|
||
if (canonical_graph_diff_rc != 0) {
|
||
printf(" [db-recovery:canonical-diff] %s\n", canonical_graph_diff_error);
|
||
preserve_accuracy_artifacts(recovery_baseline_path, "db-recovery-canonical-diff");
|
||
}
|
||
cbm_unlink(recovery_baseline_path);
|
||
delete_file_at("tests/incr_recovery_refresh.py");
|
||
int restore_config_rc = cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
|
||
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_DEFAULT);
|
||
|
||
printf(" [perf] db recovery (full reindex): %.0fms, peak=%zuMB\n", ms, peak_mb);
|
||
|
||
ASSERT_EQ(restore_config_rc, 0);
|
||
ASSERT_EQ(canonical_graph_diff_rc, 0);
|
||
PASS();
|
||
}
|
||
|
||
TEST(incr_accuracy_vs_full) {
|
||
/* This test is the strict canonical full-vs-incremental oracle. The
|
||
* production default may intentionally defer global semantic-derived edges,
|
||
* so opt into eager refresh here instead of weakening the graph comparison. */
|
||
ASSERT_EQ(cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
|
||
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_EAGER),
|
||
0);
|
||
|
||
/* Modify a file to create a known incremental state */
|
||
write_file_at("fastapi/incr_accuracy.py", "def accuracy_a():\n return 1\n"
|
||
"def accuracy_b():\n return accuracy_a() + 1\n");
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
int incr_nodes = get_node_count();
|
||
int incr_edges = get_edge_count();
|
||
int incr_calls = get_edge_count_by_type("CALLS");
|
||
int incr_type_counts[ACCURACY_EDGE_TYPE_COUNT] = {0};
|
||
capture_accuracy_edge_counts(incr_type_counts);
|
||
handle_breakdown_t incr_handles = capture_handle_breakdown();
|
||
|
||
char incr_snapshot_path[CBM_SZ_512];
|
||
int snap_len = snprintf(incr_snapshot_path, sizeof(incr_snapshot_path),
|
||
"%s/incr_accuracy_incremental.db", g_tmpdir);
|
||
ASSERT_GT(snap_len, 0);
|
||
ASSERT_LT((size_t)snap_len, sizeof(incr_snapshot_path));
|
||
cbm_unlink(incr_snapshot_path);
|
||
ASSERT_EQ(dump_current_store_to_file(incr_snapshot_path), CBM_STORE_OK);
|
||
|
||
/* Delete DB, force full reindex */
|
||
unlink(g_dbpath);
|
||
resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
int full_nodes = get_node_count();
|
||
int full_edges = get_edge_count();
|
||
int full_calls = get_edge_count_by_type("CALLS");
|
||
int full_type_counts[ACCURACY_EDGE_TYPE_COUNT] = {0};
|
||
capture_accuracy_edge_counts(full_type_counts);
|
||
handle_breakdown_t full_handles = capture_handle_breakdown();
|
||
|
||
/* Counts remain useful diagnostics, but canonical graph equality below is
|
||
* the pass/fail contract. */
|
||
if (abs(full_nodes - incr_nodes) > INCR_ACCURACY_NODE_TOLERANCE) {
|
||
printf(" [accuracy:nodes] incr=%d full=%d delta=%+d\n", incr_nodes, full_nodes,
|
||
incr_nodes - full_nodes);
|
||
}
|
||
if (abs(full_edges - incr_edges) > INCR_ACCURACY_EDGE_TOLERANCE) {
|
||
print_accuracy_edge_diff(incr_type_counts, full_type_counts);
|
||
print_handle_breakdown("incr", incr_handles);
|
||
print_handle_breakdown("full", full_handles);
|
||
}
|
||
if (abs(full_calls - incr_calls) > INCR_ACCURACY_CALL_TOLERANCE) {
|
||
printf(" [accuracy:calls] incr=%d full=%d delta=%+d\n", incr_calls, full_calls,
|
||
incr_calls - full_calls);
|
||
}
|
||
|
||
char diff_err[CBM_SZ_8K] = {0};
|
||
int graph_diff_rc =
|
||
cbm_test_compare_canonical_graphs(incr_snapshot_path, g_dbpath, g_project, diff_err,
|
||
sizeof(diff_err));
|
||
if (graph_diff_rc != 0) {
|
||
print_accuracy_edge_diff(incr_type_counts, full_type_counts);
|
||
print_handle_breakdown("incr", incr_handles);
|
||
print_handle_breakdown("full", full_handles);
|
||
printf(" [accuracy:canonical-diff] %s\n", diff_err);
|
||
preserve_accuracy_artifacts(incr_snapshot_path, "canonical-diff");
|
||
}
|
||
|
||
printf(" [accuracy] incr: %d nodes/%d edges, full: %d nodes/%d edges\n", incr_nodes,
|
||
incr_edges, full_nodes, full_edges);
|
||
|
||
delete_file_at("fastapi/incr_accuracy.py");
|
||
cbm_unlink(incr_snapshot_path);
|
||
ASSERT_EQ(cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
|
||
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_DEFAULT),
|
||
0);
|
||
ASSERT_EQ(graph_diff_rc, 0);
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 7: Performance regression guards
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
TEST(incr_perf_single_file_fast) {
|
||
/* Modifying one file should complete in <2s (not re-parse everything) */
|
||
write_file_at("fastapi/incr_perf_probe.py", "def perf_probe():\n return 42\n");
|
||
|
||
double ms = 0;
|
||
size_t peak_mb = 0;
|
||
char *resp = index_repo_timed(&ms, &peak_mb);
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
#define PERF_WARN_MS 15000 /* warn above 15s — slow CI runners may exceed this */
|
||
|
||
if ((int)ms > PERF_WARN_MS) {
|
||
printf(" [PERF WARNING] single file incremental: %.0fms (>%dms)\n", ms, PERF_WARN_MS);
|
||
} else {
|
||
printf(" [perf] single file incremental: %.0fms\n", ms);
|
||
}
|
||
|
||
delete_file_at("fastapi/incr_perf_probe.py");
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* PHASE 8: MCP tool integration — comprehensive per-tool tests
|
||
*
|
||
* Every tool, every parameter, error paths, timeouts.
|
||
* Timing: each call_tool_timed() warns if > PERF_WARN_MS.
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
static char *call_tool_timed(const char *tool, double *ms, const char *args_fmt, ...) {
|
||
char args[2048];
|
||
va_list ap;
|
||
va_start(ap, args_fmt);
|
||
vsnprintf(args, sizeof(args), args_fmt, ap);
|
||
va_end(ap);
|
||
double t0 = now_ms();
|
||
char *resp = cbm_mcp_handle_tool(g_srv, tool, args);
|
||
*ms = now_ms() - t0;
|
||
return resp;
|
||
}
|
||
|
||
/* Check if ALL results have a specific label (validates label filter works) */
|
||
static int all_results_have_label(const char *resp, const char *label) {
|
||
if (!resp)
|
||
return 0;
|
||
/* Every "label":"X" in results must match. Check no other label appears. */
|
||
char pattern[64];
|
||
snprintf(pattern, sizeof(pattern), "\"label\":\"%s\"", label);
|
||
char escaped[64];
|
||
snprintf(escaped, sizeof(escaped), "\\\"label\\\":\\\"%s\\\"", label);
|
||
/* If response contains "label" at all, every one must match */
|
||
const char *p = resp;
|
||
while ((p = strstr(p, "label")) != NULL) {
|
||
/* Skip the key name — find the value */
|
||
p += 5; /* past "label" */
|
||
const char *val = strstr(p, ":\"");
|
||
if (!val || val > p + 10) {
|
||
p++;
|
||
continue;
|
||
}
|
||
val += 2;
|
||
/* Check this label value matches */
|
||
if (strncmp(val, label, strlen(label)) != 0) {
|
||
/* Check for escaped version */
|
||
const char *eval = strstr(p, ":\\\"");
|
||
if (eval && eval < p + 10) {
|
||
eval += 3;
|
||
if (strncmp(eval, label, strlen(label)) != 0)
|
||
return 0;
|
||
} else {
|
||
return 0;
|
||
}
|
||
}
|
||
p++;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* Check response contains a specific key (in nested JSON) */
|
||
static int resp_has_key(const char *resp, const char *key) {
|
||
if (!resp)
|
||
return 0;
|
||
char pattern[64];
|
||
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
||
if (strstr(resp, pattern) != NULL)
|
||
return 1;
|
||
snprintf(pattern, sizeof(pattern), "\\\"%s\\\"", key);
|
||
if (strstr(resp, pattern) != NULL)
|
||
return 1;
|
||
/* TOON scalar form (`key: value`), the search_graph default output. */
|
||
snprintf(pattern, sizeof(pattern), "%s: ", key);
|
||
if (strstr(resp, pattern) != NULL)
|
||
return 1;
|
||
/* TOON table-header form (`key[N]{...}:`), used by trace_path. */
|
||
snprintf(pattern, sizeof(pattern), "%s[", key);
|
||
return strstr(resp, pattern) != NULL;
|
||
}
|
||
|
||
/* Check response does NOT contain a specific key */
|
||
static int resp_lacks_key(const char *resp, const char *key) {
|
||
return !resp_has_key(resp, key);
|
||
}
|
||
|
||
/* Helper: assert tool call succeeds, warn if slow */
|
||
#define TOOL_OK(resp, ms) \
|
||
do { \
|
||
ASSERT((resp) != NULL); \
|
||
if ((int)(ms) > PERF_WARN_MS) { \
|
||
printf(" [PERF WARNING] tool call: %.0fms (>%dms)\n", (ms), PERF_WARN_MS); \
|
||
} \
|
||
} while (0)
|
||
|
||
/* Helper: assert response is not an error */
|
||
#define NOT_ERROR(resp) ASSERT(strstr((resp), "\"isError\":true") == NULL)
|
||
|
||
/* ── list_projects ─────────────────────────────────────────────── */
|
||
|
||
TEST(incr_file_node_name_stays_basename) {
|
||
/* #994: File nodes must keep their basename NAME through an incremental
|
||
* re-index (the bug renamed touched files' nodes to rel_path, diverging
|
||
* from a full build). Modify a file, re-index, assert no File node name
|
||
* contains a path separator. */
|
||
char path[512];
|
||
snprintf(path, sizeof(path), "%s/fastapi/applications.py", g_repodir);
|
||
FILE *f = fopen(path, "a");
|
||
ASSERT(f != NULL);
|
||
fprintf(f, "\n# incr_file_node_name_stays_basename touch\n");
|
||
fclose(f);
|
||
|
||
char *resp = index_repo();
|
||
ASSERT(resp != NULL);
|
||
free(resp);
|
||
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\",\"format\":\"json\","
|
||
"\"query\":\"MATCH (n:File) WHERE n.name CONTAINS '/'"
|
||
" RETURN n.name LIMIT 5\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* No File node may carry a path separator in its name; the query engine
|
||
* returns an empty rows array when nothing matches. */
|
||
ASSERT(strstr(r, "\"rows\":[]") != NULL || strstr(r, "\\\"rows\\\":[]") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_list_projects_basic) {
|
||
double ms;
|
||
char *r = call_tool_timed("list_projects", &ms, "{}");
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "projects") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_defines_method_more_than_10) {
|
||
write_file_at("fastapi/big_class.py", "class BigClass:\n"
|
||
" def m1(self): pass\n"
|
||
" def m2(self): pass\n"
|
||
" def m3(self): pass\n"
|
||
" def m4(self): pass\n"
|
||
" def m5(self): pass\n"
|
||
" def m6(self): pass\n"
|
||
" def m7(self): pass\n"
|
||
" def m8(self): pass\n"
|
||
" def m9(self): pass\n"
|
||
" def m10(self): pass\n"
|
||
" def m11(self): pass\n"
|
||
" def m12(self): pass\n"
|
||
" def m13(self): pass\n"
|
||
" def m14(self): pass\n"
|
||
" def m15(self): pass\n");
|
||
char *idx = index_repo();
|
||
ASSERT(idx != NULL);
|
||
free(idx);
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method)"
|
||
" WHERE c.name = 'BigClass' RETURN count(m) AS n\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "\"15\"") != NULL || strstr(r, "\\\"15\\\"") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_class_lines_nonzero) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (c:Class) WHERE c.lines > 0 RETURN count(c) AS n\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "\"0\"") == NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_list_projects_has_current) {
|
||
double ms;
|
||
char *r = call_tool_timed("list_projects", &ms, "{}");
|
||
TOOL_OK(r, ms);
|
||
/* Our project must be in the list. Project name is path-derived,
|
||
* may contain escaped slashes in JSON. Check for "fastapi" substring
|
||
* which will appear in the path-derived name regardless of escaping. */
|
||
/* Project list must contain at least one entry */
|
||
ASSERT(strstr(r, "projects") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── index_status ──────────────────────────────────────────────── */
|
||
|
||
TEST(tool_index_status_basic) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_status", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "nodes") != NULL || strstr(r, "indexed") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_index_status_nonexistent) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_status", &ms, "{\"project\":\"does-not-exist-xyz\"}");
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "not") != NULL || strstr(r, "error") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_graph_schema ──────────────────────────────────────────── */
|
||
|
||
TEST(tool_schema_has_labels) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_graph_schema", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "Function") != NULL);
|
||
ASSERT(strstr(r, "Method") != NULL);
|
||
ASSERT(strstr(r, "Module") != NULL);
|
||
ASSERT(strstr(r, "Variable") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_schema_has_edge_types) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_graph_schema", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "CALLS") != NULL);
|
||
ASSERT(strstr(r, "IMPORTS") != NULL);
|
||
ASSERT(strstr(r, "DEFINES") != NULL);
|
||
ASSERT(strstr(r, "CONTAINS_FILE") != NULL);
|
||
/* Schema should also report node and edge counts */
|
||
ASSERT(resp_has_key(r, "node_labels") || resp_has_key(r, "labels") ||
|
||
strstr(r, "Function") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: label filters ───────────────────────────────── */
|
||
|
||
TEST(tool_sg_label_function) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\",\"limit\":5}", g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 100);
|
||
/* Verify all returned results actually have label=Function */
|
||
ASSERT(all_results_have_label(r, "Function"));
|
||
/* Must not contain Method results */
|
||
ASSERT(strstr(r, "\"Method\"") == NULL && strstr(r, "\\\"Method\\\"") == NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_method) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Method\",\"limit\":5}", g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 50);
|
||
/* Verify all returned results actually have label=Method */
|
||
ASSERT(all_results_have_label(r, "Method"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_module) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"Module\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 200);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_variable) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"Variable\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_class) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"Class\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
/* FastAPI has classes */
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_route) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"Route\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
/* May or may not have routes */
|
||
ASSERT(strstr(r, "total") != NULL || strstr(r, "results") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_nonexistent) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"Nonexistent\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_EQ(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: name_pattern ────────────────────────────────── */
|
||
|
||
TEST(tool_sg_name_exact) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\"incr_test_injected\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_EQ(total, 1);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_name_regex) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\".*test.*\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_name_no_match) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\"zzz_nonexistent_zzz\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_EQ(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: degree filters ──────────────────────────────── */
|
||
|
||
TEST(tool_sg_min_degree) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"min_degree\":10}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_max_degree_zero) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"max_degree\":0}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* Isolated functions (no edges) */
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GTE(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_degree_range) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"min_degree\":2,\"max_degree\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: pagination ──────────────────────────────────── */
|
||
|
||
TEST(tool_sg_limit) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"limit\":3}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
/* total should show real count (much more than 3) */
|
||
ASSERT_GT(total, 100);
|
||
/* but has_more should be true (pagination indicator) */
|
||
ASSERT(resp_has_key(r, "has_more"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_offset) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"limit\":5,\"offset\":10}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "results") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: file_pattern ────────────────────────────────── */
|
||
|
||
TEST(tool_sg_file_pattern) {
|
||
double ms;
|
||
/* file_pattern is glob (converted to SQL LIKE), not regex */
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"file_pattern\":\"*fastapi*\",\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: include_connected ───────────────────────────── */
|
||
|
||
TEST(tool_sg_include_connected) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\"incr_test_injected\","
|
||
"\"include_connected\":true}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "connected") != NULL || strstr(r, "results") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: relationship filter ─────────────────────────── */
|
||
|
||
TEST(tool_sg_relationship) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"relationship\":\"CALLS\",\"min_degree\":1}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: qn_pattern ──────────────────────────────────── */
|
||
|
||
TEST(tool_sg_qn_pattern) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"qn_pattern\":\".*fastapi.*applications.*\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: combined filters ────────────────────────────── */
|
||
|
||
TEST(tool_sg_combined_filters) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\".*__init__.*\","
|
||
"\"min_degree\":1,\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "results") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: project-only (no filters) ───────────────────── */
|
||
|
||
TEST(tool_sg_project_only) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 1000);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: error cases ─────────────────────────────────── */
|
||
|
||
TEST(tool_sg_invalid_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"nonexistent\",\"label\":\"Function\"}");
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "error") != NULL || strstr(r, "not found") != NULL ||
|
||
strstr(r, "not_found") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: basic patterns ───────────────────────────────── */
|
||
|
||
TEST(tool_qg_match_nodes) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) RETURN n.name LIMIT 10\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
ASSERT_LTE(total, 10);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_match_edges) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[r:CALLS]->(b) RETURN a.name, b.name LIMIT 10\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_match_imports) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[r:IMPORTS]->(b) RETURN a.name, b.name LIMIT 10\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_match_defines) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[r:DEFINES]->(b) RETURN a.name, b.name LIMIT 10\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_match_contains) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[r:CONTAINS_FILE]->(b) RETURN a.name, b.name LIMIT 5\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: WHERE clause ─────────────────────────────────── */
|
||
|
||
TEST(tool_qg_where_name) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) WHERE n.name = 'incr_test_injected' RETURN n.name\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_EQ(total, 1);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: multi-hop ────────────────────────────────────── */
|
||
|
||
TEST(tool_qg_two_hop) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[:CALLS]->(b)-[:CALLS]->(c) RETURN a.name, "
|
||
"b.name, c.name LIMIT 5\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* May have 2-hop call chains */
|
||
ASSERT(strstr(r, "columns") != NULL || strstr(r, "rows") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: max_rows ─────────────────────────────────────── */
|
||
|
||
TEST(tool_qg_max_rows) {
|
||
double ms;
|
||
/* Query without max_rows — gets many results */
|
||
char *r1 = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) RETURN n.name LIMIT 100\"}",
|
||
g_project);
|
||
TOOL_OK(r1, ms);
|
||
int total_unlimited = count_in_response(r1, "total");
|
||
free(r1);
|
||
|
||
/* Same query without LIMIT but with max_rows=3 — must cap results */
|
||
char *r2 = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) RETURN n.name\","
|
||
"\"max_rows\":3}",
|
||
g_project);
|
||
TOOL_OK(r2, ms);
|
||
int total_limited = count_in_response(r2, "total");
|
||
ASSERT_LTE(total_limited, 3);
|
||
/* Without max_rows should have more than with */
|
||
ASSERT_GT(total_unlimited, total_limited);
|
||
free(r2);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: RETURN properties ────────────────────────────── */
|
||
|
||
TEST(tool_qg_return_properties) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Module) RETURN n.name, n.file_path LIMIT 3\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
/* Should have both columns */
|
||
ASSERT(strstr(r, "n.name") != NULL || strstr(r, "columns") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: error cases ──────────────────────────────────── */
|
||
|
||
TEST(tool_qg_invalid_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"nonexistent\","
|
||
"\"query\":\"MATCH (n) RETURN n LIMIT 1\"}");
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "error") != NULL || strstr(r, "not found") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: modes ────────────────────────────────────────── */
|
||
|
||
TEST(tool_sc_compact) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"import\",\"mode\":\"compact\",\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* compact mode has "results" key with enriched matches */
|
||
ASSERT(resp_has_key(r, "results"));
|
||
ASSERT(resp_has_key(r, "total_results"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sc_full) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"FastAPI\",\"mode\":\"full\",\"limit\":3}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* full mode includes source code */
|
||
ASSERT(resp_has_key(r, "results") || resp_has_key(r, "raw_matches"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sc_files) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"def \",\"mode\":\"files\",\"limit\":10}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* files mode has "files" key, NOT "results" */
|
||
ASSERT(resp_has_key(r, "files"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: regex ────────────────────────────────────────── */
|
||
|
||
TEST(tool_sc_regex) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"def\\\\s+\\\\w+_handler\",\"regex\":true,\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: file_pattern + path_filter ───────────────────── */
|
||
|
||
TEST(tool_sc_file_pattern) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"class\",\"file_pattern\":\"*.py\",\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sc_path_filter) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"import\",\"path_filter\":\"fastapi/\",\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: context lines ────────────────────────────────── */
|
||
|
||
TEST(tool_sc_context) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"FastAPI\",\"context\":3,\"limit\":3}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: no results ───────────────────────────────────── */
|
||
|
||
TEST(tool_sc_no_results) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"xyzzy_absolutely_no_match_99\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_code_snippet ──────────────────────────────────────────── */
|
||
|
||
TEST(tool_snippet_short_name) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_code_snippet", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"qualified_name\":\"incr_test_injected\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* Should return source or suggestions */
|
||
ASSERT(strstr(r, "def") != NULL || strstr(r, "return") != NULL ||
|
||
strstr(r, "suggest") != NULL || strstr(r, "match") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_snippet_nonexistent) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_code_snippet", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"qualified_name\":\"nonexistent_func_xyz\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* Should return not found or suggestions, not crash */
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_snippet_include_neighbors) {
|
||
double ms;
|
||
/* Use include_neighbors=true — response may include caller_names
|
||
* (depends on whether the function has callers). Key thing:
|
||
* the response must differ from include_neighbors=false and must
|
||
* contain source code. */
|
||
char *r = call_tool_timed("get_code_snippet", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"qualified_name\":\"incr_test_injected\","
|
||
"\"include_neighbors\":true}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* Must have source code */
|
||
ASSERT(resp_has_key(r, "source"));
|
||
/* Must have callers/callees arrays (may be empty) */
|
||
ASSERT(resp_has_key(r, "callers") || resp_has_key(r, "callees"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── trace_path ───────────────────────────────────────────── */
|
||
|
||
TEST(tool_trace_outbound) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"outbound\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* direction=outbound → response has "callees", no "callers" */
|
||
ASSERT(resp_has_key(r, "callees"));
|
||
ASSERT(resp_lacks_key(r, "callers"));
|
||
ASSERT(resp_has_key(r, "direction"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_trace_inbound) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"inbound\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* direction=inbound → response has "callers", no "callees" */
|
||
ASSERT(resp_has_key(r, "callers"));
|
||
ASSERT(resp_lacks_key(r, "callees"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_trace_both) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"both\",\"depth\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* direction=both → response has both "callers" and "callees" */
|
||
ASSERT(resp_has_key(r, "callers"));
|
||
ASSERT(resp_has_key(r, "callees"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_trace_depth_1) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"both\",\"depth\":1}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_trace_nonexistent) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"no_such_function_xyz\","
|
||
"\"direction\":\"both\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* Should return not found or empty, not crash */
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_trace_edge_types) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"both\","
|
||
"\"edge_types\":[\"CALLS\"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_architecture ──────────────────────────────────────────── */
|
||
|
||
TEST(tool_arch_structure) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_architecture", &ms,
|
||
"{\"project\":\"%s\",\"aspects\":[\"structure\"]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "fastapi") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_arch_all) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_architecture", &ms, "{\"project\":\"%s\",\"aspects\":[\"all\"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_arch_no_aspects) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_architecture", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── detect_changes ────────────────────────────────────────────── */
|
||
|
||
TEST(tool_detect_changes_default) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
/* New tree contract: base + direction scalars, a changed_files section,
|
||
* and the seed/impact accounting. (The old changed_count/impacted_symbols/
|
||
* depth keys are gone — impact is now a real traversal, not bare names.) */
|
||
ASSERT(strstr(r, "changed_files:") != NULL);
|
||
ASSERT(strstr(r, "direction:") != NULL);
|
||
ASSERT(strstr(r, "seed_symbols:") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* The blast radius is a REAL traversal now: default inbound gives transitive
|
||
* callers of the changed symbols with an exact impacted_total and a module
|
||
* rollup. Fixture diff may be empty (shallow clone at HEAD) — the sections and
|
||
* their accounting must still be present and internally consistent. */
|
||
TEST(tool_detect_changes_impact_shape) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms,
|
||
"{\"project\":\"%s\",\"base_branch\":\"HEAD\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "direction: inbound") != NULL); /* default = blast radius */
|
||
ASSERT(strstr(r, "seed_symbols:") != NULL);
|
||
/* format:"json" returns the same model as structured JSON. */
|
||
free(r);
|
||
r = call_tool_timed("detect_changes", &ms,
|
||
"{\"project\":\"%s\",\"base_branch\":\"HEAD\",\"format\":\"json\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(resp_has_key(r, "impacted_total"));
|
||
ASSERT(resp_has_key(r, "direction"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_detect_changes_custom_branch) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms,
|
||
"{\"project\":\"%s\",\"base_branch\":\"HEAD\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "changed") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* Regression: `since` was advertised in the schema but ignored by the handler;
|
||
* it must be honored as the diff base. Fixture is a --depth=1 shallow clone, so
|
||
* HEAD~N won't resolve — use HEAD for a valid (empty) diff. */
|
||
TEST(tool_detect_changes_since) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"%s\",\"since\":\"HEAD\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(resp_has_key(r, "changed_files"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* Regression: `since` must take precedence over base_branch. A valid since plus a
|
||
* bogus base_branch must still succeed (proving since won) and must not reference
|
||
* the bogus branch. */
|
||
TEST(tool_detect_changes_since_precedence) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"detect_changes", &ms,
|
||
"{\"project\":\"%s\",\"since\":\"HEAD\",\"base_branch\":\"no-such-branch-xyz\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "no-such-branch-xyz") == NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_detect_changes_depth) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"%s\",\"depth\":5}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── manage_adr ────────────────────────────────────────────────── */
|
||
|
||
TEST(tool_adr_get) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("manage_adr", &ms, "{\"project\":\"%s\",\"mode\":\"get\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
/* May return empty if no ADR exists yet */
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_adr_sections) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("manage_adr", &ms, "{\"project\":\"%s\",\"mode\":\"sections\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── ingest_traces ─────────────────────────────────────────────── */
|
||
|
||
TEST(tool_ingest_traces_empty) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("ingest_traces", &ms, "{\"project\":\"%s\",\"traces\":[]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_ingest_traces_basic) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"ingest_traces", &ms,
|
||
"{\"project\":\"%s\",\"traces\":[{\"caller\":\"a\",\"callee\":\"b\"}]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── Error handling: every tool with bad project ───────────────── */
|
||
|
||
TEST(tool_err_query_bad_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"xxx\","
|
||
"\"query\":\"MATCH (n) RETURN n LIMIT 1\"}");
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "error") != NULL || strstr(r, "not found") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_err_search_code_bad_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms, "{\"project\":\"xxx\",\"pattern\":\"test\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_err_snippet_bad_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_code_snippet", &ms,
|
||
"{\"project\":\"xxx\",\"qualified_name\":\"test\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_err_trace_bad_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"xxx\","
|
||
"\"function_name\":\"test\",\"direction\":\"both\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_err_arch_bad_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_architecture", &ms, "{\"project\":\"xxx\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_err_detect_bad_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"xxx\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_err_delete_nonexistent) {
|
||
double ms;
|
||
char *r = call_tool_timed("delete_project", &ms, "{\"project\":\"xxx-nonexistent\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── index_repository: mode param ───────────────────────────────── */
|
||
|
||
TEST(tool_index_mode_fast) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_repository", &ms, "{\"repo_path\":\"%s\",\"mode\":\"fast\"}",
|
||
g_repodir);
|
||
ASSERT(r != NULL);
|
||
ASSERT(strstr(r, "indexed") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_index_publish_metadata) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_repository", &ms, "{\"repo_path\":\"%s\",\"mode\":\"fast\"}",
|
||
g_repodir);
|
||
ASSERT(r != NULL);
|
||
ASSERT(strstr(r, "\\\"publish_kind\\\":\\\"") != NULL);
|
||
ASSERT(strstr(r, "\\\"graph_changed\\\":") != NULL);
|
||
ASSERT(strstr(r, "\\\"publish_kind\\\":\\\"full\\\"") != NULL ||
|
||
strstr(r, "\\\"publish_kind\\\":\\\"incremental_noop\\\"") != NULL ||
|
||
strstr(r, "\\\"publish_kind\\\":\\\"incremental_exact\\\"") != NULL ||
|
||
strstr(r, "\\\"publish_kind\\\":\\\"incremental_containment\\\"") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_index_invalid_path) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_repository", &ms, "{\"repo_path\":\"/nonexistent/path/xyz\"}");
|
||
TOOL_OK(r, ms);
|
||
/* Should fail gracefully */
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_index_missing_param) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_repository", &ms, "{}");
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "required") != NULL || strstr(r, "error") != NULL ||
|
||
strstr(r, "repo_path") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: exclude_entry_points ────────────────────────── */
|
||
|
||
TEST(tool_sg_exclude_entry_points) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"exclude_entry_points\":true,\"max_degree\":0}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GTE(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_exclude_entry_points_false) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"exclude_entry_points\":false,\"max_degree\":0}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GTE(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: high offset (beyond results) ───────────────── */
|
||
|
||
TEST(tool_sg_high_offset) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"offset\":999999,\"limit\":10}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* Should return 0 results at this offset */
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: name_pattern edge cases ─────────────────────── */
|
||
|
||
TEST(tool_sg_name_pattern_dot_star) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\".*\",\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 100);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_name_pattern_anchored) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\"^incr_test_injected$\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_EQ(total, 1);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: more Cypher patterns ─────────────────────────── */
|
||
|
||
TEST(tool_qg_count_functions) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) RETURN n.name LIMIT 200\","
|
||
"\"max_rows\":200}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 50);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_edge_properties) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[r:CALLS]->(b) RETURN a.name, b.name, r.callee LIMIT 5\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "columns") != NULL || strstr(r, "rows") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_node_file_path) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) WHERE n.name = 'incr_test_injected' "
|
||
"RETURN n.name, n.file_path, n.start_line\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_EQ(total, 1);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_match_module_defines) {
|
||
double ms;
|
||
/* DEFINES edges connect modules to their symbols */
|
||
char *r =
|
||
call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[:DEFINES]->(b) RETURN a.name, b.name LIMIT 10\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_GT(total, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_qg_max_rows_1) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) RETURN n.name\","
|
||
"\"max_rows\":1}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int total = count_in_response(r, "total");
|
||
ASSERT_EQ(total, 1);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: limit param ──────────────────────────────────── */
|
||
|
||
TEST(tool_sc_limit_1) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"def \",\"limit\":1}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sc_limit_50) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"import\",\"limit\":50}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: combined params ──────────────────────────────── */
|
||
|
||
TEST(tool_sc_combined) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"class\","
|
||
"\"mode\":\"compact\","
|
||
"\"file_pattern\":\"*.py\","
|
||
"\"path_filter\":\"fastapi/\","
|
||
"\"limit\":5,\"context\":2}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_code_snippet: full QN from search ─────────────────────── */
|
||
|
||
TEST(tool_snippet_full_qn) {
|
||
/* First find a function QN via search */
|
||
double ms;
|
||
char *sr = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\"incr_test_helper\",\"limit\":1}",
|
||
g_project);
|
||
TOOL_OK(sr, ms);
|
||
|
||
/* Extract qualified_name */
|
||
const char *qn_start = strstr(sr, "qualified_name");
|
||
if (qn_start) {
|
||
qn_start = strchr(qn_start, ':');
|
||
if (qn_start) {
|
||
qn_start += 2; /* skip :" */
|
||
/* Handle escaped quotes in JSON */
|
||
if (*qn_start == '\\')
|
||
qn_start += 2;
|
||
const char *qn_end = strchr(qn_start, '"');
|
||
if (!qn_end)
|
||
qn_end = strchr(qn_start, '\\');
|
||
if (qn_end && qn_end > qn_start) {
|
||
char qn[512];
|
||
int len = (int)(qn_end - qn_start);
|
||
if (len > 0 && len < (int)sizeof(qn)) {
|
||
memcpy(qn, qn_start, (size_t)len);
|
||
qn[len] = '\0';
|
||
|
||
char *r = call_tool_timed("get_code_snippet", &ms,
|
||
"{\"project\":\"%s\",\"qualified_name\":\"%s\"}",
|
||
g_project, qn);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "source") != NULL || strstr(r, "def") != NULL ||
|
||
strstr(r, "return") != NULL);
|
||
free(r);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
free(sr);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_architecture: specific aspects ────────────────────────── */
|
||
|
||
TEST(tool_arch_dependencies) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_architecture", &ms,
|
||
"{\"project\":\"%s\",\"aspects\":[\"dependencies\"]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_arch_entry_points) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_architecture", &ms,
|
||
"{\"project\":\"%s\",\"aspects\":[\"entry_points\"]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_arch_routes) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_architecture", &ms,
|
||
"{\"project\":\"%s\",\"aspects\":[\"routes\"]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_arch_multi_aspects) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"get_architecture", &ms,
|
||
"{\"project\":\"%s\",\"aspects\":[\"structure\",\"dependencies\",\"entry_points\"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── detect_changes: scope param ───────────────────────────────── */
|
||
|
||
TEST(tool_detect_changes_scope) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"%s\",\"scope\":\"fastapi/\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── manage_adr: update + content ──────────────────────────────── */
|
||
|
||
TEST(tool_adr_update) {
|
||
double ms;
|
||
char *r = call_tool_timed("manage_adr", &ms,
|
||
"{\"project\":\"%s\",\"mode\":\"update\","
|
||
"\"content\":\"# Test ADR\\n\\nThis is a test ADR.\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
|
||
/* Verify it was stored */
|
||
r = call_tool_timed("manage_adr", &ms, "{\"project\":\"%s\",\"mode\":\"get\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "Test ADR") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_adr_sections_array) {
|
||
double ms;
|
||
char *r = call_tool_timed("manage_adr", &ms,
|
||
"{\"project\":\"%s\",\"mode\":\"sections\","
|
||
"\"sections\":[\"overview\",\"decisions\"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── ingest_traces: multiple traces ────────────────────────────── */
|
||
|
||
TEST(tool_ingest_traces_multiple) {
|
||
double ms;
|
||
char *r = call_tool_timed("ingest_traces", &ms,
|
||
"{\"project\":\"%s\",\"traces\":["
|
||
"{\"caller\":\"funcA\",\"callee\":\"funcB\",\"count\":5},"
|
||
"{\"caller\":\"funcB\",\"callee\":\"funcC\",\"count\":3},"
|
||
"{\"caller\":\"funcC\",\"callee\":\"funcD\",\"count\":1}"
|
||
"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── trace_path: more edge_types combos ───────────────────── */
|
||
|
||
TEST(tool_trace_imports_only) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"both\","
|
||
"\"edge_types\":[\"IMPORTS\"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_trace_calls_and_imports) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"both\","
|
||
"\"edge_types\":[\"CALLS\",\"IMPORTS\"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_trace_deep) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"outbound\",\"depth\":10}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════
|
||
* GAP COVERAGE: every remaining tool × param combination
|
||
* ═══════════════════════════════════════════════════════════════ */
|
||
|
||
/* ── search_graph: remaining relationship values ───────────────── */
|
||
|
||
TEST(tool_sg_rel_imports) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Module\","
|
||
"\"relationship\":\"IMPORTS\",\"min_degree\":1}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int t = count_in_response(r, "total");
|
||
ASSERT_GT(t, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_rel_defines) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Module\","
|
||
"\"relationship\":\"DEFINES\",\"min_degree\":1}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int t = count_in_response(r, "total");
|
||
ASSERT_GT(t, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_rel_contains_file) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Folder\","
|
||
"\"relationship\":\"CONTAINS_FILE\",\"min_degree\":1}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: remaining label values ──────────────────────── */
|
||
|
||
TEST(tool_sg_label_file) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"File\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_folder) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"Folder\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
TEST(tool_sg_label_package) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms, "{\"project\":\"%s\",\"label\":\"Package\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: include_connected=false ─────────────────────── */
|
||
|
||
TEST(tool_sg_include_connected_false) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"name_pattern\":\"incr_test_injected\","
|
||
"\"include_connected\":false}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int t = count_in_response(r, "total");
|
||
ASSERT_EQ(t, 1);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: qn_pattern + label combined ─────────────────── */
|
||
|
||
TEST(tool_sg_qn_and_label) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"qn_pattern\":\".*applications.*\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int t = count_in_response(r, "total");
|
||
ASSERT_GT(t, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_graph: file_pattern + name_pattern combined ────────── */
|
||
|
||
TEST(tool_sg_file_and_name) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_graph", &ms,
|
||
"{\"project\":\"%s\",\"label\":\"Function\","
|
||
"\"file_pattern\":\"*fastapi*\","
|
||
"\"name_pattern\":\".*\",\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: CONFIGURES edges ─────────────────────────────── */
|
||
|
||
TEST(tool_qg_configures) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[r:CONFIGURES]->(b) RETURN a.name, b.name LIMIT 5\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* May have 0 results if no CONFIGURES edges in FastAPI */
|
||
ASSERT(strstr(r, "columns") != NULL || strstr(r, "rows") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: HANDLES edges ────────────────────────────────── */
|
||
|
||
TEST(tool_qg_handles) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (a)-[r:HANDLES]->(b) RETURN a.name, b.name LIMIT 5\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "columns") != NULL || strstr(r, "rows") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: DEFINES_METHOD edges ─────────────────────────── */
|
||
|
||
TEST(tool_qg_defines_method) {
|
||
double ms;
|
||
char *r = call_tool_timed("query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (c:Class)-[r:DEFINES_METHOD]->(m:Method) RETURN "
|
||
"c.name, m.name LIMIT 5\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "columns") != NULL || strstr(r, "rows") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: no LIMIT (default behavior) ──────────────────── */
|
||
|
||
TEST(tool_qg_no_limit) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) WHERE n.name = 'incr_test_injected' RETURN n.name\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int t = count_in_response(r, "total");
|
||
ASSERT_GT(t, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── query_graph: empty result ─────────────────────────────────── */
|
||
|
||
TEST(tool_qg_empty_result) {
|
||
double ms;
|
||
char *r = call_tool_timed(
|
||
"query_graph", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"query\":\"MATCH (n:Function) WHERE n.name = 'zzz_no_exist_zzz' RETURN n.name\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
int t = count_in_response(r, "total");
|
||
ASSERT_EQ(t, 0);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: regex=false explicit ─────────────────────────── */
|
||
|
||
TEST(tool_sc_regex_false) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"FastAPI\",\"regex\":false,\"limit\":3}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: context=0 ────────────────────────────────────── */
|
||
|
||
TEST(tool_sc_context_zero) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"import\",\"context\":0,\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── search_code: special chars in pattern ─────────────────────── */
|
||
|
||
TEST(tool_sc_special_chars) {
|
||
double ms;
|
||
char *r = call_tool_timed("search_code", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"pattern\":\"def __init__(self\",\"limit\":5}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_code_snippet: include_neighbors=false ─────────────────── */
|
||
|
||
TEST(tool_snippet_neighbors_false) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_code_snippet", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"qualified_name\":\"incr_test_injected\","
|
||
"\"include_neighbors\":false}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* include_neighbors=false should NOT have "caller_names" field */
|
||
ASSERT(resp_lacks_key(r, "caller_names"));
|
||
/* But must still have source code */
|
||
ASSERT(resp_has_key(r, "source"));
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_code_snippet: Class name ──────────────────────────────── */
|
||
|
||
TEST(tool_snippet_class) {
|
||
/* FastAPI has classes — try a common one */
|
||
double ms;
|
||
char *r = call_tool_timed("get_code_snippet", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"qualified_name\":\"FastAPI\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── trace_path: depth=0 ──────────────────────────────────── */
|
||
|
||
TEST(tool_trace_depth_0) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"both\",\"depth\":0}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── trace_path: edge_types=[DEFINES] ─────────────────────── */
|
||
|
||
TEST(tool_trace_defines_only) {
|
||
double ms;
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"inbound\","
|
||
"\"edge_types\":[\"DEFINES\"]}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── trace_path: include_tests flag ────────────────────────────── */
|
||
|
||
TEST(tool_trace_include_tests) {
|
||
double ms;
|
||
/* Default (include_tests=false): test files should be filtered out */
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"inbound\","
|
||
"\"include_tests\":false}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* Result should not contain is_test markers when tests are excluded */
|
||
free(r);
|
||
|
||
/* With include_tests=true: test files should appear with is_test marker */
|
||
r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"inbound\","
|
||
"\"include_tests\":true}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── trace_path: risk_labels flag ─────────────────────────────── */
|
||
|
||
TEST(tool_trace_risk_labels) {
|
||
double ms;
|
||
/* Use outbound direction — incr_test_injected may call stdlib functions */
|
||
char *r = call_tool_timed("trace_path", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"function_name\":\"incr_test_injected\","
|
||
"\"direction\":\"outbound\","
|
||
"\"risk_labels\":true}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
/* If there are callees, they should have risk labels.
|
||
* If no callees, the response is still valid — just empty. */
|
||
if (strstr(r, "\"hop\"") != NULL) {
|
||
ASSERT(strstr(r, "\"risk\"") != NULL);
|
||
}
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── detect_changes: nonexistent branch ────────────────────────── */
|
||
|
||
TEST(tool_detect_nonexistent_branch) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms,
|
||
"{\"project\":\"%s\","
|
||
"\"base_branch\":\"nonexistent-branch-xyz\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
/* Should handle gracefully — git diff may fail or return empty */
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── detect_changes: depth=1 ───────────────────────────────────── */
|
||
|
||
TEST(tool_detect_depth_1) {
|
||
double ms;
|
||
char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"%s\",\"depth\":1}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── manage_adr: default mode (omitted) ────────────────────────── */
|
||
|
||
TEST(tool_adr_default_mode) {
|
||
double ms;
|
||
char *r = call_tool_timed("manage_adr", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── manage_adr: update with empty content ─────────────────────── */
|
||
|
||
TEST(tool_adr_update_empty) {
|
||
double ms;
|
||
char *r = call_tool_timed("manage_adr", &ms,
|
||
"{\"project\":\"%s\",\"mode\":\"update\","
|
||
"\"content\":\"\"}",
|
||
g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── ingest_traces: trace with missing fields ──────────────────── */
|
||
|
||
TEST(tool_ingest_traces_partial) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("ingest_traces", &ms,
|
||
"{\"project\":\"%s\",\"traces\":[{\"caller\":\"onlyA\"}]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── Error: get_graph_schema bad project ───────────────────────── */
|
||
|
||
TEST(tool_err_schema_bad_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("get_graph_schema", &ms, "{\"project\":\"nonexistent-xyz\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── Error: index_status missing project ───────────────────────── */
|
||
|
||
TEST(tool_err_index_status_no_project) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_status", &ms, "{}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── Error: manage_adr bad project ─────────────────────────────── */
|
||
|
||
TEST(tool_err_adr_bad_project) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("manage_adr", &ms, "{\"project\":\"nonexistent-xyz\",\"mode\":\"get\"}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── Error: ingest_traces bad project ──────────────────────────── */
|
||
|
||
TEST(tool_err_ingest_bad_project) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("ingest_traces", &ms, "{\"project\":\"nonexistent-xyz\",\"traces\":[]}");
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── Error: ingest_traces missing traces array ─────────────────── */
|
||
|
||
TEST(tool_err_ingest_no_traces) {
|
||
double ms;
|
||
char *r = call_tool_timed("ingest_traces", &ms, "{\"project\":\"%s\"}", g_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── index_repository: mode=full explicit ──────────────────────── */
|
||
|
||
TEST(tool_index_mode_full) {
|
||
double ms;
|
||
char *r = call_tool_timed("index_repository", &ms, "{\"repo_path\":\"%s\",\"mode\":\"full\"}",
|
||
g_repodir);
|
||
ASSERT(r != NULL);
|
||
ASSERT(strstr(r, "indexed") != NULL);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── get_architecture: empty aspects array ─────────────────────── */
|
||
|
||
TEST(tool_arch_empty_aspects) {
|
||
double ms;
|
||
char *r =
|
||
call_tool_timed("get_architecture", &ms, "{\"project\":\"%s\",\"aspects\":[]}", g_project);
|
||
TOOL_OK(r, ms);
|
||
NOT_ERROR(r);
|
||
free(r);
|
||
PASS();
|
||
}
|
||
|
||
/* ── delete_project: real delete + verify ──────────────────────── */
|
||
/* NOTE: This is destructive — must run LAST */
|
||
|
||
TEST(tool_delete_and_verify) {
|
||
/* Create a throwaway project to delete */
|
||
char tmpdir2[256];
|
||
snprintf(tmpdir2, sizeof(tmpdir2), "%s/throwaway", g_tmpdir);
|
||
cbm_mkdir(tmpdir2);
|
||
char pypath[512];
|
||
snprintf(pypath, sizeof(pypath), "%s/dummy.py", tmpdir2);
|
||
FILE *f = fopen(pypath, "w");
|
||
if (f) {
|
||
fprintf(f, "def throwaway():\n pass\n");
|
||
fclose(f);
|
||
}
|
||
|
||
double ms;
|
||
char *r = call_tool_timed("index_repository", &ms, "{\"repo_path\":\"%s\"}", tmpdir2);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
|
||
char *throwaway_project = cbm_project_name_from_path(tmpdir2);
|
||
ASSERT(throwaway_project != NULL);
|
||
|
||
/* Verify it exists */
|
||
r = call_tool_timed("index_status", &ms, "{\"project\":\"%s\"}", throwaway_project);
|
||
TOOL_OK(r, ms);
|
||
free(r);
|
||
|
||
/* Delete it */
|
||
r = call_tool_timed("delete_project", &ms, "{\"project\":\"%s\"}", throwaway_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "deleted") != NULL);
|
||
free(r);
|
||
|
||
/* Verify it's gone */
|
||
r = call_tool_timed("index_status", &ms, "{\"project\":\"%s\"}", throwaway_project);
|
||
TOOL_OK(r, ms);
|
||
ASSERT(strstr(r, "not") != NULL || strstr(r, "error") != NULL);
|
||
free(r);
|
||
|
||
free(throwaway_project);
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════
|
||
* SUITE
|
||
* ══════════════════════════════════════════════════════════════════ */
|
||
|
||
SUITE(incremental) {
|
||
if (incremental_setup() != 0) {
|
||
printf(" SETUP FAILED — skipping incremental tests (network?)\n");
|
||
return;
|
||
}
|
||
|
||
/* Phase 1: Full index baseline (needed for tool tests below) */
|
||
RUN_TEST(incr_full_index);
|
||
RUN_TEST(incr_full_has_functions);
|
||
RUN_TEST(incr_full_edge_types);
|
||
|
||
/* Inject test functions so tool tests (trace_path etc.) always have data,
|
||
* even when perf phases are skipped on CI. Update baseline counts. */
|
||
{
|
||
char path[512];
|
||
snprintf(path, sizeof(path), "%s/fastapi/applications.py", g_repodir);
|
||
FILE *f = fopen(path, "a");
|
||
if (f) {
|
||
fprintf(f, "\n\ndef incr_test_injected(x: int) -> int:\n"
|
||
" return x * 42\n"
|
||
"\n"
|
||
"def incr_test_helper(y: str) -> str:\n"
|
||
" return y.upper()\n");
|
||
fclose(f);
|
||
}
|
||
char *resp = index_repo();
|
||
free(resp);
|
||
g_full_nodes = get_node_count();
|
||
g_full_edges = get_edge_count();
|
||
g_full_calls = get_edge_count_by_type("CALLS");
|
||
g_full_imports = get_edge_count_by_type("IMPORTS");
|
||
}
|
||
|
||
/* Phase 2: Noop */
|
||
RUN_TEST(incr_noop_reindex);
|
||
|
||
/* Phase 3: Incremental deltas */
|
||
RUN_TEST(incr_modify_file);
|
||
RUN_TEST(incr_file_node_name_stays_basename);
|
||
RUN_TEST(incr_formatter_run);
|
||
RUN_TEST(incr_add_file);
|
||
RUN_TEST(incr_delete_file);
|
||
RUN_TEST(incr_simultaneous_changes);
|
||
|
||
/* Phase 4: Adversarial */
|
||
RUN_TEST(incr_empty_file);
|
||
RUN_TEST(incr_syntax_error);
|
||
RUN_TEST(incr_huge_single_function);
|
||
RUN_TEST(incr_binary_content);
|
||
RUN_TEST(incr_large_generated);
|
||
RUN_TEST(incr_new_subdir);
|
||
|
||
/* Phase 5: Stress */
|
||
RUN_TEST(incr_rapid_reindex);
|
||
RUN_TEST(incr_replace_file_content);
|
||
RUN_TEST(incr_batch_add_delete);
|
||
|
||
/* Phase 6: Recovery + accuracy */
|
||
RUN_TEST(incr_db_deleted_recovery);
|
||
RUN_TEST(incr_accuracy_vs_full);
|
||
|
||
/* Phase 7: Performance regression */
|
||
RUN_TEST(incr_perf_single_file_fast);
|
||
|
||
/* Phase 8: list_projects + index_status + schema */
|
||
RUN_TEST(tool_list_projects_basic);
|
||
RUN_TEST(tool_list_projects_has_current);
|
||
RUN_TEST(tool_index_status_basic);
|
||
RUN_TEST(tool_index_status_nonexistent);
|
||
RUN_TEST(tool_schema_has_labels);
|
||
RUN_TEST(tool_schema_has_edge_types);
|
||
|
||
/* Phase 9: search_graph — every parameter */
|
||
RUN_TEST(tool_sg_label_function);
|
||
RUN_TEST(tool_sg_label_method);
|
||
RUN_TEST(tool_sg_label_module);
|
||
RUN_TEST(tool_sg_label_variable);
|
||
RUN_TEST(tool_sg_label_class);
|
||
RUN_TEST(tool_sg_label_route);
|
||
RUN_TEST(tool_sg_label_nonexistent);
|
||
RUN_TEST(tool_sg_name_exact);
|
||
RUN_TEST(tool_sg_name_regex);
|
||
RUN_TEST(tool_sg_name_no_match);
|
||
RUN_TEST(tool_sg_min_degree);
|
||
RUN_TEST(tool_sg_max_degree_zero);
|
||
RUN_TEST(tool_sg_degree_range);
|
||
RUN_TEST(tool_sg_limit);
|
||
RUN_TEST(tool_sg_offset);
|
||
RUN_TEST(tool_sg_file_pattern);
|
||
RUN_TEST(tool_sg_include_connected);
|
||
RUN_TEST(tool_sg_relationship);
|
||
RUN_TEST(tool_sg_qn_pattern);
|
||
RUN_TEST(tool_sg_combined_filters);
|
||
RUN_TEST(tool_sg_project_only);
|
||
RUN_TEST(tool_sg_invalid_project);
|
||
|
||
/* Phase 10: query_graph — Cypher patterns */
|
||
RUN_TEST(tool_qg_match_nodes);
|
||
RUN_TEST(tool_qg_match_edges);
|
||
RUN_TEST(tool_qg_match_imports);
|
||
RUN_TEST(tool_qg_match_defines);
|
||
RUN_TEST(tool_qg_match_contains);
|
||
RUN_TEST(tool_qg_where_name);
|
||
RUN_TEST(tool_qg_two_hop);
|
||
RUN_TEST(tool_qg_max_rows);
|
||
RUN_TEST(tool_qg_return_properties);
|
||
RUN_TEST(tool_qg_invalid_project);
|
||
|
||
/* Phase 11: search_code — modes + params */
|
||
RUN_TEST(tool_sc_compact);
|
||
RUN_TEST(tool_sc_full);
|
||
RUN_TEST(tool_sc_files);
|
||
RUN_TEST(tool_sc_regex);
|
||
RUN_TEST(tool_sc_file_pattern);
|
||
RUN_TEST(tool_sc_path_filter);
|
||
RUN_TEST(tool_sc_context);
|
||
RUN_TEST(tool_sc_no_results);
|
||
|
||
/* Phase 12: get_code_snippet */
|
||
RUN_TEST(tool_snippet_short_name);
|
||
RUN_TEST(tool_snippet_nonexistent);
|
||
RUN_TEST(tool_snippet_include_neighbors);
|
||
|
||
/* Phase 13: trace_path — directions + params */
|
||
RUN_TEST(tool_trace_outbound);
|
||
RUN_TEST(tool_trace_inbound);
|
||
RUN_TEST(tool_trace_both);
|
||
RUN_TEST(tool_trace_depth_1);
|
||
RUN_TEST(tool_trace_nonexistent);
|
||
RUN_TEST(tool_trace_edge_types);
|
||
|
||
/* Phase 14: get_architecture */
|
||
RUN_TEST(tool_arch_structure);
|
||
RUN_TEST(tool_arch_all);
|
||
RUN_TEST(tool_arch_no_aspects);
|
||
|
||
/* Phase 15: detect_changes */
|
||
RUN_TEST(tool_detect_changes_default);
|
||
RUN_TEST(tool_detect_changes_impact_shape);
|
||
RUN_TEST(tool_detect_changes_custom_branch);
|
||
RUN_TEST(tool_detect_changes_since);
|
||
RUN_TEST(tool_detect_changes_since_precedence);
|
||
RUN_TEST(tool_detect_changes_depth);
|
||
|
||
/* Phase 16: manage_adr */
|
||
RUN_TEST(tool_adr_get);
|
||
RUN_TEST(tool_adr_sections);
|
||
|
||
/* Phase 17: ingest_traces */
|
||
RUN_TEST(tool_ingest_traces_empty);
|
||
RUN_TEST(tool_ingest_traces_basic);
|
||
|
||
/* Phase 18: Error handling — every tool with bad project */
|
||
RUN_TEST(tool_err_query_bad_project);
|
||
RUN_TEST(tool_err_search_code_bad_project);
|
||
RUN_TEST(tool_err_snippet_bad_project);
|
||
RUN_TEST(tool_err_trace_bad_project);
|
||
RUN_TEST(tool_err_arch_bad_project);
|
||
RUN_TEST(tool_err_detect_bad_project);
|
||
RUN_TEST(tool_err_delete_nonexistent);
|
||
|
||
/* Phase 19: index_repository params */
|
||
RUN_TEST(tool_index_mode_fast);
|
||
RUN_TEST(tool_index_publish_metadata);
|
||
RUN_TEST(tool_index_invalid_path);
|
||
RUN_TEST(tool_index_missing_param);
|
||
|
||
/* Phase 20: search_graph remaining params */
|
||
RUN_TEST(tool_sg_exclude_entry_points);
|
||
RUN_TEST(tool_sg_exclude_entry_points_false);
|
||
RUN_TEST(tool_sg_high_offset);
|
||
RUN_TEST(tool_sg_name_pattern_dot_star);
|
||
RUN_TEST(tool_sg_name_pattern_anchored);
|
||
|
||
/* Phase 21: query_graph advanced */
|
||
RUN_TEST(tool_qg_count_functions);
|
||
RUN_TEST(tool_qg_edge_properties);
|
||
RUN_TEST(tool_qg_node_file_path);
|
||
RUN_TEST(tool_qg_match_module_defines);
|
||
RUN_TEST(tool_qg_max_rows_1);
|
||
|
||
/* Phase 22: search_code extra params */
|
||
RUN_TEST(tool_sc_limit_1);
|
||
RUN_TEST(tool_sc_limit_50);
|
||
RUN_TEST(tool_sc_combined);
|
||
|
||
/* Phase 23: get_code_snippet full QN */
|
||
RUN_TEST(tool_snippet_full_qn);
|
||
|
||
/* Phase 24: get_architecture specific aspects */
|
||
RUN_TEST(tool_arch_dependencies);
|
||
RUN_TEST(tool_arch_entry_points);
|
||
RUN_TEST(tool_arch_routes);
|
||
RUN_TEST(tool_arch_multi_aspects);
|
||
|
||
/* Phase 25: detect_changes scope */
|
||
RUN_TEST(tool_detect_changes_scope);
|
||
|
||
/* Phase 26: manage_adr update + sections */
|
||
RUN_TEST(tool_adr_update);
|
||
RUN_TEST(tool_adr_sections_array);
|
||
|
||
/* Phase 27: ingest_traces multiple */
|
||
RUN_TEST(tool_ingest_traces_multiple);
|
||
|
||
/* Phase 28: trace_path edge_types combos */
|
||
RUN_TEST(tool_trace_imports_only);
|
||
RUN_TEST(tool_trace_calls_and_imports);
|
||
RUN_TEST(tool_trace_deep);
|
||
|
||
/* Phase 29: search_graph remaining relationships */
|
||
RUN_TEST(tool_sg_rel_imports);
|
||
RUN_TEST(tool_sg_rel_defines);
|
||
RUN_TEST(tool_sg_rel_contains_file);
|
||
|
||
/* Phase 30: search_graph remaining labels */
|
||
RUN_TEST(tool_sg_label_file);
|
||
RUN_TEST(tool_sg_label_folder);
|
||
RUN_TEST(tool_sg_label_package);
|
||
|
||
/* Phase 31: search_graph param combos */
|
||
RUN_TEST(tool_sg_include_connected_false);
|
||
RUN_TEST(tool_sg_qn_and_label);
|
||
RUN_TEST(tool_sg_file_and_name);
|
||
|
||
/* Phase 32: query_graph remaining edge types */
|
||
RUN_TEST(tool_qg_configures);
|
||
RUN_TEST(tool_qg_handles);
|
||
RUN_TEST(tool_qg_defines_method);
|
||
RUN_TEST(tool_qg_defines_method_more_than_10);
|
||
RUN_TEST(tool_qg_class_lines_nonzero);
|
||
RUN_TEST(tool_qg_no_limit);
|
||
RUN_TEST(tool_qg_empty_result);
|
||
|
||
/* Phase 33: search_code remaining params */
|
||
RUN_TEST(tool_sc_regex_false);
|
||
RUN_TEST(tool_sc_context_zero);
|
||
RUN_TEST(tool_sc_special_chars);
|
||
|
||
/* Phase 34: get_code_snippet remaining */
|
||
RUN_TEST(tool_snippet_neighbors_false);
|
||
RUN_TEST(tool_snippet_class);
|
||
|
||
/* Phase 35: trace_path remaining */
|
||
RUN_TEST(tool_trace_depth_0);
|
||
RUN_TEST(tool_trace_defines_only);
|
||
RUN_TEST(tool_trace_include_tests);
|
||
RUN_TEST(tool_trace_risk_labels);
|
||
|
||
/* Phase 36: detect_changes remaining */
|
||
RUN_TEST(tool_detect_nonexistent_branch);
|
||
RUN_TEST(tool_detect_depth_1);
|
||
|
||
/* Phase 37: manage_adr remaining */
|
||
RUN_TEST(tool_adr_default_mode);
|
||
RUN_TEST(tool_adr_update_empty);
|
||
|
||
/* Phase 38: ingest_traces remaining */
|
||
RUN_TEST(tool_ingest_traces_partial);
|
||
|
||
/* Phase 39: error handling remaining tools */
|
||
RUN_TEST(tool_err_schema_bad_project);
|
||
RUN_TEST(tool_err_index_status_no_project);
|
||
RUN_TEST(tool_err_adr_bad_project);
|
||
RUN_TEST(tool_err_ingest_bad_project);
|
||
RUN_TEST(tool_err_ingest_no_traces);
|
||
|
||
/* Phase 40: index_repository mode=full + arch empty aspects */
|
||
RUN_TEST(tool_index_mode_full);
|
||
RUN_TEST(tool_arch_empty_aspects);
|
||
|
||
/* Phase 41: delete_project real flow (MUST BE LAST) */
|
||
RUN_TEST(tool_delete_and_verify);
|
||
|
||
incremental_teardown();
|
||
}
|