Files
deusdata--codebase-memory-mcp/tests/test_ui.c

440 lines
13 KiB
C

/*
* test_ui.c — Tests for the graph visualization UI module.
*
* Covers: config persistence, embedded asset lookup, layout engine.
*/
#include "../src/foundation/compat.h"
#include "../src/foundation/compat_fs.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "ui/config.h"
#include "ui/embedded_assets.h"
#include "ui/layout3d.h"
#include "store/store.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
#include <unistd.h>
#endif
/* ── Config tests ─────────────────────────────────────────────── */
TEST(config_load_defaults) {
/* Loading with no config file should give defaults */
cbm_ui_config_t cfg;
cfg.ui_enabled = true; /* set non-default to verify load overwrites */
cfg.ui_port = 1234;
/* Use a temp HOME to avoid touching real config */
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_test_config_XXXXXX");
char *td = cbm_mkdtemp(tmpdir);
ASSERT_NOT_NULL(td);
char *old_home = getenv("HOME") ? strdup(getenv("HOME")) : NULL;
cbm_setenv("HOME", td, 1);
cbm_ui_config_load(&cfg);
ASSERT_FALSE(cfg.ui_enabled);
ASSERT_EQ(cfg.ui_port, 9749);
/* Restore HOME */
if (old_home) {
cbm_setenv("HOME", old_home, 1);
free(old_home);
}
PASS();
}
TEST(config_save_and_reload) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_test_config_XXXXXX");
char *td = cbm_mkdtemp(tmpdir);
ASSERT_NOT_NULL(td);
char *old_home = getenv("HOME") ? strdup(getenv("HOME")) : NULL;
cbm_setenv("HOME", td, 1);
/* Save */
cbm_ui_config_t cfg = {.ui_enabled = true, .ui_port = 8080};
cbm_ui_config_save(&cfg);
/* Reload */
cbm_ui_config_t loaded;
cbm_ui_config_load(&loaded);
ASSERT_TRUE(loaded.ui_enabled);
ASSERT_EQ(loaded.ui_port, 8080);
if (old_home) {
cbm_setenv("HOME", old_home, 1);
free(old_home);
}
PASS();
}
TEST(config_overwrite) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_test_config_XXXXXX");
char *td = cbm_mkdtemp(tmpdir);
ASSERT_NOT_NULL(td);
char *old_home = getenv("HOME") ? strdup(getenv("HOME")) : NULL;
cbm_setenv("HOME", td, 1);
/* Save with ui_enabled=true */
cbm_ui_config_t cfg1 = {.ui_enabled = true, .ui_port = 9749};
cbm_ui_config_save(&cfg1);
/* Overwrite with ui_enabled=false */
cbm_ui_config_t cfg2 = {.ui_enabled = false, .ui_port = 9749};
cbm_ui_config_save(&cfg2);
/* Reload should show false */
cbm_ui_config_t loaded;
cbm_ui_config_load(&loaded);
ASSERT_FALSE(loaded.ui_enabled);
if (old_home) {
cbm_setenv("HOME", old_home, 1);
free(old_home);
}
PASS();
}
TEST(config_corrupt_file) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_test_config_XXXXXX");
char *td = cbm_mkdtemp(tmpdir);
ASSERT_NOT_NULL(td);
char *old_home = getenv("HOME") ? strdup(getenv("HOME")) : NULL;
cbm_setenv("HOME", td, 1);
/* Write garbage to config path */
char path[1024];
cbm_ui_config_path(path, (int)sizeof(path));
/* Ensure directory exists (portable — no system("mkdir -p")) */
char dir[1024];
snprintf(dir, sizeof(dir), "%s/.cache/codebase-memory-mcp", td);
cbm_mkdir_p(dir, 0755);
FILE *f = fopen(path, "w");
ASSERT_NOT_NULL(f);
fprintf(f, "this is not json!!!");
fclose(f);
/* Should load defaults, not crash */
cbm_ui_config_t cfg;
cbm_ui_config_load(&cfg);
ASSERT_FALSE(cfg.ui_enabled);
ASSERT_EQ(cfg.ui_port, 9749);
if (old_home) {
cbm_setenv("HOME", old_home, 1);
free(old_home);
}
PASS();
}
TEST(config_missing_fields) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_test_config_XXXXXX");
char *td = cbm_mkdtemp(tmpdir);
ASSERT_NOT_NULL(td);
char *old_home = getenv("HOME") ? strdup(getenv("HOME")) : NULL;
cbm_setenv("HOME", td, 1);
/* Write JSON with only ui_port */
char path[1024];
cbm_ui_config_path(path, (int)sizeof(path));
char dir[1024];
snprintf(dir, sizeof(dir), "%s/.cache/codebase-memory-mcp", td);
cbm_mkdir_p(dir, 0755);
FILE *f = fopen(path, "w");
ASSERT_NOT_NULL(f);
fprintf(f, "{\"ui_port\": 5555}");
fclose(f);
cbm_ui_config_t cfg;
cbm_ui_config_load(&cfg);
ASSERT_FALSE(cfg.ui_enabled); /* defaults for missing field */
ASSERT_EQ(cfg.ui_port, 5555); /* present field loaded */
if (old_home) {
cbm_setenv("HOME", old_home, 1);
free(old_home);
}
PASS();
}
/* ── Embedded asset tests ─────────────────────────────────────── */
TEST(embedded_lookup_not_found) {
/* With stub, everything should return NULL */
const cbm_embedded_file_t *f = cbm_embedded_lookup("/nonexistent");
ASSERT_NULL(f);
PASS();
}
TEST(embedded_stub_count) {
/* Stub should have 0 files */
ASSERT_EQ(CBM_EMBEDDED_FILE_COUNT, 0);
PASS();
}
/* ── Layout tests ─────────────────────────────────────────────── */
TEST(layout_empty_graph) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
/* No nodes in store → empty result */
cbm_layout_result_t *r =
cbm_layout_compute(store, "test-project", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->node_count, 0);
ASSERT_EQ(r->edge_count, 0);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
}
TEST(layout_single_node) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "test", "/tmp/test");
cbm_node_t node = {
.project = "test",
.label = "Function",
.name = "main",
.qualified_name = "test::main",
.file_path = "main.c",
.start_line = 1,
.end_line = 10,
};
int64_t id = cbm_store_upsert_node(store, &node);
ASSERT_GT(id, 0);
cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->node_count, 1);
ASSERT_STR_EQ(r->nodes[0].name, "main");
ASSERT_EQ(r->total_nodes, 1);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
}
TEST(layout_two_connected) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "test", "/tmp/test");
cbm_node_t n1 = {.project = "test",
.label = "Function",
.name = "foo",
.qualified_name = "test::foo",
.file_path = "a.c",
.start_line = 1,
.end_line = 5};
cbm_node_t n2 = {.project = "test",
.label = "Function",
.name = "bar",
.qualified_name = "test::bar",
.file_path = "b.c",
.start_line = 1,
.end_line = 5};
int64_t id1 = cbm_store_upsert_node(store, &n1);
int64_t id2 = cbm_store_upsert_node(store, &n2);
cbm_edge_t edge = {.project = "test", .source_id = id1, .target_id = id2, .type = "CALLS"};
cbm_store_insert_edge(store, &edge);
cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->node_count, 2);
/* Nodes should be positioned apart (not at same point) */
float dx = r->nodes[0].x - r->nodes[1].x;
float dy = r->nodes[0].y - r->nodes[1].y;
float dz = r->nodes[0].z - r->nodes[1].z;
float dist = sqrtf(dx * dx + dy * dy + dz * dz);
ASSERT_GT((long long)(dist * 100), 0);
ASSERT_EQ(r->edge_count, 1);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
}
TEST(layout_respects_max_nodes) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "test", "/tmp/test");
/* Insert 20 nodes */
for (int i = 0; i < 20; i++) {
char name[32], qn[64];
snprintf(name, sizeof(name), "fn%d", i);
snprintf(qn, sizeof(qn), "test::fn%d", i);
cbm_node_t n = {.project = "test",
.label = "Function",
.name = name,
.qualified_name = qn,
.file_path = "a.c",
.start_line = i,
.end_line = i + 1};
cbm_store_upsert_node(store, &n);
}
/* max_nodes=5 should return at most 5 */
cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 5);
ASSERT_NOT_NULL(r);
ASSERT_LTE(r->node_count, 5);
ASSERT_EQ(r->total_nodes, 20);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
}
TEST(layout_deterministic) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "test", "/tmp/test");
cbm_node_t n1 = {.project = "test",
.label = "Function",
.name = "alpha",
.qualified_name = "test::alpha",
.file_path = "a.c",
.start_line = 1,
.end_line = 5};
cbm_node_t n2 = {.project = "test",
.label = "Function",
.name = "beta",
.qualified_name = "test::beta",
.file_path = "b.c",
.start_line = 1,
.end_line = 5};
cbm_store_upsert_node(store, &n1);
cbm_store_upsert_node(store, &n2);
/* Run twice, check positions match */
cbm_layout_result_t *r1 = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
cbm_layout_result_t *r2 = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NOT_NULL(r1);
ASSERT_NOT_NULL(r2);
ASSERT_EQ(r1->node_count, r2->node_count);
for (int i = 0; i < r1->node_count; i++) {
ASSERT_FLOAT_EQ(r1->nodes[i].x, r2->nodes[i].x, 0.001);
ASSERT_FLOAT_EQ(r1->nodes[i].y, r2->nodes[i].y, 0.001);
ASSERT_FLOAT_EQ(r1->nodes[i].z, r2->nodes[i].z, 0.001);
}
cbm_layout_free(r1);
cbm_layout_free(r2);
cbm_store_close(store);
PASS();
}
TEST(layout_to_json) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "test", "/tmp/test");
cbm_node_t n = {.project = "test",
.label = "Function",
.name = "hello",
.qualified_name = "test::hello",
.file_path = "a.c",
.start_line = 1,
.end_line = 5};
cbm_store_upsert_node(store, &n);
cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NOT_NULL(r);
char *json = cbm_layout_to_json(r);
ASSERT_NOT_NULL(json);
/* Should contain key fields */
ASSERT(strstr(json, "\"nodes\"") != NULL);
ASSERT(strstr(json, "\"edges\"") != NULL);
ASSERT(strstr(json, "\"total_nodes\"") != NULL);
ASSERT(strstr(json, "\"hello\"") != NULL);
ASSERT(strstr(json, "\"Function\"") != NULL);
free(json);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
}
TEST(layout_null_inputs) {
/* NULL store → NULL result */
cbm_layout_result_t *r = cbm_layout_compute(NULL, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NULL(r);
/* NULL project → NULL result */
cbm_store_t *store = cbm_store_open_memory();
r = cbm_layout_compute(store, NULL, CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NULL(r);
/* cbm_layout_free(NULL) should not crash */
cbm_layout_free(NULL);
/* cbm_layout_to_json(NULL) should return NULL */
char *json = cbm_layout_to_json(NULL);
ASSERT_NULL(json);
cbm_store_close(store);
PASS();
}
/* ── Suite ────────────────────────────────────────────────────── */
SUITE(ui) {
/* Config */
RUN_TEST(config_load_defaults);
RUN_TEST(config_save_and_reload);
RUN_TEST(config_overwrite);
RUN_TEST(config_corrupt_file);
RUN_TEST(config_missing_fields);
/* Embedded assets (stub) */
RUN_TEST(embedded_lookup_not_found);
RUN_TEST(embedded_stub_count);
/* Layout engine */
RUN_TEST(layout_empty_graph);
RUN_TEST(layout_single_node);
RUN_TEST(layout_two_connected);
RUN_TEST(layout_respects_max_nodes);
RUN_TEST(layout_deterministic);
RUN_TEST(layout_to_json);
RUN_TEST(layout_null_inputs);
}