/* * repro_issue521.c — Reproduce-first case for issue #521. * * BUG: "Route nodes created from URL strings in config / non-source files" * * Root cause (pipeline.c:try_upsert_infra_route + helpers.c:is_url_like): * * 1. extract_unified.c:handle_string_refs() walks every string node in a * YAML file. Any value containing "://" passes cbm_classify_string() * as CBM_STRREF_URL, landing in CBMFileResult.string_refs. * * 2. pipeline.c:cbm_pipeline_extract_infra_routes() iterates files that * match is_infra_file() — which includes ".yaml" / ".yml" — and calls * try_upsert_infra_route() for every CBM_STRREF_URL entry whose value * contains "://". * * 3. try_upsert_infra_route() unconditionally mints a "Route" node: * cbm_gbuf_upsert_node(gbuf, "Route", sr->value, route_qn, ...) * with no check for whether the URL is an upstream-config value (e.g. * an auth-server JWKS URL, a Terraform registry URL, a healthcheck * target) versus an actual route this service exposes. * * Correct behaviour: a YAML/config file that only contains upstream URL * strings (no route-registration syntax, no handler definitions) MUST NOT * yield any Route node in the graph. * * Why RED on current code: try_upsert_infra_route has no guard that * prevents minting Route nodes from arbitrary CBM_STRREF_URL values in * config files. Indexing the fixture below produces ≥ 2 Route nodes * (one per upstream URL string), so ASSERT_EQ(route_count, 0) FAILS. */ #include #include "test_framework.h" #include "test_helpers.h" #include "cbm.h" #include #include #include #include #include #include #include #include #include /* ── Minimal pipeline harness (mirrors test_grammar_probe_b.c) ───────────── */ typedef struct { char tmpdir[256]; char dbpath[512]; char *project; cbm_mcp_server_t *srv; } R521Proj; static void r521_fwd_slashes(char *p) { for (; *p; p++) { if (*p == '\\') *p = '/'; } } typedef struct { const char *name; const char *content; } R521File; static cbm_store_t *r521_index_files(R521Proj *lp, const R521File *files, int nfiles) { memset(lp, 0, sizeof(*lp)); snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_r521_XXXXXX"); if (!cbm_mkdtemp(lp->tmpdir)) return NULL; r521_fwd_slashes(lp->tmpdir); for (int i = 0; i < nfiles; i++) { char path[700]; snprintf(path, sizeof(path), "%s/%s", lp->tmpdir, files[i].name); /* create any intermediate directories */ char *slash = strrchr(path, '/'); if (slash && slash > path + (int)strlen(lp->tmpdir)) { *slash = '\0'; cbm_mkdir_p(path, 0755); *slash = '/'; } FILE *f = fopen(path, "wb"); if (!f) return NULL; fputs(files[i].content, f); fclose(f); } lp->project = cbm_project_name_from_path(lp->tmpdir); if (!lp->project) return NULL; const char *home = getenv("HOME"); if (!home) home = "/tmp"; char cache_dir[512]; snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home); cbm_mkdir(cache_dir); snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project); unlink(lp->dbpath); lp->srv = cbm_mcp_server_new(NULL); if (!lp->srv) return NULL; char args[700]; snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir); char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args); if (resp) free(resp); return cbm_store_open_path(lp->dbpath); } static void r521_cleanup(R521Proj *lp, cbm_store_t *store) { if (store) cbm_store_close(store); if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } free(lp->project); lp->project = NULL; th_rmtree(lp->tmpdir); unlink(lp->dbpath); char wal[600], shm[600]; snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath); snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath); unlink(wal); unlink(shm); } /* Count Route nodes in the indexed project. Returns -1 on error. */ static int r521_count_routes(cbm_store_t *store, const char *project) { cbm_node_t *nodes = NULL; int count = 0; if (cbm_store_find_nodes_by_label(store, project, "Route", &nodes, &count) != CBM_STORE_OK) return -1; cbm_store_free_nodes(nodes, count); return count; } /* ── Reproduction test ───────────────────────────────────────────────────── */ /* * Fixture: a three-file repo containing ONLY config files. * * config.yaml — application config; values are upstream/external URLs * (auth server, downstream service). No handler code. * dependabot.yml — Dependabot config; "registries" block holds a Terraform * registry URL. Purely a CI config — no route handlers. * compose.yaml — Docker Compose; "healthcheck" contains a curl command * with a localhost URL. No route-serving code. * * All three files match is_infra_file() (.yaml / .yml). Their URL strings * pass cbm_classify_string() as CBM_STRREF_URL. On buggy code, * try_upsert_infra_route() mints a Route node for each URL string that * contains "://", so the graph gets ≥ 2 spurious Route nodes. * * Correct behaviour: 0 Route nodes (no route handler exists anywhere). * Actual (buggy): ≥ 2 Route nodes — assertion below is RED. */ TEST(repro_issue521_no_route_from_config_url) { static const R521File files[] = { { "config.yaml", "auth:\n" " jwks_url: \"https://auth.example.com/.well-known/jwks.json\"\n" "upstream:\n" " order_service_url: \"http://order-service:8080/v2/orders/{id}\"\n" }, { "dependabot.yml", "version: 2\n" "registries:\n" " terraform-registry:\n" " type: terraform-registry\n" " url: https://app.terraform.io\n" "updates:\n" " - package-ecosystem: terraform\n" " directory: \"/\"\n" " schedule:\n" " interval: weekly\n" }, { "compose.yaml", "services:\n" " app:\n" " image: myapp:latest\n" " healthcheck:\n" " test: [\"CMD-SHELL\", \"curl --fail http://localhost:9000/ || exit 1\"]\n" " interval: 30s\n" }, }; R521Proj lp; cbm_store_t *store = r521_index_files(&lp, files, 3); ASSERT_NOT_NULL(store); int route_count = r521_count_routes(store, lp.project); /* * CORRECT behaviour: no Route node must exist. * Upstream/config/healthcheck URLs are not routes this service serves. * * WHY RED on current code: * pipeline.c:try_upsert_infra_route() calls cbm_gbuf_upsert_node(…,"Route",…) * for every CBM_STRREF_URL string_ref extracted from files matching * is_infra_file() — which includes all three YAML files above. * The function has no guard to reject upstream/config URL values, so * it mints Route nodes for "https://auth.example.com/…", "https://app.terraform.io", * "http://order-service:8080/…", and "http://localhost:9000/" — at * least 2 spurious Route nodes, so route_count > 0, and this ASSERT_EQ * FAILS (RED). */ ASSERT_EQ(route_count, 0); r521_cleanup(&lp, store); PASS(); } /* ── Suite ───────────────────────────────────────────────────────────────── */ SUITE(repro_issue521) { RUN_TEST(repro_issue521_no_route_from_config_url); }