From 7a43b38ef7eef0d9d5091404aa29f9a78c7b08ca Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 5 Jun 2026 20:33:11 +0200 Subject: [PATCH] Add per-grammar regression suite across all 159 grammars Index a fixture for every grammar through the full pipeline and assert graph-level invariants: golden node-label histograms, def/node breadth, CALLS and IMPORTS breadth, presence of all 26 pipeline edge types (structural, type-relationship, service/dataflow, and similarity), no-crash, and call resolution. Reproduced quality gaps (under-extraction, import-extraction, and CALLS-resolution) are kept as hard failures for the fix phase rather than skipped. Adds a real-repo scale tier (scale_contract.sh) and ignores local-only private/ scratch. --- .gitignore | 3 + Makefile.cbm | 4 +- tests/grammar_cases.h | 31 + tests/scale_contract.sh | 132 ++++ tests/test_grammar_imports.c | 152 ++++ tests/test_grammar_labels.c | 353 +++++++++ tests/test_grammar_regression.c | 18 +- tests/test_lang_contract.c | 1248 +++++++++++++++++++++++++++++++ tests/test_main.c | 9 + 9 files changed, 1937 insertions(+), 13 deletions(-) create mode 100644 tests/grammar_cases.h create mode 100644 tests/scale_contract.sh create mode 100644 tests/test_grammar_imports.c create mode 100644 tests/test_grammar_labels.c create mode 100644 tests/test_lang_contract.c diff --git a/.gitignore b/.gitignore index 7a7666e0f..f7093ee9f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,9 @@ Thumbs.db memory/ reference/ +# Local-only scratch / session notes (never pushed) +private/ + # Build artifacts build/ node_modules/ diff --git a/Makefile.cbm b/Makefile.cbm index 54458460c..763895e7a 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -276,6 +276,8 @@ TEST_FOUNDATION_SRCS = \ TEST_EXTRACTION_SRCS = \ tests/test_extraction.c \ tests/test_grammar_regression.c \ + tests/test_grammar_labels.c \ + tests/test_grammar_imports.c \ tests/test_ac.c TEST_STORE_SRCS = \ @@ -338,7 +340,7 @@ TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c -TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c +TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c TEST_TRACES_SRCS = tests/test_traces.c diff --git a/tests/grammar_cases.h b/tests/grammar_cases.h new file mode 100644 index 000000000..7e280e8b6 --- /dev/null +++ b/tests/grammar_cases.h @@ -0,0 +1,31 @@ +/* + * grammar_cases.h — Shared per-language fixture table. + * + * One minimal source fixture per supported grammar. Shared by: + * - test_grammar_regression.c : EXTRACTION-level breadth (cbm_extract_file with + * an explicit language → defs/labels). Bypasses discover, so it covers every + * grammar including those whose file extension collides with another's. + * - test_lang_contract.c : GRAPH-level breadth (full pipeline index → the + * fixture's defs must reach the graph as nodes). + * + * Keep this the single source of truth for per-grammar fixtures. + */ +#ifndef CBM_TEST_GRAMMAR_CASES_H +#define CBM_TEST_GRAMMAR_CASES_H + +#include "cbm.h" +#include + +typedef struct { + const char *name; /* short grammar label (unique) */ + CBMLanguage lang; /* explicit language for extraction-level tests */ + const char *path; /* representative filename (drives discover by ext/name) */ + const char *src; /* fixture source */ + int min_defs; /* catastrophic-break floor (0 for data/config/markup) */ + const char *expect[4]; /* expected def names (NULL-terminated), optional */ +} GrammarCase; + +extern const GrammarCase CBM_GRAMMAR_CASES[]; +extern const size_t CBM_GRAMMAR_CASES_COUNT; + +#endif /* CBM_TEST_GRAMMAR_CASES_H */ diff --git a/tests/scale_contract.sh b/tests/scale_contract.sh new file mode 100644 index 000000000..5798f91d5 --- /dev/null +++ b/tests/scale_contract.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# scale_contract.sh — Real-repo SCALE-TIER contracts for known-bug languages. +# +# Some behavioral bugs only manifest at real-repo scale and cannot be reproduced +# by the small in-process fixtures in tests/test_lang_contract.c: +# - Java / TypeScript : SIGBUS during parallel extraction on large repos. +# - C : function-call attribution (calls land on the file's +# Module node instead of the enclosing Function). +# - Kotlin : 0 IMPORTS edges (package -> module resolution). +# +# This tier runs the COMPILED binary on the cbm-bench-validate repos and asserts +# invariants at scale. It is SLOW and needs the local bench repos, so it is +# OPT-IN — not part of the fast `scripts/test.sh` unit run. +# +# Usage: +# bash tests/scale_contract.sh [lang ...] # default: kotlin java ts +# CBM_SCALE_INCLUDE_C=1 bash tests/scale_contract.sh c # add the slow C tier +# CBM_BENCH_DIR=/path bash tests/scale_contract.sh # override repo root +# Exit 0 if all selected contracts pass, non-zero otherwise. + +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN="$ROOT/build/c/codebase-memory-mcp" +BENCH="${CBM_BENCH_DIR:-$HOME/cbm-bench-validate}" +PY="${PYTHON:-python3.9}" +FAILURES=0 +RAN=0 + +# ── repo map (lang -> bench subdir) ────────────────────────────── +repo_for() { + case "$1" in + kotlin) echo "$BENCH/ktorio_ktor" ;; + java) echo "$BENCH/spring-projects_spring-boot" ;; + ts) echo "$BENCH/microsoft_TypeScript" ;; + c) echo "$BENCH/linux_mono/copy1" ;; + *) echo "" ;; + esac +} + +# project name == cbm_project_name_from_path(): strip leading '/', map every +# char outside [A-Za-z0-9._-] to '-'. +project_name() { printf '%s' "$1" | sed 's#^/##; s#[^A-Za-z0-9._-]#-#g'; } + +# Run query_graph and print the first integer in the JSON response (or -1). +query_count() { + local proj="$1" cypher="$2" resp + resp="$("$BIN" cli query_graph "{\"project\":\"$proj\",\"query\":\"$cypher\"}" 2>/dev/null)" + printf '%s' "$resp" | "$PY" -c 'import sys,re +s=sys.stdin.read() +m=re.findall(r"-?\d+", s) +print(m[0] if m else -1)' 2>/dev/null || echo -1 +} + +assert_lang() { + local lang="$1" repo + repo="$(repo_for "$lang")" + if [ -z "$repo" ] || [ ! -d "$repo" ]; then + echo "[scale] SKIP $lang — repo not found ($repo)" + return 0 + fi + RAN=$((RAN + 1)) + local proj + proj="$(project_name "$repo")" + rm -f "$HOME/.cache/codebase-memory-mcp/$proj.db"* + + echo "[scale] $lang — indexing $repo ..." + "$BIN" cli index_repository "{\"repo_path\":\"$repo\"}" >/dev/null 2>&1 + local rc=$? + + # Contract 1 (all langs): indexing must not crash by signal (rc >= 128). + if [ "$rc" -ge 128 ]; then + echo "[scale] FAIL $lang — index crashed by signal (exit=$rc, signal=$((rc - 128)))" + FAILURES=$((FAILURES + 1)) + return 0 + fi + echo "[scale] ok $lang — index did not crash (exit=$rc)" + + # Contract 2 (per-lang invariant). + case "$lang" in + kotlin) + local imports + imports="$(query_count "$proj" "MATCH ()-[r:IMPORTS]->() RETURN count(r) AS n")" + if [ "$imports" -ge 1 ] 2>/dev/null; then + echo "[scale] PASS kotlin — IMPORTS edges present (n>=$imports)" + else + echo "[scale] FAIL kotlin — 0 IMPORTS edges (got '$imports')" + FAILURES=$((FAILURES + 1)) + fi + ;; + c) + # Function nodes that have >=1 outbound CALLS (attribution invariant). + local fcalls + fcalls="$(query_count "$proj" "MATCH (a:Function)-[:CALLS]->() RETURN count(DISTINCT a) AS n")" + if [ "$fcalls" -ge 1 ] 2>/dev/null; then + echo "[scale] PASS c — Function-sourced CALLS present (functions-with-calls>=$fcalls)" + else + echo "[scale] FAIL c — no Function node has outbound CALLS (got '$fcalls')" + FAILURES=$((FAILURES + 1)) + fi + ;; + java | ts) + echo "[scale] PASS $lang — no-crash contract satisfied" + ;; + esac +} + +# ── build the prod binary if missing ───────────────────────────── +if [ ! -x "$BIN" ]; then + echo "[scale] building prod binary ..." + (cd "$ROOT" && ./scripts/build.sh) || { echo "[scale] FAIL — build failed"; exit 1; } +fi +[ -x "$BIN" ] || { echo "[scale] FAIL — binary not found at $BIN"; exit 1; } + +# ── select languages ───────────────────────────────────────────── +LANGS=("$@") +if [ "${#LANGS[@]}" -eq 0 ]; then + LANGS=(kotlin java ts) + [ "${CBM_SCALE_INCLUDE_C:-0}" = "1" ] && LANGS+=(c) +fi + +for lang in "${LANGS[@]}"; do + assert_lang "$lang" +done + +echo "────────────────────────────────────────────" +if [ "$FAILURES" -eq 0 ]; then + echo "[scale] All scale contracts passed ($RAN ran)" + exit 0 +fi +echo "[scale] $FAILURES scale contract(s) FAILED ($RAN ran)" +exit 1 diff --git a/tests/test_grammar_imports.c b/tests/test_grammar_imports.c new file mode 100644 index 000000000..df2b840fd --- /dev/null +++ b/tests/test_grammar_imports.c @@ -0,0 +1,152 @@ +/* + * test_grammar_imports.c — Per-grammar IMPORTS EXTRACTION breadth. + * + * For every grammar cbm_extract_imports() handles, assert cbm_extract_file() + * captures its import/include/use/require statements (imports.count >= N). + * This is the EXTRACTION-level import contract: it uses an explicit language + * (no discover, no pkgmap), so it is reliable in-unit for ALL import-capable + * grammars — the graph IMPORTS-edge (which needs module resolution) is covered + * for relative imports by test_lang_contract.c and at real scale by the scale + * tier. No skips: a grammar that fails to extract its imports is a hard FAILURE + * reproducing the bug (gap_note documents the already-root-caused ones). + */ +#include "test_framework.h" +#include "cbm.h" + +#include + +typedef struct { + const char *name; + CBMLanguage lang; + const char *filename; + const char *content; + int expected; /* imports.count must be >= this */ + const char *gap_note; +} ImportCase; + +static const ImportCase IMPORT_CASES[] = { + {"astro", CBM_LANG_ASTRO, "fixture.astro", + "\n
hi
\n", + 2, NULL}, + {"bash", CBM_LANG_BASH, "a.sh", "#!/usr/bin/env bash\nsource ./lib/util.sh\n. ./lib/other.sh\n", 2, + NULL}, + {"c", CBM_LANG_C, "imports_fixture.c", + "#include \n#include \"mymodule.h\"\n\nint main(void) {\n return 0;\n}\n", 2, NULL}, + {"cpp", CBM_LANG_CPP, "main.cpp", + "#include \n#include \"widget.h\"\n\nint main() {\n std::cout << \"hi\\n\";\n return 0;\n}\n", + 2, NULL}, + {"csharp", CBM_LANG_CSHARP, "Imports.cs", + "using System;\nusing static System.Math;\nusing Json = System.Text.Json;\n\nnamespace Demo;\n\nclass Program\n{\n static void Main() => Console.WriteLine(Sqrt(4.0));\n}\n", + 3, NULL}, + {"css", CBM_LANG_CSS, "styles.css", + "@import \"reset.css\";\n@import url(\"theme.css\");\n\nbody {\n margin: 0;\n}\n", 2, NULL}, + {"dart", CBM_LANG_DART, "imports.dart", + "import 'dart:async';\nimport 'package:meta/meta.dart';\n\nvoid main() {}\n", 2, + "extract_imports.c SWIFT/DART case matches 'import_declaration' but tree-sitter-dart emits 'import_or_export' -> 0 imports"}, + {"elixir", CBM_LANG_ELIXIR, "imports_fixture.ex", + "import Enum\nalias MyApp.Accounts.User\nrequire Logger\n", 3, NULL}, + {"erlang", CBM_LANG_ERLANG, "greeter.erl", + "-module(greeter).\n-import(lists, [foldl/3]).\n-include_lib(\"kernel/include/file.hrl\").\n\nsum(Ns) -> foldl(fun(X, A) -> X + A end, 0, Ns).\n", + 1, NULL}, + {"form", CBM_LANG_FORM, "imports.frm", "#include \"procedures.prc\"\n#include \"tables.h\"\n", 2, NULL}, + {"go", CBM_LANG_GO, "imports_fixture.go", + "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\tmrand \"math/rand\"\n)\n\nfunc main() {\n\tfmt.Fprintln(os.Stdout, mrand.Int())\n}\n", + 3, NULL}, + {"groovy", CBM_LANG_GROOVY, "a.groovy", + "import java.util.List\nimport groovy.transform.CompileStatic\n\nclass Greeter {\n String greet() { \"hi\" }\n}\n", + 2, NULL}, + {"haskell", CBM_LANG_HASKELL, "a.hs", + "module Main where\n\nimport Data.List (sort)\nimport qualified Data.Map as Map\n\nmain :: IO ()\nmain = print (sort [3, 1, 2])\n", + 2, + "imports nested under an 'imports' container node; parse_generic_imports scans only root children -> 0 (needs parse_haskell_imports, like Kotlin)"}, + {"html", CBM_LANG_HTML, "fixture_imports.html", + "\n\n \n \n Import Fixture\n \n \n \n\n", + 2, NULL}, + {"java", CBM_LANG_JAVA, "Fixture.java", + "import java.util.List;\nimport java.util.Map;\n\nclass Fixture {\n List names;\n Map counts;\n}\n", + 2, NULL}, + {"javascript", CBM_LANG_JAVASCRIPT, "imports_fixture.js", + "import path from \"node:path\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nexport async function copyConfig(src) {\n const dst = path.join(\"/tmp\", \"config.json\");\n const data = await readFile(src, \"utf8\");\n await writeFile(dst, data);\n return dst;\n}\n", + 3, NULL}, + {"kotlin", CBM_LANG_KOTLIN, "fixture.kt", + "package com.example.app\n\nimport kotlin.collections.List\nimport java.util.UUID\nimport kotlinx.coroutines.runBlocking\n", + 3, NULL}, + {"lean", CBM_LANG_LEAN, "imports.lean", + "import Std.Data.List.Basic\nimport Mathlib.Tactic\n\ndef hello : String := \"world\"\n", 2, NULL}, + {"lua", CBM_LANG_LUA, "a.lua", + "local socket = require(\"socket\")\nlocal json = require(\"dkjson\")\n\nlocal function connect()\n return socket, json\nend\n\nreturn connect\n", + 2, NULL}, + {"magma", CBM_LANG_MAGMA, "a.magma", + "load \"mylib.m\";\nload \"helpers.m\";\n\nfunction foo()\n return 1;\nend function;\n", 2, NULL}, + {"objc", CBM_LANG_OBJC, "fixture_imports.m", + "#import \n#import \"AppModel.h\"\n", 2, NULL}, + {"ocaml", CBM_LANG_OCAML, "a.ml", + "open List\nopen String\n\nlet greet name = String.cat \"Hi \" name\n", 2, NULL}, + {"perl", CBM_LANG_PERL, "imports.pl", + "use strict;\nuse warnings;\nuse Data::Dumper;\n\nsub run { print Dumper([1, 2, 3]); }\n", 3, NULL}, + {"php", CBM_LANG_PHP, "a.php", + " List[int]:\n return [len(s) + (pad or 0) for s in items if os.path.exists(s)]\n", + 3, NULL}, + {"r", CBM_LANG_R, "imports_fixture.R", "library(jsonlite)\nrequire(stats)\nsource(\"utils.R\")\n", 3, + NULL}, + {"ruby", CBM_LANG_RUBY, "a.rb", "require \"json\"\nrequire_relative \"helper\"\n", 2, NULL}, + {"rust", CBM_LANG_RUST, "imports.rs", + "use std::collections::HashMap;\nuse std::fmt::{self, Display};\nuse serde::Serialize;\n\nfn main() {\n let _m: HashMap = HashMap::new();\n}\n", + 3, NULL}, + {"scala", CBM_LANG_SCALA, "a.scala", + "package example\n\nimport scala.collection.mutable.ListBuffer\nimport java.time.Instant\n\nobject Main {\n def run(): Unit = {\n val buf = ListBuffer[Instant]()\n buf += Instant.now()\n }\n}\n", + 2, NULL}, + {"scss", CBM_LANG_SCSS, "imports.scss", + "@import \"variables\";\n@import \"mixins\";\n\n.button {\n color: red;\n}\n", 2, NULL}, + {"svelte", CBM_LANG_SVELTE, "a.svelte", + "\n\n\n", + 2, NULL}, + {"swift", CBM_LANG_SWIFT, "a.swift", "import Foundation\nimport SwiftUI\n", 2, NULL}, + {"tsx", CBM_LANG_TSX, "a.tsx", + "import React from \"react\";\nimport { useState, useEffect } from \"react\";\nimport * as styles from \"./Button.module.css\";\n\nexport function Button(): React.ReactElement {\n const [count, setCount] = useState(0);\n useEffect(() => {}, []);\n return ;\n}\n", + 4, NULL}, + {"typescript", CBM_LANG_TYPESCRIPT, "imports_fixture.ts", + "import express from \"express\";\nimport * as path from \"path\";\nimport { useState, useEffect } from \"react\";\n\nconst app = express();\nconst dir: string = path.join(\".\");\nconsole.log(app, dir, useState, useEffect);\n", + 4, NULL}, + {"vue", CBM_LANG_VUE, "component.vue", + "\n\n\n", + 2, NULL}, + {"wolfram", CBM_LANG_WOLFRAM, "main.wl", + "Needs[\"MyPackage`Utils`\"]\n<< \"helpers.wl\"\n\nmain[x_] := process[x] + 1\n", 2, NULL}, + {"zig", CBM_LANG_ZIG, "imports.zig", + "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\npub fn main() void {}\n", 2, + "@import is nested in variable_declaration; parse_generic_imports scans only root children for builtin_function -> 0"}, +}; + +TEST(grammar_imports_extracted) { + int n = (int)(sizeof(IMPORT_CASES) / sizeof(IMPORT_CASES[0])); + int failures = 0; + for (int i = 0; i < n; i++) { + const ImportCase *c = &IMPORT_CASES[i]; + CBMFileResult *r = cbm_extract_file(c->content, (int)strlen(c->content), c->lang, "imp", + c->filename, 0, NULL, NULL); + if (!r) { + fprintf(stderr, " [IMPORTS] FAIL %-12s extract returned NULL\n", c->name); + failures++; + continue; + } + int got = r->imports.count; + cbm_free_result(r); + if (got < c->expected) { + fprintf(stderr, " [IMPORTS] FAIL %-12s imports=%d expected>=%d%s%s\n", c->name, got, + c->expected, c->gap_note ? " — " : "", c->gap_note ? c->gap_note : ""); + failures++; + } + } + fprintf(stderr, " [IMPORTS] %d import-capable grammars: %d FAILURES (each = a grammar whose " + "imports are not extracted)\n", + n, failures); + ASSERT_EQ(failures, 0); + PASS(); +} + +void suite_grammar_imports(void) { + RUN_TEST(grammar_imports_extracted); +} diff --git a/tests/test_grammar_labels.c b/tests/test_grammar_labels.c new file mode 100644 index 000000000..952d03a1f --- /dev/null +++ b/tests/test_grammar_labels.c @@ -0,0 +1,353 @@ +/* + * test_grammar_labels.c — Per-grammar node-type LABEL golden snapshot. + * + * For every supported grammar, snapshot the deterministic node-type-label + * histogram that cbm_extract_file() produces for the shared fixture (e.g. + * "Class:1,Function:2,Module:1"). Asserting the histogram stays fixed catches + * ANY future change to how a grammar's constructs are labeled — e.g. a class + * silently downgraded to a Function, a lost Method, or a new spurious node. + * + * Node counts are deterministic (unlike edge counts), so an exact histogram + * golden is stable. Labels come from CBMDefinition.label assigned during + * extraction (the same labels that become graph-node labels downstream). + * + * Capture workflow: a grammar with no golden row prints a `[LABEL-CAPTURE]` + * line and fails; copy those lines into LABEL_GOLDENS, then the test asserts. + */ +#include "test_framework.h" +#include "cbm.h" +#include "grammar_cases.h" + +#include +#include + +/* Build a sorted "Label:count,Label:count" histogram of a result's def labels + * (includes the always-emitted Module node). Deterministic + stable. */ +static void label_histogram(CBMFileResult *r, char *out, size_t out_sz) { + /* Collect distinct labels + counts (small N — linear scan is fine). */ + enum { MAXL = 32 }; + const char *labels[MAXL]; + int counts[MAXL]; + int nl = 0; + for (int i = 0; i < r->defs.count; i++) { + const char *l = r->defs.items[i].label ? r->defs.items[i].label : "(null)"; + int j = 0; + for (; j < nl; j++) { + if (strcmp(labels[j], l) == 0) { + counts[j]++; + break; + } + } + if (j == nl && nl < MAXL) { + labels[nl] = l; + counts[nl] = 1; + nl++; + } + } + /* Insertion sort labels alphabetically for a canonical string. */ + for (int i = 1; i < nl; i++) { + const char *lk = labels[i]; + int ck = counts[i]; + int j = i - 1; + while (j >= 0 && strcmp(labels[j], lk) > 0) { + labels[j + 1] = labels[j]; + counts[j + 1] = counts[j]; + j--; + } + labels[j + 1] = lk; + counts[j + 1] = ck; + } + out[0] = '\0'; + size_t used = 0; + for (int i = 0; i < nl; i++) { + int w = snprintf(out + used, out_sz - used, "%s%s:%d", i ? "," : "", labels[i], counts[i]); + if (w < 0 || (size_t)w >= out_sz - used) { + break; + } + used += (size_t)w; + } +} + +typedef struct { + const char *name; + const char *hist; +} LabelGolden; + +/* Golden node-type-label histograms — captured from the current (correct) + * extractor output. A mismatch = a labeling regression for that grammar. + * (Populated from [LABEL-CAPTURE] output; see header.) */ +static const LabelGolden LABEL_GOLDENS[] = { + {"go", "Function:2,Module:1"}, + {"c", "Function:2,Module:1"}, + {"cpp", "Class:1,Function:1,Module:1"}, + {"cuda", "Function:2,Module:1"}, + {"python", "Class:1,Function:1,Module:1"}, + {"javascript", "Class:1,Function:1,Module:1"}, + {"typescript", "Class:1,Function:1,Module:1"}, + {"tsx", "Function:1,Module:1"}, + {"java", "Class:1,Method:1,Module:1"}, + {"kotlin", "Class:1,Function:1,Module:1"}, + {"rust", "Class:1,Function:1,Module:1"}, + {"ruby", "Class:1,Function:1,Module:1"}, + {"php", "Class:1,Function:1,Module:1"}, + {"c_sharp", "Class:1,Method:1,Module:1"}, + {"bash", "Function:2,Module:1"}, + {"zsh", "Function:2,Module:1"}, + {"lua", "Function:2,Module:1"}, + {"luau", "Function:2,Module:1"}, + {"perl", "Function:2,Module:1"}, + {"dart", "Class:1,Function:1,Module:1"}, + {"swift", "Class:1,Function:1,Module:1"}, + {"scala", "Class:1,Function:1,Method:1,Module:1"}, + {"gdscript", "Function:1,Module:1"}, + {"groovy", "Class:1,Method:1,Module:1"}, + {"zig", "Function:2,Module:1"}, + {"nim", "Function:2,Module:1"}, + {"solidity", "Class:1,Function:1,Method:1,Module:1"}, + {"tcl", "Function:2,Module:1"}, + {"powershell", "Function:2,Module:1"}, + {"r", "Function:2,Module:1"}, + {"julia", "Function:1,Module:1"}, + {"matlab", "Function:2,Module:1"}, + {"ada", "Function:1,Module:1"}, + {"agda", "Module:1"}, + {"apex", "Class:1,Method:1,Module:1"}, + {"awk", "Function:2,Module:1"}, + {"cairo", "Function:2,Module:1"}, + {"clojure", "Function:2,Module:1"}, + {"commonlisp", "Function:2,Module:1"}, + {"emacslisp", "Function:2,Module:1"}, + {"crystal", "Class:1,Function:1,Module:1"}, + {"d", "Function:2,Module:1"}, + {"elixir", "Class:1,Function:1,Module:1"}, + {"erlang", "Function:2,Module:1"}, + {"fennel", "Function:2,Module:1"}, + {"fish", "Function:1,Module:1"}, + {"fortran", "Function:1,Module:1"}, + {"fsharp", "Function:2,Module:1"}, + {"gleam", "Function:2,Module:1"}, + {"glsl", "Function:2,Module:1"}, + {"hare", "Function:2,Module:1"}, + {"haskell", "Function:1,Module:1"}, + {"hlsl", "Function:2,Module:1"}, + {"ispc", "Function:2,Module:1"}, + {"objc", "Class:1,Method:1,Module:1"}, + {"ocaml", "Function:2,Module:1"}, + {"odin", "Function:2,Module:1"}, + {"pascal", "Function:1,Module:1"}, + {"pony", "Module:1"}, + {"purescript", "Function:1,Module:1"}, + {"racket", "Function:2,Module:1"}, + {"rescript", "Function:2,Module:1"}, + {"scheme", "Function:2,Module:1"}, + {"slang", "Function:2,Module:1"}, + {"squirrel", "Function:2,Module:1"}, + {"starlark", "Function:2,Module:1"}, + {"sway", "Function:2,Module:1"}, + {"teal", "Function:2,Module:1"}, + {"vimscript", "Function:1,Module:1"}, + {"elm", "Class:1,Function:1,Module:1"}, + {"func", "Function:1,Module:1"}, + {"lean", "Function:2,Module:1"}, + {"move", "Module:1"}, + {"smali", "Module:1"}, + {"systemverilog", "Module:1"}, + {"verilog", "Module:1"}, + {"vhdl", "Module:1"}, + {"wgsl", "Function:2,Module:1"}, + {"tlaplus", "Function:1,Module:1"}, + {"llvm", "Function:1,Module:1"}, + {"tablegen", "Module:1"}, + {"puppet", "Module:1"}, + {"assembly", "Module:1"}, + {"nasm", "Module:1"}, + {"cfml", "Function:1,Module:1"}, + {"cfscript", "Function:1,Module:1"}, + {"cobol", "Module:1"}, + {"janet", "Module:1"}, + {"magma", "Function:1,Module:1"}, + {"qml", "Function:1,Module:1"}, + {"wolfram", "Function:1,Module:1"}, + {"pine", "Module:1"}, + {"form", "Module:1,Variable:1"}, + {"protobuf", "Module:1"}, + {"soql", "Module:1"}, + {"sosl", "Module:1"}, + {"dotenv", "Module:1"}, + {"json", "Module:1,Variable:2"}, + {"json5", "Module:1"}, + {"jsonnet", "Module:1"}, + {"jsdoc", "Module:1"}, + {"yaml", "Module:1,Variable:2"}, + {"k8s", "Module:1"}, + {"kustomize", "Module:1"}, + {"toml", "Class:1,Module:1,Variable:1"}, + {"ini", "Class:1,Module:1,Variable:1"}, + {"csv", "Module:1"}, + {"sql", "Module:1,Variable:1"}, + {"xml", "Class:2,Module:1"}, + {"html", "Module:1"}, + {"css", "Module:1"}, + {"scss", "Module:1,Variable:1"}, + {"markdown", "Module:1,Section:1"}, + {"rst", "Module:1"}, + {"dockerfile", "Module:1"}, + {"makefile", "Function:1,Module:1"}, + {"cmake", "Module:1"}, + {"meson", "Module:1"}, + {"gn", "Module:1"}, + {"just", "Module:1"}, + {"hcl", "Class:1,Module:1"}, + {"nix", "Module:1"}, + {"gomod", "Module:1"}, + {"gotemplate", "Module:1"}, + {"graphql", "Module:1"}, + {"prisma", "Module:1"}, + {"thrift", "Module:1"}, + {"capnp", "Module:1"}, + {"smithy", "Module:1"}, + {"wit", "Module:1"}, + {"kdl", "Module:1"}, + {"ron", "Module:1"}, + {"nickel", "Module:1"}, + {"pkl", "Module:1"}, + {"bicep", "Module:1"}, + {"bitbake", "Module:1"}, + {"beancount", "Module:1"}, + {"bibtex", "Module:1"}, + {"po", "Module:1"}, + {"diff", "Module:1"}, + {"regex", "Module:1"}, + {"requirements", "Module:1"}, + {"properties", "Module:1"}, + {"gitignore", "Module:1"}, + {"gitattributes", "Module:1"}, + {"sshconfig", "Module:1"}, + {"hyprlang", "Module:1"}, + {"kconfig", "Class:1,Module:1"}, + {"linkerscript", "Module:1"}, + {"devicetree", "Module:1"}, + {"jinja2", "Module:1"}, + {"liquid", "Module:1"}, + {"blade", "Module:1"}, + {"vue", "Module:1"}, + {"svelte", "Module:1"}, + {"astro", "Module:1"}, + {"templ", "Class:1,Module:1"}, + {"typst", "Module:1"}, + {"mermaid", "Module:1"}, + {NULL, NULL}, +}; + +static const char *golden_for(const char *name) { + for (int i = 0; LABEL_GOLDENS[i].name; i++) { + if (strcmp(LABEL_GOLDENS[i].name, name) == 0) { + return LABEL_GOLDENS[i].hist; + } + } + return NULL; +} + +TEST(grammar_label_goldens) { + int n = (int)CBM_GRAMMAR_CASES_COUNT; + int failures = 0; + int missing = 0; + for (int i = 0; i < n; i++) { + const GrammarCase *c = &CBM_GRAMMAR_CASES[i]; + CBMFileResult *r = + cbm_extract_file(c->src, (int)strlen(c->src), c->lang, "lbl", c->path, 0, NULL, NULL); + if (!r) { + fprintf(stderr, " [LABEL] %-14s extract returned NULL\n", c->name); + failures++; + continue; + } + char hist[256]; + label_histogram(r, hist, sizeof(hist)); + cbm_free_result(r); + + const char *golden = golden_for(c->name); + if (!golden) { + fprintf(stderr, " [LABEL-CAPTURE] {\"%s\", \"%s\"},\n", c->name, hist); + missing++; + } else if (strcmp(golden, hist) != 0) { + fprintf(stderr, " [LABEL] %-14s MISMATCH golden=[%s] actual=[%s]\n", c->name, golden, + hist); + failures++; + } + } + if (missing > 0) { + fprintf(stderr, " [LABEL] %d grammar(s) missing a golden — copy the [LABEL-CAPTURE] lines " + "into LABEL_GOLDENS\n", + missing); + } + ASSERT_EQ(failures, 0); + ASSERT_EQ(missing, 0); + PASS(); +} + +/* Count def nodes that are NOT the structural file/module wrappers. */ +static int non_module_defs(CBMFileResult *r) { + int n = 0; + for (int i = 0; i < r->defs.count; i++) { + const char *l = r->defs.items[i].label; + if (l && strcmp(l, "Module") != 0 && strcmp(l, "File") != 0 && strcmp(l, "Folder") != 0 && + strcmp(l, "Package") != 0 && strcmp(l, "Directory") != 0) { + n++; + } + } + return n; +} + +/* Code / IDL grammars whose fixture contains a real definition construct + * (function / type / message / model / module / label) that MUST extract to at + * least one non-Module node. These currently extract only a Module node — an + * under-extraction gap reproduced here as a hard failure (fix phase). Data / + * config / markup grammars (json/yaml/css/…) are intentionally excluded — a + * lone Module node is correct for them. */ +static const char *MUST_EXTRACT_DEFS[] = { + "agda", "pony", "move", "cobol", "janet", "pine", "smali", "verilog", + "vhdl", "systemverilog", "protobuf", "graphql", "thrift", "capnp", "smithy", + "wit", "prisma", "cmake", "puppet", "tablegen", "assembly", "nasm", NULL}; + +TEST(grammar_code_extracts_defs) { + int failures = 0; + for (int k = 0; MUST_EXTRACT_DEFS[k]; k++) { + const char *name = MUST_EXTRACT_DEFS[k]; + const GrammarCase *c = NULL; + for (int i = 0; i < (int)CBM_GRAMMAR_CASES_COUNT; i++) { + if (strcmp(CBM_GRAMMAR_CASES[i].name, name) == 0) { + c = &CBM_GRAMMAR_CASES[i]; + break; + } + } + if (!c) { + fprintf(stderr, " [CODE-DEFS] FAIL %-14s — no fixture in CBM_GRAMMAR_CASES\n", name); + failures++; + continue; + } + CBMFileResult *r = + cbm_extract_file(c->src, (int)strlen(c->src), c->lang, "cd", c->path, 0, NULL, NULL); + if (!r) { + fprintf(stderr, " [CODE-DEFS] FAIL %-14s extract returned NULL\n", name); + failures++; + continue; + } + int nd = non_module_defs(r); + cbm_free_result(r); + if (nd < 1) { + fprintf(stderr, " [CODE-DEFS] FAIL %-14s extracts 0 non-Module defs (under-extraction " + "gap — only a Module node)\n", + name); + failures++; + } + } + fprintf(stderr, " [CODE-DEFS] %d code/IDL grammars: %d under-extraction FAILURES\n", + (int)(sizeof(MUST_EXTRACT_DEFS) / sizeof(MUST_EXTRACT_DEFS[0])) - 1, failures); + ASSERT_EQ(failures, 0); + PASS(); +} + +void suite_grammar_labels(void) { + RUN_TEST(grammar_label_goldens); + RUN_TEST(grammar_code_extracts_defs); +} diff --git a/tests/test_grammar_regression.c b/tests/test_grammar_regression.c index d3988f98d..528d662f2 100644 --- a/tests/test_grammar_regression.c +++ b/tests/test_grammar_regression.c @@ -15,6 +15,7 @@ */ #include "test_framework.h" #include "cbm.h" +#include "grammar_cases.h" static int reg_has_def_any(CBMFileResult *r, const char *name) { for (int i = 0; i < r->defs.count; i++) { @@ -29,16 +30,7 @@ static CBMFileResult *extract(const char *src, CBMLanguage lang, const char *pro return cbm_extract_file(src, (int)strlen(src), lang, proj, path, 0, NULL, NULL); } -typedef struct { - const char *name; - CBMLanguage lang; - const char *path; - const char *src; - int min_defs; - const char *expect[4]; -} GrammarCase; - -static const GrammarCase CASES[] = { +const GrammarCase CBM_GRAMMAR_CASES[] = { /* ── LSP-backed / mainstream code languages (expect named defs) ── */ {"go", CBM_LANG_GO, "a.go", "package m\nfunc Foo() {}\nfunc Bar() int { return 0 }\n", 2, {"Foo", "Bar", NULL}}, {"c", CBM_LANG_C, "a.c", "int foo(void){return 0;}\nint bar(void){return 1;}\n", 2, {"foo", "bar", NULL}}, @@ -207,11 +199,13 @@ static const GrammarCase CASES[] = { {"mermaid", CBM_LANG_MERMAID, "a.mmd", "graph TD\n A --> B\n", 0, {NULL}}, }; +const size_t CBM_GRAMMAR_CASES_COUNT = sizeof(CBM_GRAMMAR_CASES) / sizeof(CBM_GRAMMAR_CASES[0]); + TEST(grammar_regression_all) { int failures = 0; - size_t n = sizeof(CASES) / sizeof(CASES[0]); + size_t n = CBM_GRAMMAR_CASES_COUNT; for (size_t i = 0; i < n; i++) { - const GrammarCase *c = &CASES[i]; + const GrammarCase *c = &CBM_GRAMMAR_CASES[i]; CBMFileResult *r = extract(c->src, c->lang, "reg", c->path); if (!r) { fprintf(stderr, " [REG] %-14s extract returned NULL\n", c->name); diff --git a/tests/test_lang_contract.c b/tests/test_lang_contract.c new file mode 100644 index 000000000..eac83c1a6 --- /dev/null +++ b/tests/test_lang_contract.c @@ -0,0 +1,1248 @@ +/* + * test_lang_contract.c — Per-language GRAPH-CONTRACT regression suite. + * + * Unlike the in-process unit tests (which exercise tiny inline snippets and + * passed while real behavioral bugs shipped), this suite indexes a per-language + * fixture through the FULL pipeline into a real graph DB and asserts INVARIANT + * contracts on the result: expected node/edge TYPES are present, calls are + * attributed to the calling Function (not the file/Module), resolution happens, + * and extraction does not crash. + * + * Design notes: + * - INVARIANTS, not golden snapshots: edge counts are non-deterministic + * (parallel LSH/similarity/resolve), so we assert PRESENCE + floors, never + * exact counts. This survives grammar refreshes without churn. + * - Crashes are caught via a FORKED subprocess + exit-signal check: ASan does + * NOT intercept SIGBUS, so an in-process crash would kill the test runner. + */ +#include "../src/foundation/compat.h" +#include "test_framework.h" +#include "test_helpers.h" +#include "cbm.h" +#include "grammar_cases.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* ── Shared harness: index one fixture file through the production pipeline ── */ + +typedef struct { + char tmpdir[256]; + char dbpath[512]; + char *project; + cbm_mcp_server_t *srv; +} LangProj; + +typedef struct { + const char *name; /* relative filename, may include '/' for subdirs */ + const char *content; +} LangFile; + +/* Given lp->tmpdir already populated with source files, run the production + * index_repository flow and open the resulting graph DB (NULL on failure). + * Split out of lang_index_files so a caller can interpose between writing files + * and indexing (e.g. the FILE_CHANGES_WITH test git-inits + commits first). */ +static cbm_store_t *lang_open_indexed(LangProj *lp) { + 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); +} + +/* Write each fixture file into a fresh temp project, index it via the MCP + * production flow (discover -> extract -> registry -> resolve -> dump), and open + * the resulting graph DB. Returns the store (NULL on any failure). */ +static cbm_store_t *lang_index_files(LangProj *lp, const LangFile *files, int nfiles) { + memset(lp, 0, sizeof(*lp)); + snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_lc_XXXXXX"); + if (!cbm_mkdtemp(lp->tmpdir)) { + return NULL; + } + for (int i = 0; i < nfiles; i++) { + char path[700]; + snprintf(path, sizeof(path), "%s/%s", lp->tmpdir, files[i].name); + /* Create parent directories if the fixture name embeds subdirs. + * Use mkdir_p (not mkdir) so multi-level paths like + * "pkg/validation/x.go" create every intermediate directory. */ + char *slash = strrchr(path, '/'); + if (slash && slash > path + strlen(lp->tmpdir)) { + *slash = '\0'; + cbm_mkdir_p(path, 0755); + *slash = '/'; + } + FILE *f = fopen(path, "w"); + if (!f) { + return NULL; + } + fputs(files[i].content, f); + fclose(f); + } + + return lang_open_indexed(lp); +} + +/* Convenience: index a single fixture file. */ +static cbm_store_t *lang_index(LangProj *lp, const char *filename, const char *content) { + LangFile f = {filename, content}; + return lang_index_files(lp, &f, 1); +} + +static void lang_cleanup(LangProj *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]; + char shm[600]; + snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath); + unlink(wal); + snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath); + unlink(shm); +} + +/* Count nodes of `label` with >=1 outbound edge. Returns -1 on query error. */ +static int label_with_outbound(cbm_store_t *store, const char *project, const char *label) { + cbm_search_params_t p = {0}; + p.project = project; + p.label = label; + p.min_degree = 1; + p.max_degree = -1; + p.limit = 50; + cbm_search_output_t out = {0}; + int n = -1; + if (cbm_store_search(store, &p, &out) == CBM_STORE_OK) { + n = out.count; + } + cbm_store_search_free(&out); + return n; +} + +/* Callables (Function OR Method) that have >=1 outbound edge — the cross-language + * "calls are attributed to the calling routine, not lumped on the file/Module + * node" invariant (a mis-attributed call would leave the callable at degree 0). */ +static int callables_with_outbound(cbm_store_t *store, const char *project) { + int fn = label_with_outbound(store, project, "Function"); + int mt = label_with_outbound(store, project, "Method"); + return (fn < 0 ? 0 : fn) + (mt < 0 ? 0 : mt); +} + +/* Count nodes carrying `label`. Returns -1 on query error. */ +static int count_label(cbm_store_t *store, const char *project, const char *label) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) != CBM_STORE_OK) { + return -1; + } + cbm_store_free_nodes(nodes, count); + return count; +} + +/* Count "type-like" nodes across the labels different languages use for a + * user-defined type (class/struct/interface/enum/trait/type alias). Lets a + * contract assert "the type was modeled" without hard-coding one label. */ +static int type_like_nodes(cbm_store_t *store, const char *project) { + static const char *labels[] = {"Class", "Struct", "Interface", "Enum", "Trait", "Type", NULL}; + int total = 0; + for (int i = 0; labels[i]; i++) { + int n = count_label(store, project, labels[i]); + if (n > 0) { + total += n; + } + } + return total; +} + +/* Run cbm_extract_file in a forked child; return true if the child died from a + * signal (SIGBUS/SIGSEGV/...). ASan does not install a SIGBUS handler, so an + * in-process crash would terminate the whole test runner — forking isolates it + * and lets us assert "extraction must not crash" deterministically. */ +static bool extract_crashes(const char *content, CBMLanguage lang, const char *relpath) { + fflush(NULL); + pid_t pid = fork(); + if (pid < 0) { + return false; /* cannot fork — do not flag a crash we can't observe */ + } + if (pid == 0) { + CBMFileResult *r = cbm_extract_file(content, (int)strlen(content), lang, "lc", relpath, 0, + NULL, NULL); + if (r) { + cbm_free_result(r); + } + _exit(0); + } + int status = 0; + (void)waitpid(pid, &status, 0); + return WIFSIGNALED(status); +} + +/* ══════════════════════════════════════════════════════════════════ + * KNOWN-BUG CONTRACTS (must FAIL until fixed) — see P2 + * ══════════════════════════════════════════════════════════════════ */ + +/* Kotlin 0-IMPORTS bug — ROOT CAUSE: extraction layer. The Kotlin AST nests + * imports as source_file -> import_list -> import_header*, but the extractor + * matched the "import" keyword token as a direct child of root -> 0 imports. + * This probe asserts cbm_extract_file captures them; it is the precise, reliable + * reproduction + fix-verification (graph-layer IMPORTS-edge formation depends on + * Maven/Gradle module resolution and is verified on the real ktor repo in the + * P5 scale tier — a synthetic small fixture can't faithfully exercise it). */ +static const char *KT_SRC = "import kotlin.io.path.Path\n" + "import java.util.ArrayList\n" + "\n" + "fun build(): ArrayList {\n" + " val xs = ArrayList()\n" + " xs.add(Path(\"/tmp/x\").toString())\n" + " return xs\n" + "}\n"; + +TEST(contract_kotlin_imports_extracted) { + CBMFileResult *r = + cbm_extract_file(KT_SRC, (int)strlen(KT_SRC), CBM_LANG_KOTLIN, "lc", "a.kt", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + int n = r->imports.count; + cbm_free_result(r); + ASSERT_TRUE(n >= 1); /* 2 imports in the fixture */ + PASS(); +} + +/* C: a function that calls another must yield a CALLS edge attributed to the + * CALLING FUNCTION (not lumped onto the file/Module node). */ +static const char *C_SRC = "static int helper(int x) { return x + 1; }\n" + "int run(int y) {\n" + " int a = helper(y);\n" + " return helper(a);\n" + "}\n"; + +TEST(contract_c_calls_attributed_to_function) { + LangProj lp; + cbm_store_t *store = lang_index(&lp, "a.c", C_SRC); + int calls = store ? cbm_store_count_edges_by_type(store, lp.project, "CALLS") : -1; + int fn_callers = store ? callables_with_outbound(store, lp.project) : -1; + lang_cleanup(&lp, store); + ASSERT_TRUE(calls >= 1); /* run() -> helper() */ + ASSERT_TRUE(fn_callers >= 1); /* the CALLS must hang off a Function, not Module */ + PASS(); +} + +/* Java: extraction must not crash on a real-world construct mix (enhanced-for + + * method reference + method chain + pattern instanceof) — reproduces the SIGBUS. */ +static const char *JAVA_SRC = "package zip;\n" + "import java.io.Closeable;\n" + "import java.lang.ref.Cleaner.Cleanable;\n" + "import java.util.ArrayList;\n" + "import java.util.List;\n" + "class Tracker {\n" + " private final List clean = new ArrayList<>();\n" + " private final List close = new ArrayList<>();\n" + " void assertAllClosed() throws Exception {\n" + " for (Closeable closeable : this.close) {\n" + " closeable.close();\n" + " }\n" + " this.clean.forEach(Cleanable::clean);\n" + " }\n" + " void added(Object obj, Cleanable cleanable) {\n" + " if (obj instanceof Closeable closeable) {\n" + " this.close.add(closeable);\n" + " }\n" + " }\n" + "}\n"; + +TEST(contract_java_extract_no_crash) { + bool crashed = extract_crashes(JAVA_SRC, CBM_LANG_JAVA, "Tracker.java"); + ASSERT_TRUE(!crashed); /* CURRENTLY MAY FAIL: SIGBUS in the Java LSP */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * RICH PER-LANGUAGE CONTRACTS (P3) — invariants that must hold for each + * hybrid-LSP language on a clean same-file fixture: resolution produces + * CALLS, those CALLS are attributed to a callable (not the Module), and + * user-defined types are modeled. These guard against silent per-language + * regressions (a grammar/LSP change that stops resolving or mis-attributes). + * ══════════════════════════════════════════════════════════════════ */ + +typedef struct { + int ok; /* graph DB opened */ + int calls; + int callers; /* callables (Function/Method) with an outbound edge */ + int types; /* type-like nodes */ + int imports; +} LangMetrics; + +static LangMetrics lang_metrics(const LangFile *files, int nfiles) { + LangProj lp; + cbm_store_t *store = lang_index_files(&lp, files, nfiles); + LangMetrics m = {0}; + if (store) { + m.ok = 1; + m.calls = cbm_store_count_edges_by_type(store, lp.project, "CALLS"); + m.callers = callables_with_outbound(store, lp.project); + m.types = type_like_nodes(store, lp.project); + m.imports = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS"); + } + lang_cleanup(&lp, store); + return m; +} + +/* Go: same-package function call. */ +TEST(contract_go_calls) { + static const LangFile f[] = { + {"svc.go", "package svc\n\n" + "func helper(x int) int { return x + 1 }\n\n" + "func run(y int) int {\n return helper(helper(y))\n}\n"}}; + LangMetrics m = lang_metrics(f, 1); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + PASS(); +} + +/* Rust: struct + impl methods + method call. */ +TEST(contract_rust_methods) { + static const LangFile f[] = { + {"calc.rs", "struct Calc {\n base: i32,\n}\n\n" + "impl Calc {\n" + " fn helper(&self, x: i32) -> i32 { self.base + x }\n" + " fn run(&self, y: i32) -> i32 { self.helper(y) }\n}\n\n" + "fn main() {\n let c = Calc { base: 1 };\n let _ = c.run(2);\n}\n"}}; + LangMetrics m = lang_metrics(f, 1); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + ASSERT_TRUE(m.types >= 1); /* struct Calc */ + PASS(); +} + +/* C#: class with methods + intra-class call. */ +TEST(contract_csharp_methods) { + static const LangFile f[] = { + {"Calc.cs", "namespace App {\n" + " class Calc {\n" + " public int Helper(int x) { return x + 1; }\n" + " public int Run(int y) { return Helper(y); }\n" + " }\n}\n"}}; + LangMetrics m = lang_metrics(f, 1); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + ASSERT_TRUE(m.types >= 1); /* class Calc */ + PASS(); +} + +/* PHP: class with methods + $this-> call. */ +TEST(contract_php_methods) { + static const LangFile f[] = { + {"Calc.php", "helper($y); }\n}\n"}}; + LangMetrics m = lang_metrics(f, 1); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + ASSERT_TRUE(m.types >= 1); /* class Calc */ + PASS(); +} + +/* Java: class with methods + intra-class call (single file — no crash risk). */ +TEST(contract_java_methods) { + static const LangFile f[] = { + {"Calc.java", "package app;\n\nclass Calc {\n" + " int helper(int x) { return x + 1; }\n" + " int run(int y) { return helper(y); }\n}\n"}}; + LangMetrics m = lang_metrics(f, 1); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + ASSERT_TRUE(m.types >= 1); /* class Calc */ + PASS(); +} + +/* Kotlin: class with methods + intra-class call. */ +TEST(contract_kotlin_methods) { + static const LangFile f[] = { + {"Calc.kt", "class Calc {\n" + " fun helper(x: Int): Int = x + 1\n" + " fun run(y: Int): Int = helper(y)\n}\n"}}; + LangMetrics m = lang_metrics(f, 1); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + ASSERT_TRUE(m.types >= 1); /* class Calc */ + PASS(); +} + +/* Python: a RELATIVE import resolves to a sibling module — produces an IMPORTS + * edge without needing a package manifest (resolve_relative_import), so this is + * a reliable small-fixture graph-layer IMPORTS contract. */ +TEST(contract_python_relative_import) { + static const LangFile f[] = { + {"util.py", "def helper(x):\n return x + 1\n"}, + {"main.py", "from .util import helper\n\n\ndef run(y):\n return helper(y)\n"}}; + LangMetrics m = lang_metrics(f, 2); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + ASSERT_TRUE(m.imports >= 1); /* IMPORTS edge for `from .util import helper` */ + PASS(); +} + +/* TypeScript: a relative import resolves to a sibling module → IMPORTS edge. */ +TEST(contract_typescript_relative_import) { + static const LangFile f[] = { + {"util.ts", "export function helper(x: number): number {\n return x + 1;\n}\n"}, + {"main.ts", "import { helper } from './util';\n\n" + "export function run(y: number): number {\n return helper(y);\n}\n"}}; + LangMetrics m = lang_metrics(f, 2); + ASSERT_TRUE(m.ok); + ASSERT_TRUE(m.calls >= 1); + ASSERT_TRUE(m.callers >= 1); + ASSERT_TRUE(m.imports >= 1); /* IMPORTS edge for `import { helper } from './util'` */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * GRAPH-LEVEL BREADTH (P4) — every grammar's fixture, indexed through the + * full pipeline, must produce its defs as graph nodes. Indexes all fixtures + * in ONE pass (each in its own gNNN/ subdir to avoid filename collisions). + * + * Discover maps files to a language by extension, so grammars whose extension + * collides with another's (e.g. .m objc/matlab, .yaml yaml/k8s) — or whose + * files discover intentionally skips — can't be isolated through index_repository. + * Those are allow-listed here and remain covered at the EXTRACTION level by + * test_grammar_regression.c (which uses an explicit language, bypassing discover). + * ══════════════════════════════════════════════════════════════════ */ + +enum { GRAMMAR_BREADTH_MAX = 300, GRAMMAR_PATH_BUF = 96 }; + +/* Grammars not reachable as standalone indexed files via index_repository; + * covered at the extraction level by test_grammar_regression.c instead. Reasons: + * nasm — fixture ext .asm; discover maps .nasm (.asm is ambiguous across assemblers) + * dotenv — discover does not route .env (often gitignored/secret) + * jsdoc, regex — not standalone source files; the grammars parse content embedded in JS + * gitignore — discover consumes .gitignore AS ignore rules, not as an indexed source + * gitattributes — same: consumed as a git metadata file + * sshconfig — discover detects ssh_config / .ssh/config, not the generic name "config" */ +static bool grammar_graph_allowlisted(const char *name) { + static const char *allow[] = {"nasm", "dotenv", "jsdoc", "regex", + "gitignore", "gitattributes", "sshconfig", NULL}; + for (int i = 0; allow[i]; i++) { + if (strcmp(allow[i], name) == 0) { + return true; + } + } + return false; +} + +/* Non-structural node count for a file (excludes the File/Module/Folder/Package + * wrappers the pipeline always creates) — i.e. how many defs reached the graph. */ +static int def_nodes_for_file(cbm_store_t *store, const char *project, const char *rel) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_file(store, project, rel, &nodes, &count) != CBM_STORE_OK) { + return -1; + } + int defs = 0; + for (int i = 0; i < count; i++) { + const char *l = nodes[i].label; + if (l && strcmp(l, "File") != 0 && strcmp(l, "Module") != 0 && strcmp(l, "Folder") != 0 && + strcmp(l, "Package") != 0 && strcmp(l, "Directory") != 0) { + defs++; + } + } + cbm_store_free_nodes(nodes, count); + return count == 0 ? -1 : defs; /* -1 signals "file not indexed at all" */ +} + +/* On a breadth failure, explain WHY: does direct extraction (no pipeline, no + * timeout) yield defs, and what node labels did the pipeline actually create for + * the file? Distinguishes "extraction itself broke" from "extraction works but + * the def didn't reach the graph / got an unexpected label". */ +static void breadth_diag(cbm_store_t *store, const char *project, const char *rel, + const GrammarCase *c) { + CBMFileResult *dr = + cbm_extract_file(c->src, (int)strlen(c->src), c->lang, "lc", c->path, 0, NULL, NULL); + int direct = dr ? dr->defs.count : -1; + const char *direct_label = (dr && dr->defs.count > 0) ? dr->defs.items[0].label : "-"; + char labels[256] = {0}; + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_file(store, project, rel, &nodes, &count) == CBM_STORE_OK) { + for (int i = 0; i < count && strlen(labels) < sizeof(labels) - 40; i++) { + char one[48]; + snprintf(one, sizeof(one), "%s ", nodes[i].label ? nodes[i].label : "?"); + strncat(labels, one, sizeof(labels) - strlen(labels) - 1); + } + } + cbm_store_free_nodes(nodes, count); + fprintf(stderr, " └─ direct_extract_defs=%d (label0=%s) graph_labels=[%s]\n", direct, + direct_label ? direct_label : "(null)", labels); + if (dr) { + cbm_free_result(dr); + } +} + +TEST(contract_all_grammars_in_graph) { + int n = (int)CBM_GRAMMAR_CASES_COUNT; + if (n > GRAMMAR_BREADTH_MAX) { + n = GRAMMAR_BREADTH_MAX; + } + static char names[GRAMMAR_BREADTH_MAX][GRAMMAR_PATH_BUF]; + LangFile files[GRAMMAR_BREADTH_MAX]; + for (int i = 0; i < n; i++) { + snprintf(names[i], sizeof(names[i]), "g%03d/%s", i, CBM_GRAMMAR_CASES[i].path); + files[i].name = names[i]; + files[i].content = CBM_GRAMMAR_CASES[i].src; + } + + LangProj lp; + cbm_store_t *store = lang_index_files(&lp, files, n); + int total_nodes = store ? cbm_store_count_nodes(store, lp.project) : -1; + + int not_indexed = 0; + int below_min = 0; + int checked = 0; + for (int i = 0; i < n; i++) { + const GrammarCase *c = &CBM_GRAMMAR_CASES[i]; + if (grammar_graph_allowlisted(c->name)) { + continue; + } + char rel[GRAMMAR_PATH_BUF]; + snprintf(rel, sizeof(rel), "g%03d/%s", i, c->path); + int defs = store ? def_nodes_for_file(store, lp.project, rel) : -1; + checked++; + if (defs < 0) { + fprintf(stderr, " [GRAPH-BREADTH] %-14s NOT INDEXED (0 nodes for %s)\n", c->name, rel); + breadth_diag(store, lp.project, rel, c); + not_indexed++; + } else if (defs < c->min_defs) { + fprintf(stderr, " [GRAPH-BREADTH] %-14s defs-in-graph=%d < min=%d\n", c->name, defs, + c->min_defs); + breadth_diag(store, lp.project, rel, c); + below_min++; + } + } + fprintf(stderr, + " [GRAPH-BREADTH] checked=%d not_indexed=%d below_min=%d total_nodes=%d (of %d grammars)\n", + checked, not_indexed, below_min, total_nodes, n); + + lang_cleanup(&lp, store); + ASSERT_TRUE(total_nodes > 0); + ASSERT_EQ(not_indexed, 0); + ASSERT_EQ(below_min, 0); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * CALLS-EDGE BREADTH (P5) — beyond the 9 hybrid-LSP languages, verify that a + * same-file caller->callee resolves to a CALLS edge through the generic + * name-based resolver (pass_calls.c). Languages where it does NOT are real + * edge gaps (existing bugs) — flagged here with expect_calls=false + a note, + * reproducibly, for the next (bugfixing) phase. Fixtures authored per-language + * to parse cleanly with the vendored grammar. + * ══════════════════════════════════════════════════════════════════ */ + +typedef struct { + const char *lang; + const char *filename; + const char *content; + bool expect_calls; /* true: must resolve a CALLS edge; false: known edge gap */ + const char *gap_note; /* why it doesn't resolve (for known gaps) */ +} CallCase; + +static const CallCase CALL_CASES[] = { + {"ruby", "a.rb", "def helper\n 42\nend\n\ndef run\n helper()\nend\n", true, NULL}, + {"lua", "a.lua", "function helper(x)\n return x + 1\nend\n\nfunction run()\n return helper(41)\nend\n", + true, NULL}, + {"swift", "a.swift", + "func helper() -> Int {\n return 42\n}\n\nfunc run() {\n let value = helper()\n print(value)\n}\n", + true, NULL}, + {"dart", "a.dart", "void helper() {\n print('helper');\n}\n\nvoid run() {\n helper();\n}\n", false, + "selector call node carries no callee field; no dart branch in extract_calls.c"}, + {"scala", "a.scala", "def helper(): Int =\n 21 + 21\n\ndef run(): Int =\n helper() * 2\n", true, NULL}, + {"bash", "a.sh", "helper() {\n echo \"doing work\"\n}\n\nrun() {\n helper\n}\n", true, NULL}, + {"perl", "a.pl", + "#!/usr/bin/perl\nuse strict;\nuse warnings;\n\nsub helper {\n my ($name) = @_;\n return \"Hello, $name\";\n}\n\nsub run {\n my $greeting = helper(\"world\");\n print \"$greeting\\n\";\n return;\n}\n\nrun();\n", + true, NULL}, + {"r", "a.R", "helper <- function(x) {\n x * 2\n}\n\nrun <- function() {\n helper(21)\n}\n", true, NULL}, + {"julia", "a.jl", "function helper(x)\n return x + 1\nend\n\nfunction run(n)\n return helper(n)\nend\n", + true, NULL}, + {"zig", "a.zig", "fn helper() void {}\n\npub fn run() void {\n helper();\n}\n", true, NULL}, + {"nim", "a.nim", "proc helper(): int =\n result = 42\n\nproc run() =\n let value = helper()\n echo value\n", + true, NULL}, + {"gdscript", "a.gd", + "func helper() -> int:\n\treturn 42\n\n\nfunc run() -> void:\n\tvar value = helper()\n\tprint(value)\n", + true, NULL}, + {"groovy", "a.groovy", "def helper() {\n println \"helping\"\n}\n\ndef run() {\n helper()\n}\n", + false, + "function_call callee not on a function/name field and first child is not 'identifier'; no groovy branch in extract_calls.c"}, + {"elixir", "a.ex", + "defmodule Sample do\n def helper(x) do\n x + 1\n end\n\n def run do\n helper(41)\n end\nend\n", + true, NULL}, + {"ocaml", "a.ml", + "let helper x = x + 1\n\nlet run () =\n let result = helper 41 in\n print_int result\n", true, NULL}, + {"gleam", "a.gleam", "pub fn helper(x: Int) -> Int {\n x + 1\n}\n\npub fn run() -> Int {\n helper(2)\n}\n", + true, NULL}, + {"crystal", "a.cr", + "def helper(name : String) : String\n \"hello, #{name}\"\nend\n\ndef run\n message = helper(\"world\")\n puts message\nend\n\nrun\n", + true, NULL}, + {"haskell", "a.hs", "helper :: Int -> Int\nhelper x = x + 1\n\nrun :: Int\nrun = helper 41\n", true, NULL}, + {"fortran", "a.f90", + "function helper(x) result(y)\n integer, intent(in) :: x\n integer :: y\n y = x + 1\nend function helper\n\nfunction run(n) result(total)\n integer, intent(in) :: n\n integer :: total\n total = helper(n) + helper(n + 1)\nend function run\n", + true, NULL}, + {"pascal", "a.pas", + "procedure Helper(x: Integer);\nbegin\n WriteLn(x);\nend;\n\nprocedure Run;\nbegin\n Helper(1);\nend;\n", + true, NULL}, + {"tcl", "a.tcl", + "proc helper {x} {\n return [expr {$x * 2}]\n}\n\nproc run {} {\n set result [helper 21]\n puts $result\n}\n", + true, NULL}, + {"solidity", "a.sol", + "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nfunction helper(uint256 x) pure returns (uint256) {\n return x + 1;\n}\n\nfunction run(uint256 y) pure returns (uint256) {\n return helper(y);\n}\n", + false, + "call_expression callee not resolved by generic field/first-child extraction; no solidity branch in extract_calls.c"}, + {"commonlisp", "a.lisp", "(defun helper (x)\n (* x 2))\n\n(defun run ()\n (helper 21))\n", false, + "list_lit call head is sym_lit not identifier; no commonlisp branch in extract_callee_name"}, + {"powershell", "a.ps1", "function helper {\n Write-Output 'hi'\n}\n\nfunction run {\n helper\n}\n", + false, + "command node child is command_name not identifier; extract_scripting_callee handles MATLAB not PowerShell"}, + + /* The remaining code grammars (extends CALLS-edge breadth to ALL 66 code + * grammars). expected_calls=false marks an already-root-caused gap. */ + {"ada", "a.adb", + "procedure Run is\n procedure Helper is\n begin\n null;\n end Helper;\nbegin\n Helper;\nend Run;\n", + false, + "procedure_call_statement callee did not resolve to a CALLS edge (empirically confirmed gap; no Ada branch in extract_calls.c)"}, + {"apex", "A.cls", + "public class A {\n static void helper() {\n System.debug('helper');\n }\n\n static void run() {\n helper();\n }\n}\n", + true, NULL}, + {"awk", "a.awk", "function helper(x) {\n return x + 1\n}\n\nfunction run() {\n print helper(3)\n}\n", + true, NULL}, + {"cairo", "a.cairo", "fn helper(x: felt252) -> felt252 {\n x + 1\n}\n\nfn run() -> felt252 {\n helper(41)\n}\n", + true, NULL}, + {"clojure", "a.clj", "(defn helper [] 42)\n\n(defn run [] (helper))\n", false, + "lisp: call is a list_lit whose head is a sym_lit (not a field, not a first-child 'identifier'); no lisp branch in extract_callee_name"}, + {"cuda", "a.cu", + "__device__ int helper(int x) {\n return x * 2;\n}\n\n__global__ void run(int *out) {\n out[0] = helper(21);\n}\n", + true, NULL}, + {"d", "a.d", "int helper(int x)\n{\n return x + 1;\n}\n\nvoid run()\n{\n int y = helper(41);\n}\n", true, + NULL}, + {"emacslisp", "a.el", + "(defun helper (x)\n \"Add one to X.\"\n (+ x 1))\n\n(defun run ()\n \"Call helper.\"\n (helper 41))\n", + false, "lisp: call is a 'list' whose head is a 'symbol'; generic resolver wants a first-child 'identifier'"}, + {"erlang", "a.erl", "-module(a).\n-export([run/0]).\n\nhelper(X) -> X + 1.\n\nrun() -> helper(41).\n", true, + NULL}, + {"fennel", "a.fnl", "(fn helper [x]\n (+ x 1))\n\n(fn run [n]\n (helper n))\n", false, + "lisp: call is a 'list' whose head is a 'symbol'; no fennel branch in extract_callee_name"}, + {"fish", "a.fish", "function helper\n echo \"helping\"\nend\n\nfunction run\n helper\nend\n", true, NULL}, + {"fsharp", "a.fs", "let helper x = x + 1\n\nlet run () = helper 41\n", false, + "application_expression callee head is a 'long_identifier_or_op' wrapper, not a bare identifier/field; no fsharp callee branch"}, + {"glsl", "a.glsl", "float helper(float x) {\n return x * 2.0;\n}\n\nvoid run() {\n float y = helper(3.0);\n}\n", + true, NULL}, + {"hare", "a.ha", "fn helper() void = void;\n\nexport fn run() void = {\n\thelper();\n};\n", true, NULL}, + {"hlsl", "a.hlsl", + "float helper(float x)\n{\n return x * 2.0;\n}\n\nfloat run(float v)\n{\n return helper(v) + 1.0;\n}\n", + true, NULL}, + {"ispc", "a.ispc", + "static inline uniform float helper(uniform float x) {\n return x * 2.0f;\n}\n\nexport void run(uniform float in[], uniform float out[], uniform int n) {\n foreach (i = 0 ... n) {\n out[i] = helper(in[i]);\n }\n}\n", + true, NULL}, + {"luau", "a.luau", "function helper(x)\n\treturn x + 1\nend\n\nfunction run(n)\n\treturn helper(n) * 2\nend\n", + true, NULL}, + {"matlab", "a.m", "function run()\n helper();\nend\n\nfunction helper()\n disp('hi');\nend\n", true, + "watch: .m extension collides with Objective-C under discover (content-sniffed)"}, + {"odin", "a.odin", + "package fixture\n\nhelper :: proc() -> int {\n\treturn 42\n}\n\nrun :: proc() {\n\tx := helper()\n\t_ = x\n}\n", + true, NULL}, + {"racket", "a.rkt", "#lang racket\n\n(define (helper x)\n (+ x 1))\n\n(define (run)\n (helper 41))\n", false, + "lisp: call is a 'list' whose head is a 'symbol' (grammar has no 'identifier' node); no racket branch"}, + {"rescript", "a.res", "let helper = (x) => x + 1\n\nlet run = () => helper(41)\n", false, + "call_expression 'function' field is a 'value_identifier' (not in extract_callee_from_fields' accepted type list)"}, + {"scheme", "a.scm", "(define (helper x)\n (* x 2))\n\n(define (run)\n (helper 21))\n", false, + "lisp: call is a 'list' whose head is a 'symbol'; no scheme branch in extract_callee_name"}, + {"slang", "a.slang", "void helper()\n{\n int x = 1;\n}\n\nvoid run()\n{\n helper();\n}\n", true, NULL}, + {"squirrel", "a.nut", "function helper(x) {\n return x + 1;\n}\n\nfunction run() {\n return helper(41);\n}\n", + true, NULL}, + {"starlark", "a.bzl", "def helper(x):\n return x + 1\n\ndef run():\n return helper(41)\n", true, NULL}, + {"sway", "a.sw", "fn helper() {}\n\nfn run() {\n helper();\n}\n", true, NULL}, + {"teal", "a.tl", + "local function helper(x: number): number\n return x + 1\nend\n\nlocal function run(): number\n return helper(41)\nend\n", + true, NULL}, + {"vimscript", "a.vim", + "function! Helper() abort\n return 1\nendfunction\n\nfunction! Run() abort\n call Helper()\nendfunction\n", + true, NULL}, + {"wgsl", "a.wgsl", "fn helper() -> f32 {\n return 1.0;\n}\n\nfn run() -> f32 {\n return helper();\n}\n", false, + "callee nested in type_constructor_or_function_call_expression -> type_declaration -> identifier; no field, first child is type_declaration"}, + {"zsh", "a.zsh", "function helper {\n print \"helping\"\n}\n\nfunction run {\n helper\n}\n", true, NULL}, +}; + +TEST(contract_calls_breadth) { + /* EVERY language must resolve a same-file caller->callee into a CALLS edge. + * No skips/allowlists: a language that does not is a hard FAILURE that + * reproduces the bug (the gap_note explains the known cause for the ones we + * have already root-caused). These stay RED until fixed. */ + int n = (int)(sizeof(CALL_CASES) / sizeof(CALL_CASES[0])); + int failures = 0; + for (int i = 0; i < n; i++) { + const CallCase *c = &CALL_CASES[i]; + const LangFile f = {c->filename, c->content}; + LangMetrics m = lang_metrics(&f, 1); + if (!(m.ok && m.calls >= 1 && m.callers >= 1)) { + fprintf(stderr, " [CALLS-BREADTH] FAIL %-11s ok=%d calls=%d callers=%d%s%s\n", c->lang, + m.ok, m.calls, m.callers, c->gap_note ? " — " : "", + c->gap_note ? c->gap_note : ""); + failures++; + } + } + fprintf(stderr, " [CALLS-BREADTH] %d langs: %d FAILURES (each = a language that does not " + "resolve a same-file CALLS edge)\n", + n, failures); + ASSERT_EQ(failures, 0); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * CROSS-CUTTING / SEMANTIC EDGE-TYPE PRESENCE CONTRACTS (P6) + * + * CALLS (P3/P5) and IMPORTS (test_grammar_imports.c) cover the two + * highest-volume edge types. This section guards the REMAINING graph + * edge types the pipeline emits, each with the minimal fixture that + * triggers it through the SAME production index_repository flow + * (mode=full, so the similarity + semantic predump passes run): + * + * structural DEFINES, DEFINES_METHOD, CONTAINS_FILE + * type-relationship INHERITS, IMPLEMENTS, DECORATES + * test-mapping TESTS_FILE + * service/dataflow HANDLES, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS + * similarity SIMILAR_TO, SEMANTICALLY_RELATED + * + * We assert PRESENCE (count >= floor), never an exact count — edge + * counts are non-deterministic (parallel LSH / similarity / resolve). + * A regression that stops emitting any of these turns its test RED. + * Fixtures are reasoned from the producing pass (and SIMILAR_TO / + * SEMANTICALLY_RELATED were also verified end-to-end) so these are + * GREEN contracts: a failure here is a real edge-production regression, + * not a known gap. + * ══════════════════════════════════════════════════════════════════ */ + +/* Every graph edge type the pipeline can emit — used to print a histogram + * when an edge contract fails, so a regression shows exactly what WAS + * produced instead of the missing type. */ +static const char *ALL_EDGE_TYPES[] = { + "CALLS", "CONFIGURES", "CONTAINS_FILE", "CONTAINS_FOLDER", + "DATA_FLOWS", "DECORATES", "DEFINES", "DEFINES_METHOD", + "DEPENDS_ON", "FILE_CHANGES_WITH", "GRAPHQL_CALLS", "GRPC_CALLS", + "HANDLES", "HTTP_CALLS", "IMPLEMENTS", "IMPORTS", + "INHERITS", "INFRA_MAPS", "OVERRIDE", "SEMANTICALLY_RELATED", + "SIMILAR_TO", "TESTS_FILE", "TESTS", "TRPC_CALLS", + "USAGE", "ASYNC_CALLS", NULL}; + +static void dump_edge_histogram(cbm_store_t *store, const char *project) { + if (!store) { + fprintf(stderr, " └─ (no graph DB)\n"); + return; + } + char line[640] = {0}; + for (int i = 0; ALL_EDGE_TYPES[i]; i++) { + int c = cbm_store_count_edges_by_type(store, project, ALL_EDGE_TYPES[i]); + if (c > 0 && strlen(line) < sizeof(line) - 48) { + char one[64]; + snprintf(one, sizeof(one), "%s=%d ", ALL_EDGE_TYPES[i], c); + strncat(line, one, sizeof(line) - strlen(line) - 1); + } + } + fprintf(stderr, " └─ edges produced: [%s]\n", line[0] ? line : "(none)"); +} + +/* Index `files`, assert the named edge type appears at least `floor` times. + * On failure, dump the full edge histogram so a regression is diagnosable. */ +static int edge_present(const LangFile *files, int nfiles, const char *edge, int floor) { + LangProj lp; + cbm_store_t *store = lang_index_files(&lp, files, nfiles); + int got = store ? cbm_store_count_edges_by_type(store, lp.project, edge) : -1; + if (got < floor) { + fprintf(stderr, " [EDGE] FAIL %-20s count=%d expected>=%d\n", edge, got, floor); + dump_edge_histogram(store, lp.project); + } + lang_cleanup(&lp, store); + return got >= floor; +} + +/* DEFINES — File -> definition, once per top-level def (purely structural). */ +TEST(contract_edge_defines) { + static const LangFile f[] = { + {"main.py", "def greet(name):\n return 'Hello ' + name\n\n\n" + "def farewell(name):\n return 'Goodbye ' + name\n\n\n" + "def main():\n msg = greet('World')\n" + " msg2 = farewell('World')\n print(msg, msg2)\n"}}; + ASSERT_TRUE(edge_present(f, 1, "DEFINES", 3)); /* greet, farewell, main */ + PASS(); +} + +/* DEFINES_METHOD — Class -> Method when the method's parent_class resolves. */ +TEST(contract_edge_defines_method) { + static const LangFile f[] = { + {"greeter.py", "class Greeter:\n def hello(self):\n return \"hi\"\n\n" + " def bye(self):\n return \"bye\"\n\n\n" + "def main():\n g = Greeter()\n return g.hello()\n"}}; + ASSERT_TRUE(edge_present(f, 1, "DEFINES_METHOD", 1)); /* Greeter.hello, Greeter.bye */ + PASS(); +} + +/* CONTAINS_FILE — Folder/Project -> File for each discovered file. */ +TEST(contract_edge_contains_file) { + static const LangFile f[] = { + {"pkg/util.py", "def add(a, b):\n return a + b\n\n\ndef sub(a, b):\n return a - b\n"}, + {"pkg/helpers.py", "def greet(name):\n return \"hello \" + name\n\n\n" + "class Greeter:\n def __init__(self, prefix):\n" + " self.prefix = prefix\n\n" + " def say(self, name):\n return self.prefix + name\n"}}; + ASSERT_TRUE(edge_present(f, 2, "CONTAINS_FILE", 1)); /* pkg/ -> util.py, helpers.py */ + PASS(); +} + +/* INHERITS — subclass -> base via base_classes resolving to a Class/Type node. + * Uses JAVA `extends`: Java's base_classes extraction is verified by + * test_extraction.c:java_class_extends_and_implements, and same-file classes + * resolve same-module. (Python `class X(Base)` does NOT yet work in-unit — that + * is a real extraction bug reproduced RED by + * test_extraction.c:python_class_base_extracted_bare; do not use Python here.) */ +TEST(contract_edge_inherits) { + static const LangFile f[] = { + {"Zoo.java", "class Base {\n int base() { return 0; }\n}\n\n" + "class Animal extends Base {\n int speak() { return 1; }\n}\n\n" + "class Dog extends Animal {\n int speak() { return 2; }\n}\n"}}; + ASSERT_TRUE(edge_present(f, 1, "INHERITS", 1)); /* Animal->Base, Dog->Animal */ + PASS(); +} + +/* IMPLEMENTS — Rust `impl Trait for Struct` (Java/TS `implements` -> INHERITS). */ +TEST(contract_edge_implements) { + static const LangFile f[] = { + {"shapes.rs", "trait Greet {\n fn hello(&self) -> String;\n}\n\n" + "trait Area {\n fn area(&self) -> f64;\n}\n\n" + "struct English;\n\nstruct Square {\n side: f64,\n}\n\n" + "impl Greet for English {\n fn hello(&self) -> String {\n" + " String::from(\"hi\")\n }\n}\n\n" + "impl Area for Square {\n fn area(&self) -> f64 {\n" + " self.side * self.side\n }\n}\n\n" + "impl Greet for Square {\n fn hello(&self) -> String {\n" + " String::from(\"square\")\n }\n}\n"}}; + ASSERT_TRUE(edge_present(f, 1, "IMPLEMENTS", 2)); /* 3 impl-for blocks */ + PASS(); +} + +/* DECORATES — decorated def -> locally-defined decorator function. */ +TEST(contract_edge_decorates) { + static const LangFile f[] = { + {"audit.py", "def audit(func):\n def wrapper(*args, **kwargs):\n" + " return func(*args, **kwargs)\n return wrapper\n\n\n" + "def trace(func):\n def inner(*args, **kwargs):\n" + " return func(*args, **kwargs)\n return inner\n\n\n" + "@audit\ndef process_order(order_id):\n return order_id\n\n\n" + "@trace\ndef cancel_order(order_id):\n return order_id\n\n\n" + "@audit\ndef refund_order(order_id):\n return order_id\n"}}; + ASSERT_TRUE(edge_present(f, 1, "DECORATES", 2)); /* 3 decorated defs */ + PASS(); +} + +/* TESTS_FILE — test file -> production file via path derivation (test_foo->foo). */ +TEST(contract_edge_tests_file) { + static const LangFile f[] = { + {"foo.py", "def add(a, b):\n return a + b\n\n\ndef multiply(a, b):\n return a * b\n"}, + {"test_foo.py", "from foo import add, multiply\n\n\n" + "def test_add():\n assert add(2, 3) == 5\n\n\n" + "def test_multiply():\n assert multiply(2, 3) == 6\n"}}; + ASSERT_TRUE(edge_present(f, 2, "TESTS_FILE", 1)); + PASS(); +} + +/* HANDLES — Flask @app.route decorator handler -> Route node. */ +TEST(contract_edge_handles) { + static const LangFile f[] = { + {"app.py", "from flask import Flask\n\napp = Flask(__name__)\n\n\n" + "@app.route(\"/users\")\ndef list_users():\n return {\"users\": []}\n\n\n" + "@app.route(\"/health\")\ndef health_check():\n return {\"status\": \"ok\"}\n"}}; + ASSERT_TRUE(edge_present(f, 1, "HANDLES", 1)); /* 2 route handlers */ + PASS(); +} + +/* HTTP_CALLS — call resolving to a QN containing an HTTP-client lib id + * ("requests") with a URL-shaped first arg -> Route node. Uses the SEQUENTIAL + * pass path (small fixture): a local wrapper whose QN carries the lib substring. */ +TEST(contract_edge_http_calls) { + static const LangFile f[] = { + {"service_b.py", + "def requests_get(url, params=None):\n return {\"url\": url, \"params\": params}\n\n\n" + "def requests_post(url, body=None):\n return {\"url\": url, \"body\": body}\n\n\n" + "def service_a_fetch_order(order_id):\n" + " return requests_get(\"/api/orders/list\", params={\"id\": order_id})\n\n\n" + "def service_a_create_order(payload):\n" + " return requests_post(\"/api/orders/create\", body=payload)\n"}}; + ASSERT_TRUE(edge_present(f, 1, "HTTP_CALLS", 1)); + PASS(); +} + +/* ASYNC_CALLS — call resolving to a QN containing an async-broker id (dir + * "pubsub/" injects it via the FQN scheme) with a topic-shaped first arg. */ +TEST(contract_edge_async_calls) { + static const LangFile f[] = { + {"pubsub/publisher.py", + "def publish_event(topic, payload):\n return (topic, payload)\n\n\n" + "def enqueue_order(order):\n return publish_event(\"order-events-topic\", order)\n\n\n" + "def enqueue_shipment(shipment):\n" + " return publish_event(\"shipment-events-topic\", shipment)\n"}}; + ASSERT_TRUE(edge_present(f, 1, "ASYNC_CALLS", 1)); + PASS(); +} + +/* DATA_FLOWS — caller -> handler through a shared Route: a decorator handler + * (HANDLES) and an HTTP call to the SAME ANY-method path (HTTP_CALLS) on a + * Route whose QN matches (__route__ANY__/orders). create_data_flows then links + * the HTTP caller to the route handler. Sequential-path-safe: the HTTP call + * resolves to a local "requests"-substring wrapper, and method NULL -> "ANY". */ +TEST(contract_edge_data_flows) { + static const LangFile f[] = { + {"app.py", "from flask import Flask\n\napp = Flask(__name__)\n\n\n" + "@app.route(\"/orders\")\ndef list_orders():\n return {\"orders\": []}\n\n\n" + "def requests_get(url, params=None):\n return {\"url\": url}\n\n\n" + "def client():\n return requests_get(\"/orders\")\n"}}; + ASSERT_TRUE(edge_present(f, 1, "DATA_FLOWS", 1)); /* client -> list_orders via /orders */ + PASS(); +} + +/* SIMILAR_TO — structural near-clones (>=0.95 MinHash Jaccard), same extension. + * This is the exact clone pair from tests/test_simhash.c. */ +TEST(contract_edge_similar_to) { + static const LangFile f[] = { + {"pkg/validation/user_validator.go", + "package validation\nimport \"errors\"\nimport \"strings\"\n" + "func ValidateUser(u User) error {\n" + " if u.Name == \"\" { return errors.New(\"name required\") }\n" + " if len(u.Name) > 100 { return errors.New(\"name too long\") }\n" + " if u.Age < 0 { return errors.New(\"invalid age\") }\n" + " if u.Age > 200 { return errors.New(\"age too high\") }\n" + " if u.Email == \"\" { return errors.New(\"email required\") }\n" + " if !strings.Contains(u.Email, \"@\") { return errors.New(\"invalid email\") }\n" + " if u.Phone == \"\" { return errors.New(\"phone required\") }\n" + " if len(u.Phone) < 7 { return errors.New(\"phone too short\") }\n" + " if u.Country == \"\" { return errors.New(\"country required\") }\n" + " for _, c := range u.Tags {\n if c == \"\" { return errors.New(\"empty tag\") }\n }\n" + " return nil\n}\n"}, + {"pkg/validation/order_validator.go", + "package validation\nimport \"errors\"\nimport \"strings\"\n" + "func ValidateOrder(o Order) error {\n" + " if o.Title == \"\" { return errors.New(\"title required\") }\n" + " if len(o.Title) > 100 { return errors.New(\"title too long\") }\n" + " if o.Amount < 0 { return errors.New(\"invalid amount\") }\n" + " if o.Amount > 200 { return errors.New(\"amount too high\") }\n" + " if o.Status == \"\" { return errors.New(\"status required\") }\n" + " if !strings.Contains(o.Status, \"@\") { return errors.New(\"invalid status\") }\n" + " if o.Region == \"\" { return errors.New(\"region required\") }\n" + " if len(o.Region) < 7 { return errors.New(\"region too short\") }\n" + " if o.Vendor == \"\" { return errors.New(\"vendor required\") }\n" + " for _, c := range o.Items {\n if c == \"\" { return errors.New(\"empty item\") }\n }\n" + " return nil\n}\n"}}; + ASSERT_TRUE(edge_present(f, 2, "SIMILAR_TO", 1)); + PASS(); +} + +/* SEMANTICALLY_RELATED — near-identical functions sharing vocabulary/callees/ + * types but DIFFERENT control-flow shapes, so the semantic score clears 0.75 + * while the structural MinHash stays under the 0.95 SIMILAR_TO short-circuit. */ +TEST(contract_edge_semantically_related) { + static const LangFile f[] = { + {"records.py", + "def sanitize(value: str) -> str:\n return value.strip().lower()\n\n\n" + "def lookup(table: dict, key: str) -> str:\n return table.get(key, \"\")\n\n\n" + "def audit_log(message: str) -> None:\n print(message)\n\n\n" + "def normalize_user_record(record: dict, table: dict) -> dict:\n" + " \"\"\"Normalize a user record by sanitizing fields and looking up defaults.\"\"\"\n" + " result = {}\n name = sanitize(record.get(\"name\", \"\"))\n" + " email = sanitize(record.get(\"email\", \"\"))\n role = lookup(table, name)\n" + " if name and email:\n result[\"name\"] = name\n result[\"email\"] = email\n" + " result[\"role\"] = role\n audit_log(\"normalized user record\")\n" + " return result\n\n\n" + "def normalize_account_record(record: dict, table: dict) -> dict:\n" + " \"\"\"Normalize an account record by sanitizing fields and looking up defaults.\"\"\"\n" + " result = {}\n name = sanitize(record.get(\"name\", \"\"))\n" + " email = sanitize(record.get(\"email\", \"\"))\n role = lookup(table, name)\n" + " while name and email:\n result[\"name\"] = name\n result[\"email\"] = email\n" + " result[\"role\"] = role\n audit_log(\"normalized account record\")\n" + " break\n return result\n\n\n" + "def normalize_member_record(record: dict, table: dict) -> dict:\n" + " \"\"\"Normalize a member record by sanitizing fields and looking up defaults.\"\"\"\n" + " result = {}\n name = sanitize(record.get(\"name\", \"\"))\n" + " email = sanitize(record.get(\"email\", \"\"))\n role = lookup(table, name)\n" + " for _ in range(1):\n if not (name and email):\n continue\n" + " result[\"name\"] = name\n result[\"email\"] = email\n" + " result[\"role\"] = role\n audit_log(\"normalized member record\")\n" + " return result\n\n\n" + "def normalize_profile_record(record: dict, table: dict) -> dict:\n" + " \"\"\"Normalize a profile record by sanitizing fields and looking up defaults.\"\"\"\n" + " result = {}\n name = sanitize(record.get(\"name\", \"\"))\n" + " email = sanitize(record.get(\"email\", \"\"))\n role = lookup(table, name)\n" + " try:\n assert name and email\n result[\"name\"] = name\n" + " result[\"email\"] = email\n result[\"role\"] = role\n" + " audit_log(\"normalized profile record\")\n" + " except AssertionError:\n audit_log(\"skipped profile record\")\n" + " return result\n"}}; + ASSERT_TRUE(edge_present(f, 1, "SEMANTICALLY_RELATED", 1)); + PASS(); +} + +/* ── P6 (continued): the remaining pipeline edge types ───────────────── + * TESTS + DEPENDS_ON are produced in any pipeline path. The service-protocol + * edges (GRAPHQL_CALLS/GRPC_CALLS/TRPC_CALLS) and INFRA_MAPS are emitted ONLY + * in the PARALLEL path (file_count > 50), and FILE_CHANGES_WITH needs real git + * history — so those use a padded / git-initialised fixture. All fixtures here + * were empirically verified to produce their edge against the prod binary. */ + +/* TESTS — function-level test->tested-function (distinct from TESTS_FILE). + * Requires a CROSS-FILE call: a Test* function in a *_test file calling a + * production function in a NON-test file (the target must not be in a test + * path). pass_tests runs regardless of file count. */ +TEST(contract_edge_tests) { + static const LangFile f[] = { + {"calc.go", "package calc\n\nfunc Add(a int, b int) int {\n\treturn a + b\n}\n\n" + "func Mul(a int, b int) int {\n\treturn a * b\n}\n"}, + {"calc_test.go", "package calc\n\nfunc TestAdd(t *T) {\n\tgot := Add(2, 3)\n\t_ = got\n}\n\n" + "func TestMul(t *T) {\n\tgot := Mul(2, 3)\n\t_ = got\n}\n"}}; + ASSERT_TRUE(edge_present(f, 2, "TESTS", 1)); /* TestAdd->Add, TestMul->Mul */ + PASS(); +} + +/* DEPENDS_ON — Helm Chart.yaml `dependencies:` -> per-dependency Chart node. + * Basename must be exactly "Chart.yaml"; pass_k8s runs in both pipeline paths. */ +TEST(contract_edge_depends_on) { + static const LangFile f[] = { + {"charts/myapp/Chart.yaml", + "apiVersion: v2\nname: myapp\nversion: 1.0.0\ndescription: A test chart\n" + "dependencies:\n - name: postgresql\n version: \"11.6.12\"\n" + " repository: \"https://charts.bitnami.com/bitnami\"\n" + " - name: redis\n version: \"16.8.5\"\n" + " repository: \"https://charts.bitnami.com/bitnami\"\n"}, + {"main.py", "def hello():\n return \"world\"\n"}}; + ASSERT_TRUE(edge_present(f, 2, "DEPENDS_ON", 1)); /* postgresql, redis */ + PASS(); +} + +/* Index `meaningful` files plus enough trivial pad files to cross the + * MIN_FILES_FOR_PARALLEL (=50) threshold, forcing the PARALLEL pipeline path + * (the only path that emits GRAPHQL_CALLS/GRPC_CALLS/TRPC_CALLS/INFRA_MAPS). */ +enum { PARALLEL_PAD_FILES = 52 }; + +static cbm_store_t *index_parallel_fixture(LangProj *lp, const LangFile *meaningful, int n_mean) { + static char pad_name[PARALLEL_PAD_FILES][40]; + static char pad_body[PARALLEL_PAD_FILES][64]; + LangFile files[PARALLEL_PAD_FILES + 16]; + int n = 0; + for (int i = 0; i < n_mean; i++) { + files[n++] = meaningful[i]; + } + for (int i = 0; i < PARALLEL_PAD_FILES; i++) { + snprintf(pad_name[i], sizeof(pad_name[i]), "pad/pad_%02d.py", i); + snprintf(pad_body[i], sizeof(pad_body[i]), "def pad_%02d():\n return %d\n", i, i); + files[n].name = pad_name[i]; + files[n].content = pad_body[i]; + n++; + } + return lang_index_files(lp, files, n); +} + +/* GRAPHQL_CALLS + GRPC_CALLS + TRPC_CALLS + INFRA_MAPS — all parallel-path-only, + * so they share one padded (>50-file) index. Each edge is triggered by an + * independent in-repo symbol whose resolved QN carries the relevant library + * substring; combining them in one repo doesn't couple them. */ +TEST(contract_edge_parallel_service_edges) { + static const LangFile meaningful[] = { + /* GRAPHQL_CALLS: local gql() — resolved callee QN contains "gql". */ + {"gql.py", "def gql(query_string):\n return query_string\n"}, + {"client.py", "from gql import gql\n\n\ndef fetch_user():\n" + " return gql(\"query GetUser { user { id name } }\")\n\n\n" + "def create_user():\n" + " return gql(\"mutation CreateUser { addUser(name: \\\"x\\\") { id } }\")\n"}, + /* TRPC_CALLS: local createTRPCProxyClient (same-module resolution). */ + {"trpc_client.ts", + "export function createTRPCProxyClient(opts: any): any {\n return { invoke: () => opts };\n}\n\n" + "export function loadUser(id: string) {\n const client = createTRPCProxyClient({ id });\n" + " return client;\n}\n"}, + /* GRPC_CALLS: Go ServiceClient suffix heuristic (cross-package call). */ + {"go.mod", "module example.com/grpcdemo\n\ngo 1.21\n"}, + {"cartpb/cart_grpc.pb.go", + "package cartpb\n\nimport \"context\"\n\ntype GetCartRequest struct{ UserId string }\n" + "type GetCartResponse struct{ Items int }\n\ntype CartServiceClient interface {\n" + "\tGetCart(ctx context.Context, in *GetCartRequest) (*GetCartResponse, error)\n}\n\n" + "type cartServiceClient struct{}\n\nfunc NewCartServiceClient(cc interface{}) CartServiceClient {\n" + "\treturn &cartServiceClient{}\n}\n\nfunc (c *cartServiceClient) GetCart(ctx context.Context, " + "in *GetCartRequest) (*GetCartResponse, error) {\n\treturn &GetCartResponse{}, nil\n}\n"}, + {"client/main.go", + "package main\n\nimport (\n\t\"context\"\n\n\tpb \"example.com/grpcdemo/cartpb\"\n)\n\n" + "func FetchCart(conn interface{}) {\n\tclient := pb.NewCartServiceClient(conn)\n" + "\tclient.GetCart(context.Background(), &pb.GetCartRequest{UserId: \"u1\"})\n}\n\n" + "func main() {\n\tFetchCart(nil)\n}\n"}, + /* INFRA_MAPS: ASYNC_CALLS topic route (pubsub) matched to an infra YAML + * subscription whose push_endpoint becomes the infra route. */ + {"app/pubsub.py", "def publish(topic, data):\n return (topic, data)\n"}, + {"app/publisher.py", "import pubsub\n\n\ndef publish_order(data):\n" + " pubsub.publish(\"order-events\", data)\n"}, + {"infra/pubsub/subscription.yaml", + "subscriptions:\n - name: order-worker-sub\n topic: order-events\n" + " config:\n push_endpoint: https://order-worker-abc123-uc.a.run.app/handle\n"}}; + + LangProj lp; + cbm_store_t *store = + index_parallel_fixture(&lp, meaningful, (int)(sizeof(meaningful) / sizeof(meaningful[0]))); + int graphql = store ? cbm_store_count_edges_by_type(store, lp.project, "GRAPHQL_CALLS") : -1; + int grpc = store ? cbm_store_count_edges_by_type(store, lp.project, "GRPC_CALLS") : -1; + int trpc = store ? cbm_store_count_edges_by_type(store, lp.project, "TRPC_CALLS") : -1; + int infra = store ? cbm_store_count_edges_by_type(store, lp.project, "INFRA_MAPS") : -1; + if (graphql < 1 || grpc < 1 || trpc < 1 || infra < 1) { + fprintf(stderr, + " [EDGE] parallel-service: GRAPHQL_CALLS=%d GRPC_CALLS=%d TRPC_CALLS=%d INFRA_MAPS=%d\n", + graphql, grpc, trpc, infra); + dump_edge_histogram(store, lp.project); + } + lang_cleanup(&lp, store); + ASSERT_TRUE(graphql >= 1); + ASSERT_TRUE(grpc >= 1); + ASSERT_TRUE(trpc >= 1); + ASSERT_TRUE(infra >= 1); + PASS(); +} + +/* Run "git -C "; direct git invocation (no shell builtins) so it + * works under both POSIX shells and cmd.exe. Returns the command exit status. */ +static int run_git(const char *dir, const char *args) { + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "git -C \"%s\" %s", dir, args); + return system(cmd); +} + +/* FILE_CHANGES_WITH — git co-change coupling: two files that change together + * across >=3 commits (coupling >= 0.3). The standard harness indexes a plain + * non-git dir, so this test builds its own git repo (init + 4 commits touching + * BOTH files) before indexing via lang_open_indexed. */ +TEST(contract_edge_file_changes_with) { + LangProj lp; + memset(&lp, 0, sizeof(lp)); + snprintf(lp.tmpdir, sizeof(lp.tmpdir), "/tmp/cbm_fcw_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(lp.tmpdir)); + + char a[700]; + char b[700]; + snprintf(a, sizeof(a), "%s/alpha.py", lp.tmpdir); + snprintf(b, sizeof(b), "%s/beta.py", lp.tmpdir); + th_write_file(a, "def alpha_v1():\n return 1\n"); + th_write_file(b, "def beta_v1():\n return 1\n"); + + /* git must be present (this IS a git project) — a failed init = broken env, + * which is a real failure, not a skip. */ + ASSERT_EQ(run_git(lp.tmpdir, "init -q"), 0); + run_git(lp.tmpdir, "config user.email t@t.io"); + run_git(lp.tmpdir, "config user.name t"); + run_git(lp.tmpdir, "add -A"); + run_git(lp.tmpdir, "commit -qm c1"); + for (int i = 2; i <= 4; i++) { + th_append_file(a, "\n\ndef alpha_more():\n return 9\n"); + th_append_file(b, "\n\ndef beta_more():\n return 9\n"); + char msg[24]; + run_git(lp.tmpdir, "add -A"); + snprintf(msg, sizeof(msg), "commit -qm c%d", i); + run_git(lp.tmpdir, msg); + } + + cbm_store_t *store = lang_open_indexed(&lp); + int fcw = store ? cbm_store_count_edges_by_type(store, lp.project, "FILE_CHANGES_WITH") : -1; + if (fcw < 1) { + dump_edge_histogram(store, lp.project); + } + lang_cleanup(&lp, store); + ASSERT_TRUE(fcw >= 1); /* alpha.py <-> beta.py co-change across 4 commits */ + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ */ + +SUITE(lang_contract) { + /* The three known-bug reproductions. Kotlin reproduces in-fixture (an + * extraction-layer bug, scale-independent). C call-attribution and the + * Java/TS extraction crash only manifest at real-repo scale, so a small + * fixture passes here — their reproductions live in the real-repo scale + * tier; these fast contracts still guard against regressions. */ + RUN_TEST(contract_kotlin_imports_extracted); + RUN_TEST(contract_c_calls_attributed_to_function); + RUN_TEST(contract_java_extract_no_crash); + + /* Rich per-language invariants (P3). */ + RUN_TEST(contract_go_calls); + RUN_TEST(contract_rust_methods); + RUN_TEST(contract_csharp_methods); + RUN_TEST(contract_php_methods); + RUN_TEST(contract_java_methods); + RUN_TEST(contract_kotlin_methods); + RUN_TEST(contract_python_relative_import); + RUN_TEST(contract_typescript_relative_import); + + /* Graph-level breadth across all grammars (P4). */ + RUN_TEST(contract_all_grammars_in_graph); + + /* CALLS-edge breadth across non-LSP languages (P5). */ + RUN_TEST(contract_calls_breadth); + + /* Cross-cutting / semantic edge-type presence (P6). Each asserts the + * pipeline still emits that edge type; a RED here is a real regression. */ + RUN_TEST(contract_edge_defines); + RUN_TEST(contract_edge_defines_method); + RUN_TEST(contract_edge_contains_file); + RUN_TEST(contract_edge_inherits); + RUN_TEST(contract_edge_implements); + RUN_TEST(contract_edge_decorates); + RUN_TEST(contract_edge_tests_file); + RUN_TEST(contract_edge_handles); + RUN_TEST(contract_edge_http_calls); + RUN_TEST(contract_edge_async_calls); + RUN_TEST(contract_edge_data_flows); + RUN_TEST(contract_edge_similar_to); + RUN_TEST(contract_edge_semantically_related); + + /* P6 (continued): remaining edge types — TESTS, DEPENDS_ON, the + * parallel-path service edges (GRAPHQL/GRPC/TRPC_CALLS + INFRA_MAPS), and + * FILE_CHANGES_WITH (git co-change). Completes 25-edge-type coverage. */ + RUN_TEST(contract_edge_tests); + RUN_TEST(contract_edge_depends_on); + RUN_TEST(contract_edge_parallel_service_edges); + RUN_TEST(contract_edge_file_changes_with); +} diff --git a/tests/test_main.c b/tests/test_main.c index adaadff9b..a30964b49 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -21,6 +21,8 @@ extern void suite_str_util(void); extern void suite_platform(void); extern void suite_extraction(void); extern void suite_grammar_regression(void); +extern void suite_grammar_labels(void); +extern void suite_grammar_imports(void); extern void suite_ac(void); extern void suite_store_nodes(void); extern void suite_store_edges(void); @@ -73,6 +75,7 @@ extern void suite_ui(void); extern void suite_security(void); extern void suite_yaml(void); extern void suite_integration(void); +extern void suite_lang_contract(void); extern void suite_incremental(void); extern void suite_simhash(void); extern void suite_stack_overflow(void); @@ -93,6 +96,8 @@ int main(void) { RUN_SUITE(ac); RUN_SUITE(extraction); RUN_SUITE(grammar_regression); + RUN_SUITE(grammar_labels); + RUN_SUITE(grammar_imports); /* Store (M5) */ RUN_SUITE(store_nodes); @@ -196,6 +201,10 @@ int main(void) { /* Integration (end-to-end) */ RUN_SUITE(integration); + + /* Per-language graph contracts (node/edge types, attribution, no-crash) */ + RUN_SUITE(lang_contract); + RUN_SUITE(incremental); /* Release sqlite3 internal caches so ASan doesn't report them as leaks */