Files
codebase-memory-mcp/tests/test_grammar_probe_e.c
T
2026-10-28 07:37:07 +01:00

1795 lines
92 KiB
C

/*
* test_grammar_probe_e.c — IDL/template node/import probe.
*
* SCOPE
* ─────
* IDL / schema languages:
* protobuf (.proto), thrift (.thrift), capnp (.capnp), wit (.wit),
* smithy (.smithy), prisma (.prisma), graphql (.graphql / .gql)
*
* Template / component languages:
* vue (.vue), svelte (.svelte), astro (.astro),
* blade (.blade.php), liquid (.liquid), jinja2 (.j2 / .jinja2),
* gotemplate (.gotmpl / .tmpl), mermaid (.mmd / .mermaid), jsdoc (.jsdoc)
*
* WHAT IS PROBED
* ──────────────
* • NODE creation — type-like nodes (Class/Struct/Interface/Enum) for IDL
* languages; Module-only for template/markup grammars.
* • IMPORTS edges — cross-file import/include/use where the grammar
* supports it. Two-file fixtures are used where
* applicable. Template grammars without a file-level
* import mechanism are tested for no-crash instead.
*
* COLOUR LEGEND
* ─────────────
* GREEN = guard: the pipeline already produces the correct result; a
* failure here is a real regression.
* RED = bug reproduction: the pipeline does NOT yet produce the expected
* node/edge. Brief inline comments identify the root-cause class.
*
* CALLS coverage is intentionally omitted (IDL/template languages have no
* in-language function calls to resolve; templates are covered by P5 breadth
* where applicable).
*
* Do NOT register this suite in test_main.c — a sibling agent owns that file.
*
* SUITE(grammar_probe_e)
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "cbm.h"
#include "resolver_quality.h"
#include <mcp/mcp.h>
#include <store/store.h>
#include <pipeline/pipeline.h>
#include <foundation/log.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
/* ══════════════════════════════════════════════════════════════════
* Harness — mirrors test_grammar_probe_a.c exactly.
* Prefix "gpe_" to avoid symbol collisions with sibling probe files.
* ══════════════════════════════════════════════════════════════════ */
typedef struct {
char tmpdir[256];
char dbpath[512];
char *project;
cbm_mcp_server_t *srv;
} GpeProj;
typedef struct {
const char *name; /* relative filename, may include '/' for subdirs */
const char *content;
} GpeFile;
static void gpe_to_fwd_slashes(char *p) {
for (; *p; p++) {
if (*p == '\\')
*p = '/';
}
}
static cbm_store_t *gpe_open_indexed(GpeProj *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);
}
static cbm_store_t *gpe_index_files(GpeProj *lp, const GpeFile *files, int nfiles) {
memset(lp, 0, sizeof(*lp));
snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_gpe_XXXXXX");
if (!cbm_mkdtemp(lp->tmpdir))
return NULL;
gpe_to_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);
char *slash = strrchr(path, '/');
if (slash && slash > path + 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);
}
return gpe_open_indexed(lp);
}
static void gpe_cleanup(GpeProj *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);
unlink(wal);
snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath);
unlink(shm);
}
/* ── Node-count helpers ─────────────────────────────────────────── */
static int gpe_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;
}
/* Sum of all type-like labels. */
static int gpe_type_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 = gpe_count_label(store, project, labels[i]);
if (n > 0)
total += n;
}
return total;
}
/* Metrics bundled per index pass. */
typedef struct {
int ok;
int total_nodes;
int functions;
int methods;
int types; /* type-like sum */
int imports; /* IMPORTS edges */
int inherits; /* INHERITS edges */
} GpeMetrics;
static GpeMetrics gpe_metrics_files(const GpeFile *files, int nfiles) {
GpeProj lp;
cbm_store_t *store = gpe_index_files(&lp, files, nfiles);
GpeMetrics m = {0};
if (store) {
m.ok = 1;
m.total_nodes = cbm_store_count_nodes(store, lp.project);
m.functions = gpe_count_label(store, lp.project, "Function");
m.methods = gpe_count_label(store, lp.project, "Method");
m.types = gpe_type_nodes(store, lp.project);
m.imports = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS");
m.inherits = cbm_store_count_edges_by_type(store, lp.project, "INHERITS");
}
gpe_cleanup(&lp, store);
return m;
}
static GpeMetrics gpe_metrics(const char *filename, const char *content) {
GpeFile f = {filename, content};
return gpe_metrics_files(&f, 1);
}
/* Full production indexing, a counted source-authored import obligation,
* and a required exact ESM call keep an all-gap fixture from passing vacuously. */
static bool gpe_import_quality(const GpeFile *files, size_t file_count, const RQ_Source *inputs,
size_t source_count, const char *suite, const RQ_Expected *gap,
const RQ_Forbidden *forbidden, RQ_Report *report) {
if (file_count > 7 || source_count > 7)
return false;
const GpeFile control = {
"quality_control.js",
"export function target() { return 1; }\nexport function caller() { return target(); }\n"};
GpeFile indexed[8];
RQ_Source sources[8];
memcpy(indexed, files, file_count * sizeof(*files));
memcpy(sources, inputs, source_count * sizeof(*inputs));
indexed[file_count++] = control;
sources[source_count++] = (RQ_Source){.path = control.name, .text = control.content};
const RQ_Expected expected[] = {
*gap,
{.case_id = "required-esm-local-call",
.site = {.file = "quality_control.js",
.caller = "quality_control.caller",
.needle = "target()",
.occurrence_index = 1},
.relation = "CALLS",
.target = {.file = "quality_control.js",
.suffix = "quality_control.target",
.label = "Function",
.declaration_line = 1,
.declaration_text = "export function target() { return 1; }"},
.mode = RQ_REQUIRED}};
GpeProj lp;
cbm_store_t *store = gpe_index_files(&lp, indexed, (int)file_count);
if (!store) {
gpe_cleanup(&lp, store);
return false;
}
bool ok =
rq_check_project_declarations(store, lp.project, suite, sources, source_count, expected, 2,
forbidden, forbidden ? 1 : 0, report, stderr);
gpe_cleanup(&lp, store);
return ok;
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 1 — Protobuf (.proto)
*
* Label histogram (test_grammar_labels.c): Class:1, Module:1
* A `message` definition → Class node.
* `enum` — whether it also produces a Class node is not confirmed; the
* histogram shows Class:1 for a single-message fixture, so a second Class
* for an enum would require a fixture with both message + enum.
* `service` — the label histogram does not show a Function node for a
* service method in the single-message fixture; service methods are
* extractable as Class/Function depending on extractor policy.
* `import` — Protobuf `import "other.proto"` is not resolved into IMPORTS
* graph edges; single-pass grammar-only extractor has no proto import
* resolver.
* ══════════════════════════════════════════════════════════════════ */
/* Protobuf: single message definition → at least 1 Class node. */
TEST(probe_proto_message_node) {
GpeMetrics m = gpe_metrics("user.proto", "syntax = \"proto3\";\n"
"package user;\n"
"\n"
"message User {\n"
" int64 id = 1;\n"
" string name = 2;\n"
" string email = 3;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: the `message` definition must produce a Class node. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Protobuf: service definition — whether the service itself or its RPC
* methods produce extractable nodes. */
TEST(probe_proto_service_node) {
GpeMetrics m = gpe_metrics("svc.proto", "syntax = \"proto3\";\n"
"package svc;\n"
"\n"
"message Req { string query = 1; }\n"
"message Resp { string result = 1; }\n"
"\n"
"service SearchService {\n"
" rpc Search(Req) returns (Resp);\n"
" rpc Suggest(Req) returns (Resp);\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least the two message defs must produce Class nodes. */
ASSERT_TRUE(m.types >= 2);
PASS();
}
/* Protobuf: enum definition — confirm enum maps to a Class-like node. */
TEST(probe_proto_enum_node) {
GpeMetrics m = gpe_metrics("status.proto", "syntax = \"proto3\";\n"
"\n"
"enum Status {\n"
" STATUS_UNKNOWN = 0;\n"
" STATUS_ACTIVE = 1;\n"
" STATUS_INACTIVE = 2;\n"
"}\n"
"\n"
"message Item {\n"
" int64 id = 1;\n"
" Status status = 2;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at minimum the message must produce a Class node; enum may add
* another — total >= 1 is the safe floor. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Exact source import: retained as a named resolver-quality obligation. */
TEST(probe_proto_import_edge) {
static const GpeFile files[] = {{"common.proto", "syntax = \"proto3\";\n"
"package common;\n"
"message Timestamp { int64 seconds = 1; }\n"},
{"event.proto", "syntax = \"proto3\";\n"
"package event;\n"
"import \"common.proto\";\n"
"message Event {\n"
" string id = 1;\n"
" common.Timestamp created_at = 2;\n"
"}\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* Keep the original fixture above. The source/configuration below makes
* this import obligation explicit without guessing ambient build roots. */
const GpeFile configured[] = {
{"configured/common.proto",
"syntax = \"proto3\";\npackage common;\nmessage Timestamp { int64 seconds = 1; }\n"},
{"configured/event.proto",
"syntax = \"proto3\";\npackage event;\nimport \"common.proto\";\nmessage Event {\n "
"string id = 1;\n common.Timestamp created_at = 2;\n}\n"},
{"configured/compile.sh", "#!/bin/sh\nset -eu\ncd \"$(dirname \"$0\")\"\nexec protoc "
"--proto_path=. --descriptor_set_out=event.pb event.proto\n"},
{"configured/decoy.proto",
"syntax = \"proto3\";\npackage decoy;\nmessage Timestamp { int64 seconds = 1; }\n"}};
const RQ_Source sources[] = {{.path = configured[0].name, .text = configured[0].content},
{.path = configured[1].name, .text = configured[1].content},
{.path = configured[2].name, .text = configured[2].content},
{.path = configured[3].name, .text = configured[3].content}};
const RQ_Expected gap = {
.case_id = "probe_proto_import_edge-exact-configured-import",
.site = {.file = "configured/event.proto",
.caller = "configured.event.proto.__file__",
.needle = "import \"common.proto\";"},
.relation = "IMPORTS",
.target = {.file = "configured/common.proto",
.suffix = "configured.common.proto.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "syntax = \"proto3\";"},
.mode = RQ_KNOWN_GAP,
.gap_id = "protobuf-configured-include-domain",
.why = "The separate launcher pins protoc to its own directory and --proto_path=.; the "
"import selects configured/common.proto, not a same-named message in another file. "
"No audited Protobuf import adapter exists.",
.tried = "Original fixture bytes and node/index checks are preserved. The separate exact "
"source/configuration obligation, wrong-target control, and required ESM call use "
"the full production pipeline; no source or launcher is executed by this test."};
const RQ_Forbidden forbidden = {.case_id = "probe_proto_import_edge-wrong-target",
.site = {.file = "configured/event.proto",
.caller = "configured.event.proto.__file__",
.needle = "import \"common.proto\";"},
.relation = "IMPORTS",
.target = {.file = "configured/decoy.proto",
.suffix = "configured.decoy.proto.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "syntax = \"proto3\";"}};
RQ_Report report;
ASSERT_TRUE(gpe_import_quality(configured, sizeof(configured) / sizeof(configured[0]), sources,
sizeof(sources) / sizeof(sources[0]), "probe_proto_import_edge",
&gap, &forbidden, &report));
ASSERT_EQ(report.genuine, 2); /* exact import + required supported control */
ASSERT_EQ(report.found, 1);
ASSERT_EQ(report.missing, 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 2 — Thrift (.thrift)
*
* Label histogram: Function:1, Module:1
* `service Foo { void ping() }` → Function node (the method).
* `struct` — the histogram shows no Struct/Class node for a service-only
* fixture; struct extraction may not be implemented.
* `include` — Thrift uses `include "other.thrift"` for imports; not
* resolved by the grammar-only pipeline.
* ══════════════════════════════════════════════════════════════════ */
/* Thrift: service method → at least 1 Function node. */
TEST(probe_thrift_service_function) {
GpeMetrics m = gpe_metrics("calc.thrift", "namespace go calc\n"
"\n"
"service Calculator {\n"
" i32 add(1: i32 a, 2: i32 b)\n"
" i32 subtract(1: i32 a, 2: i32 b)\n"
" i32 multiply(1: i32 a, 2: i32 b)\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least one service method must appear as a Function node. */
ASSERT_TRUE(m.functions >= 1);
PASS();
}
/* Thrift: struct definition — does the extractor produce a type-like node?
* RED: histogram shows Function:1/Module:1 for service-only fixtures; struct
* extraction is not confirmed and may not be implemented in the grammar
* extractor. */
TEST(probe_thrift_struct_node) {
GpeMetrics m = gpe_metrics("types.thrift", "namespace go types\n"
"\n"
"struct User {\n"
" 1: required i64 id\n"
" 2: required string name\n"
" 3: optional string email\n"
"}\n"
"\n"
"struct Address {\n"
" 1: required string street\n"
" 2: required string city\n"
"}\n");
ASSERT_TRUE(m.ok);
ASSERT_TRUE(m.total_nodes >= 1); /* GREEN: at minimum Module node */
/* RED: Thrift struct not yet extracted as a Struct/Class node. */
ASSERT_TRUE(m.types >= 1); /* expected RED — no Thrift struct extractor */
PASS();
}
/* Thrift: exception definition — a special struct-like type. */
TEST(probe_thrift_exception_node) {
GpeMetrics m = gpe_metrics("errors.thrift", "namespace go errors\n"
"\n"
"exception NotFound {\n"
" 1: required string message\n"
"}\n"
"\n"
"exception Unauthorized {\n"
" 1: required string reason\n"
"}\n"
"\n"
"service Auth {\n"
" void login(1: string user, 2: string pass)\n"
" throws (1: Unauthorized e)\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least the service method must appear as a Function node. */
ASSERT_TRUE(m.functions >= 1);
PASS();
}
/* Exact source import: retained as a named resolver-quality obligation. */
TEST(probe_thrift_include_edge) {
static const GpeFile files[] = {{"common.thrift",
"namespace go common\n"
"struct Timestamp { 1: required i64 seconds }\n"},
{"service.thrift", "namespace go service\n"
"include \"common.thrift\"\n"
"struct Event {\n"
" 1: required string id\n"
" 2: required common.Timestamp created_at\n"
"}\n"
"service EventService {\n"
" Event getEvent(1: string id)\n"
"}\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* Keep the original fixture above. The source/configuration below makes
* this import obligation explicit without guessing ambient build roots. */
const GpeFile configured[] = {
{"configured/common.thrift",
"namespace go common\nstruct Timestamp { 1: required i64 seconds }\n"},
{"configured/service.thrift",
"namespace go service\ninclude \"common.thrift\"\nstruct Event {\n 1: required string "
"id\n 2: required common.Timestamp created_at\n}\nservice EventService {\n Event "
"getEvent(1: string id)\n}\n"},
{"configured/compile.sh", "#!/bin/sh\nset -eu\ncd \"$(dirname \"$0\")\"\nexec thrift -I . "
"--gen cpp service.thrift\n"},
{"configured/decoy.thrift",
"namespace go decoy\nstruct Timestamp { 1: required i64 seconds }\n"}};
const RQ_Source sources[] = {{.path = configured[0].name, .text = configured[0].content},
{.path = configured[1].name, .text = configured[1].content},
{.path = configured[2].name, .text = configured[2].content},
{.path = configured[3].name, .text = configured[3].content}};
const RQ_Expected gap = {
.case_id = "probe_thrift_include_edge-exact-configured-import",
.site = {.file = "configured/service.thrift",
.caller = "configured.service.thrift.__file__",
.needle = "include \"common.thrift\""},
.relation = "IMPORTS",
.target = {.file = "configured/common.thrift",
.suffix = "configured.common.thrift.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "namespace go common"},
.mode = RQ_KNOWN_GAP,
.gap_id = "thrift-configured-include-domain",
.why = "The separate launcher pins the source directory and -I .; common.thrift is the "
"exact included file. The decoy has the same type leaf but is not included. No "
"audited Thrift include adapter exists.",
.tried = "Original fixture bytes and node/index checks are preserved. The separate exact "
"source/configuration obligation, wrong-target control, and required ESM call use "
"the full production pipeline; no source or launcher is executed by this test."};
const RQ_Forbidden forbidden = {.case_id = "probe_thrift_include_edge-wrong-target",
.site = {.file = "configured/service.thrift",
.caller = "configured.service.thrift.__file__",
.needle = "include \"common.thrift\""},
.relation = "IMPORTS",
.target = {.file = "configured/decoy.thrift",
.suffix = "configured.decoy.thrift.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "namespace go decoy"}};
RQ_Report report;
ASSERT_TRUE(gpe_import_quality(configured, sizeof(configured) / sizeof(configured[0]), sources,
sizeof(sources) / sizeof(sources[0]),
"probe_thrift_include_edge", &gap, &forbidden, &report));
ASSERT_EQ(report.genuine, 2); /* exact import + required supported control */
ASSERT_EQ(report.found, 1);
ASSERT_EQ(report.missing, 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 3 — Cap'n Proto (.capnp)
*
* Label histogram: Class:1, Module:1
* `struct` → Class node.
* `interface` — may also produce a Class node (or Interface node).
* `import` with `@0x...` annotation — Capnp uses `using Import = import
* "other.capnp";`; not resolved by the grammar-only pipeline.
* ══════════════════════════════════════════════════════════════════ */
/* Cap'n Proto: struct definition → at least 1 Class node. */
TEST(probe_capnp_struct_node) {
GpeMetrics m = gpe_metrics("schema.capnp", "@0xdbb9ad1f14bf0b36;\n"
"\n"
"struct Person {\n"
" id @0 :UInt32;\n"
" name @1 :Text;\n"
" email @2 :Text;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: the struct must produce a Class node. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Cap'n Proto: interface definition → Class (or Interface) node. */
TEST(probe_capnp_interface_node) {
GpeMetrics m = gpe_metrics("service.capnp", "@0xabc123def456789a;\n"
"\n"
"struct Request { query @0 :Text; }\n"
"struct Response { result @0 :Text; }\n"
"\n"
"interface SearchService {\n"
" search @0 (req :Request) -> (resp :Response);\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at minimum the two struct definitions must produce Class nodes. */
ASSERT_TRUE(m.types >= 2);
PASS();
}
/* Cap'n Proto: enum definition. */
TEST(probe_capnp_enum_node) {
GpeMetrics m = gpe_metrics("enums.capnp", "@0x1234567890abcdef;\n"
"\n"
"enum Color {\n"
" red @0;\n"
" green @1;\n"
" blue @2;\n"
"}\n"
"\n"
"struct Widget {\n"
" color @0 :Color;\n"
" label @1 :Text;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: the struct must produce a Class node; enum may add another. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Cap'n Proto: two-file import → IMPORTS edge.
* RED: grammar-only Cap'n Proto has no import-resolver pass in the pipeline. */
TEST(probe_capnp_import_edge) {
static const GpeFile files[] = {{"common.capnp", "@0xaabbccddeeff0011;\n"
"struct Timestamp { seconds @0 :Int64; }\n"},
{"event.capnp", "@0x1122334455667788;\n"
"using Common = import \"common.capnp\";\n"
"struct Event {\n"
" id @0 :Text;\n"
" createdAt @1 :Common.Timestamp;\n"
"}\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* RED: Cap'n Proto `import` not resolved into IMPORTS edges. */
ASSERT_TRUE(m.imports >= 1); /* expected RED — no capnp import resolver */
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 4 — WIT (.wit)
*
* Label histogram: Class:2, Function:1, Module:1
* `record` → Class node; `enum` → Class node; `func` → Function node.
* `world` and `interface` container constructs; the histogram's Class:2
* comes from record + enum inside an interface block.
* `use` — WIT uses `use pkg:component/iface.{Type}` for imports;
* not resolved by the grammar-only pipeline.
* ══════════════════════════════════════════════════════════════════ */
/* WIT: record and enum inside an interface → 2 Class nodes + 1 Function. */
TEST(probe_wit_record_enum_function) {
GpeMetrics m = gpe_metrics("types.wit", "interface geometry {\n"
" record point { x: u32, y: u32 }\n"
" enum color { red, green, blue }\n"
" area: func() -> u32;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: record → Class, enum → Class, func → Function.
* Histogram confirms Class:2, Function:1 for this pattern. */
ASSERT_TRUE(m.types >= 2);
ASSERT_TRUE(m.functions >= 1);
PASS();
}
/* WIT: world definition as a top-level construct. */
TEST(probe_wit_world_node) {
GpeMetrics m = gpe_metrics("world.wit", "world calculator {\n"
" export add: func(a: u32, b: u32) -> u32;\n"
" export subtract: func(a: u32, b: u32) -> u32;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* REAL BUG (class 16 node-extraction incompleteness): `func`s declared
* inside a WIT `world { export X: func ... }` block are NOT extracted as
* Function nodes, whereas `interface`-scoped funcs ARE (see passing
* probe_wit_record_enum_function and grammar_labels wit=Function:1).
* The extractor's WIT func handling covers interface members but not
* world exports. Root cause: extract_defs.c WIT walk / lang_specs.c
* wit func_node_types scope. RED. */
ASSERT_TRUE(m.functions >= 1); /* REAL BUG — world-scoped funcs not extracted */
PASS();
}
/* WIT: type alias definition. */
TEST(probe_wit_type_alias) {
GpeMetrics m = gpe_metrics("alias.wit", "interface types {\n"
" type error-code = u32;\n"
" record error { code: error-code, msg: string }\n"
" get-error: func(code: error-code) -> error;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: the record → Class, func → Function. */
ASSERT_TRUE(m.types >= 1);
ASSERT_TRUE(m.functions >= 1);
PASS();
}
/* WIT: variant definition (tagged union). */
TEST(probe_wit_variant_node) {
GpeMetrics m = gpe_metrics("result.wit", "interface result-types {\n"
" variant result {\n"
" ok(u32),\n"
" err(string),\n"
" }\n"
" unwrap: func(r: result) -> u32;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at minimum the function must appear; variant may add a Class. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Exact source import: retained as a named resolver-quality obligation. */
TEST(probe_wit_use_edge) {
static const GpeFile files[] = {{"types.wit", "interface types {\n"
" record point { x: u32, y: u32 }\n"
"}\n"},
{"geometry.wit",
"interface geometry {\n"
" use types.{point};\n"
" distance: func(a: point, b: point) -> u32;\n"
"}\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* Keep the original fixture above. The source/configuration below makes
* this import obligation explicit without guessing ambient build roots. */
const GpeFile configured[] = {
{"configured/types.wit", "interface types {\n record point { x: u32, y: u32 }\n}\n"},
{"configured/geometry.wit", "interface geometry {\n use types.{point};\n distance: "
"func(a: point, b: point) -> u32;\n}\n"},
{"configured/package.wit", "package quality:geometry;\n"},
{"configured/decoy.wit", "interface other {\n record point { x: u32, y: u32 }\n}\n"}};
const RQ_Source sources[] = {{.path = configured[0].name, .text = configured[0].content},
{.path = configured[1].name, .text = configured[1].content},
{.path = configured[2].name, .text = configured[2].content},
{.path = configured[3].name, .text = configured[3].content}};
const RQ_Expected gap = {
.case_id = "probe_wit_use_edge-exact-configured-import",
.site = {.file = "configured/geometry.wit",
.caller = "configured.geometry.wit.__file__",
.needle = "use types.{point};"},
.relation = "IMPORTS",
.target = {.file = "configured/types.wit",
.suffix = "configured.types.wit.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "interface types {"},
.mode = RQ_KNOWN_GAP,
.gap_id = "wit-package-interface-use-domain",
.why = "A peer package declaration explicitly groups the configured WIT files. The use "
"names interface types and its point record; the point in interface other is not "
"that declaration. No audited WIT import adapter exists.",
.tried = "Original fixture bytes and node/index checks are preserved. The separate exact "
"source/configuration obligation, wrong-target control, and required ESM call use "
"the full production pipeline; no source or launcher is executed by this test."};
const RQ_Forbidden forbidden = {.case_id = "probe_wit_use_edge-wrong-target",
.site = {.file = "configured/geometry.wit",
.caller = "configured.geometry.wit.__file__",
.needle = "use types.{point};"},
.relation = "IMPORTS",
.target = {.file = "configured/decoy.wit",
.suffix = "configured.decoy.wit.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "interface other {"}};
RQ_Report report;
ASSERT_TRUE(gpe_import_quality(configured, sizeof(configured) / sizeof(configured[0]), sources,
sizeof(sources) / sizeof(sources[0]), "probe_wit_use_edge", &gap,
&forbidden, &report));
ASSERT_EQ(report.genuine, 2); /* exact import + required supported control */
ASSERT_EQ(report.found, 1);
ASSERT_EQ(report.missing, 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 5 — Smithy (.smithy)
*
* Label histogram: Class:1, Module:1
* `structure` → Class node (confirmed by grammar_regression: Foo → Class).
* `service` and `operation` — may produce additional Class nodes.
* `use` — Smithy uses `use com.example#Shape` for cross-namespace imports;
* not resolved by the grammar-only pipeline.
* ══════════════════════════════════════════════════════════════════ */
/* Smithy: structure definition → at least 1 Class node. */
TEST(probe_smithy_structure_node) {
/* Fixture fix: valid Smithy ordering is control section ($version) FIRST,
* then namespace, then shapes. The original fixture had `namespace`
* before `$version`, which is invalid Smithy and failed to parse → 0
* nodes. Mirrors the working test_grammar_regression.c smithy fixture
* (namespace, then structure) which yields Class:1. */
GpeMetrics m = gpe_metrics("model.smithy", "$version: \"2\"\n"
"\n"
"namespace com.example\n"
"\n"
"structure User {\n"
" @required\n"
" id: String\n"
" name: String\n"
" email: String\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: structure → Class node. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Smithy: service + operation definitions. */
TEST(probe_smithy_service_node) {
GpeMetrics m =
gpe_metrics("service.smithy", "namespace com.example\n"
"\n"
"service UserService {\n"
" version: \"2024-01-01\"\n"
" operations: [GetUser, CreateUser]\n"
"}\n"
"\n"
"operation GetUser {\n"
" input: GetUserInput\n"
" output: GetUserOutput\n"
"}\n"
"\n"
"operation CreateUser {\n"
" input: CreateUserInput\n"
"}\n"
"\n"
"structure GetUserInput { @required id: String }\n"
"structure GetUserOutput { id: String, name: String }\n"
"structure CreateUserInput { @required name: String }\n");
ASSERT_TRUE(m.ok);
/* GREEN: at minimum the structure definitions must produce Class nodes. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Smithy: enum definition. */
TEST(probe_smithy_enum_node) {
GpeMetrics m = gpe_metrics("enum.smithy", "namespace com.example\n"
"\n"
"enum Status {\n"
" ACTIVE = \"active\"\n"
" INACTIVE = \"inactive\"\n"
" PENDING = \"pending\"\n"
"}\n"
"\n"
"structure Item {\n"
" id: String\n"
" status: Status\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: structure → Class; enum may produce another type node. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Exact source import: retained as a named resolver-quality obligation. */
TEST(probe_smithy_use_edge) {
static const GpeFile files[] = {{"common.smithy", "namespace com.example.common\n"
"structure Timestamp { seconds: Long }\n"},
{"events.smithy", "namespace com.example.events\n"
"use com.example.common#Timestamp\n"
"structure Event {\n"
" id: String\n"
" createdAt: Timestamp\n"
"}\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* Keep the original fixture above. The source/configuration below makes
* this import obligation explicit without guessing ambient build roots. */
const GpeFile configured[] = {
{"configured/common.smithy",
"namespace com.example.common\nstructure Timestamp { seconds: Long }\n"},
{"configured/events.smithy",
"namespace com.example.events\nuse com.example.common#Timestamp\nstructure Event {\n id: "
"String\n createdAt: Timestamp\n}\n"},
{"configured/smithy-build.json", "{\"version\":\"1.0\",\"sources\":[\"common.smithy\","
"\"events.smithy\",\"decoy.smithy\"]}\n"},
{"configured/decoy.smithy",
"namespace com.example.other\nstructure Timestamp { seconds: Long }\n"}};
const RQ_Source sources[] = {{.path = configured[0].name, .text = configured[0].content},
{.path = configured[1].name, .text = configured[1].content},
{.path = configured[2].name, .text = configured[2].content},
{.path = configured[3].name, .text = configured[3].content}};
const RQ_Expected gap = {
.case_id = "probe_smithy_use_edge-exact-configured-import",
.site = {.file = "configured/events.smithy",
.caller = "configured.events.smithy.__file__",
.needle = "use com.example.common#Timestamp"},
.relation = "IMPORTS",
.target = {.file = "configured/common.smithy",
.suffix = "configured.common.smithy.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "namespace com.example.common"},
.mode = RQ_KNOWN_GAP,
.gap_id = "smithy-configured-shape-use-domain",
.why = "smithy-build.json selects this exact source model. The absolute shape ID "
"com.example.common#Timestamp identifies the definition in common.smithy; the other "
"namespace is a different shape. No audited Smithy import adapter exists.",
.tried = "Original fixture bytes and node/index checks are preserved. The separate exact "
"source/configuration obligation, wrong-target control, and required ESM call use "
"the full production pipeline; no source or launcher is executed by this test."};
const RQ_Forbidden forbidden = {.case_id = "probe_smithy_use_edge-wrong-target",
.site = {.file = "configured/events.smithy",
.caller = "configured.events.smithy.__file__",
.needle = "use com.example.common#Timestamp"},
.relation = "IMPORTS",
.target = {.file = "configured/decoy.smithy",
.suffix = "configured.decoy.smithy.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "namespace com.example.other"}};
RQ_Report report;
ASSERT_TRUE(gpe_import_quality(configured, sizeof(configured) / sizeof(configured[0]), sources,
sizeof(sources) / sizeof(sources[0]), "probe_smithy_use_edge",
&gap, &forbidden, &report));
ASSERT_EQ(report.genuine, 2); /* exact import + required supported control */
ASSERT_EQ(report.found, 1);
ASSERT_EQ(report.missing, 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 6 — Prisma (.prisma)
*
* Label histogram: Class:1, Module:1
* `model` → Class node.
* `enum` — may produce a second Class node.
* Prisma schema files are single-file; there is no cross-file import
* mechanism in the Prisma schema language itself (all models live in
* one schema.prisma or are merged via --schema flag, not `import`).
* ══════════════════════════════════════════════════════════════════ */
/* Prisma: model definition → at least 1 Class node. */
TEST(probe_prisma_model_node) {
GpeMetrics m = gpe_metrics("schema.prisma", "generator client {\n"
" provider = \"prisma-client-js\"\n"
"}\n"
"\n"
"datasource db {\n"
" provider = \"postgresql\"\n"
" url = env(\"DATABASE_URL\")\n"
"}\n"
"\n"
"model User {\n"
" id Int @id @default(autoincrement())\n"
" name String\n"
" email String @unique\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: `model User` must produce a Class node. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Prisma: multiple model definitions → multiple Class nodes. */
TEST(probe_prisma_multiple_models) {
GpeMetrics m = gpe_metrics("blog.prisma",
"model User {\n"
" id Int @id @default(autoincrement())\n"
" name String\n"
" posts Post[]\n"
"}\n"
"\n"
"model Post {\n"
" id Int @id @default(autoincrement())\n"
" title String\n"
" content String\n"
" authorId Int\n"
" author User @relation(fields: [authorId], references: [id])\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: two model definitions → at least 2 Class nodes. */
ASSERT_TRUE(m.types >= 2);
PASS();
}
/* Prisma: enum definition — does it yield an additional type node? */
TEST(probe_prisma_enum_node) {
GpeMetrics m = gpe_metrics("status.prisma", "enum Role {\n"
" USER\n"
" ADMIN\n"
" MODERATOR\n"
"}\n"
"\n"
"model Account {\n"
" id Int @id @default(autoincrement())\n"
" role Role @default(USER)\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: model → Class; enum may add another — floor is >= 1. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* Prisma: no-crash for a datasource-only schema (no models). */
TEST(probe_prisma_datasource_only) {
GpeMetrics m = gpe_metrics("config.prisma", "generator client {\n"
" provider = \"prisma-client-js\"\n"
"}\n"
"\n"
"datasource db {\n"
" provider = \"sqlite\"\n"
" url = \"file:./dev.db\"\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash on a schema with no models. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 7 — GraphQL (.graphql / .gql)
*
* Label histogram: Class:1, Module:1
* `type` definition → Class node.
* `input` and `interface` types — may also produce Class nodes.
* GraphQL schema files are typically standalone; cross-file imports are
* not part of the SDL (Schema Definition Language); no IMPORTS edges
* are expected from the grammar-only pipeline.
* ══════════════════════════════════════════════════════════════════ */
/* GraphQL: type definition → at least 1 Class node. */
TEST(probe_graphql_type_node) {
GpeMetrics m = gpe_metrics("schema.graphql", "type User {\n"
" id: ID!\n"
" name: String!\n"
" email: String!\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: `type User` must produce a Class node. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* GraphQL: multiple type definitions → multiple Class nodes. */
TEST(probe_graphql_multiple_types) {
GpeMetrics m = gpe_metrics("schema.gql", "type Query {\n"
" user(id: ID!): User\n"
" users: [User!]!\n"
"}\n"
"\n"
"type User {\n"
" id: ID!\n"
" name: String!\n"
" posts: [Post!]!\n"
"}\n"
"\n"
"type Post {\n"
" id: ID!\n"
" title: String!\n"
" content: String!\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: 3 type definitions → at least 2 Class nodes. */
ASSERT_TRUE(m.types >= 2);
PASS();
}
/* GraphQL: interface definition → type-like node. */
TEST(probe_graphql_interface_node) {
GpeMetrics m = gpe_metrics("iface.graphql", "interface Node {\n"
" id: ID!\n"
"}\n"
"\n"
"type User implements Node {\n"
" id: ID!\n"
" name: String!\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at minimum `type User` → Class; `interface Node` may add one. */
ASSERT_TRUE(m.types >= 1);
PASS();
}
/* GraphQL: input type definition. */
TEST(probe_graphql_input_type) {
GpeMetrics m = gpe_metrics("mutations.graphql", "input CreateUserInput {\n"
" name: String!\n"
" email: String!\n"
"}\n"
"\n"
"type Mutation {\n"
" createUser(input: CreateUserInput!): User!\n"
"}\n"
"\n"
"type User {\n"
" id: ID!\n"
" name: String!\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least two type-like nodes expected (input + type + type). */
ASSERT_TRUE(m.types >= 2);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 8 — Vue (.vue)
*
* Label histogram: Module:1 only — no Class or Function nodes from the
* Vue component itself at the graph level.
* IMPORTS: `import` statements inside <script> blocks ARE extracted at the
* extraction level (test_grammar_imports confirms 2 imports for a vue
* fixture with 2 import statements). Whether these become IMPORTS graph
* edges in a two-file cross-component fixture depends on whether the
* JS/TS resolver handles .vue paths.
* ══════════════════════════════════════════════════════════════════ */
/* Vue: single-file component indexes without crash. */
TEST(probe_vue_no_crash) {
GpeMetrics m = gpe_metrics("App.vue", "<template>\n"
" <div>{{ message }}</div>\n"
"</template>\n"
"\n"
"<script setup>\n"
"import { ref } from 'vue'\n"
"const message = ref('Hello World')\n"
"</script>\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash and must produce at least 1 node. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Vue: <script setup> with two imports — extraction-level import count. */
TEST(probe_vue_script_imports) {
GpeMetrics m =
gpe_metrics("Counter.vue", "<script setup>\n"
"import { ref, computed } from 'vue'\n"
"import BaseButton from './BaseButton.vue'\n"
"\n"
"const count = ref(0)\n"
"const doubled = computed(() => count.value * 2)\n"
"</script>\n"
"\n"
"<template>\n"
" <BaseButton @click=\"count++\">{{ doubled }}</BaseButton>\n"
"</template>\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline indexes without crash. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Vue: the source-defined component dependency is a counted embedded-proof gap. */
TEST(probe_vue_cross_component_import) {
static const GpeFile files[] = {
{"BaseButton.vue", "<template><button><slot /></button></template>\n"
"<script setup>\n"
"defineProps({ label: String })\n"
"</script>\n"},
{"App.vue", "<script setup>\n"
"import BaseButton from './BaseButton.vue'\n"
"</script>\n"
"<template><BaseButton label=\"Click\" /></template>\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
const GpeFile quality_files[] = {
files[0], files[1], {"decoy/BaseButton.vue", files[0].content}};
RQ_Source sources[sizeof(quality_files) / sizeof(quality_files[0])];
for (size_t i = 0; i < sizeof(sources) / sizeof(sources[0]); i++)
sources[i] = (RQ_Source){.path = quality_files[i].name, .text = quality_files[i].content};
const RQ_Expected expected = {
.case_id = "probe_vue_cross_component_import-exact-dependency",
.site = {.file = "App.vue",
.caller = "App.vue.__file__",
.needle = "import BaseButton from './BaseButton.vue'"},
.relation = "IMPORTS",
.target = {.file = "BaseButton.vue",
.suffix = "BaseButton.vue.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "<template><button><slot /></button></template>"},
.mode = RQ_KNOWN_GAP,
.gap_id = "vue-embedded-component-import-proof",
.why = "Vue script imports need an audited embedded-source origin and raw literal proof.",
.tried = "Preserved original sources and indexing checks; exact target, wrong-target "
"control, and required ESM call use full production indexing."};
const RQ_Forbidden forbidden = {
.case_id = "probe_vue_cross_component_import-invalid-target",
.site = {.file = "App.vue",
.caller = "App.vue.__file__",
.needle = "import BaseButton from './BaseButton.vue'"},
.relation = "IMPORTS",
.target = {.file = "decoy/BaseButton.vue",
.suffix = "decoy.BaseButton.vue.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "<template><button><slot /></button></template>"}};
RQ_Report report;
ASSERT_TRUE(gpe_import_quality(quality_files, sizeof(quality_files) / sizeof(quality_files[0]),
sources, sizeof(sources) / sizeof(sources[0]),
"probe_vue_cross_component_import", &expected, &forbidden,
&report));
ASSERT_EQ(report.genuine, 2);
ASSERT_EQ(report.found, 1);
ASSERT_EQ(report.missing, 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 9 — Svelte (.svelte)
*
* Label histogram: Module:1 only.
* IMPORTS: <script> imports ARE extracted at the extraction level
* (test_grammar_imports confirms 2 imports for a svelte fixture).
* Cross-component IMPORTS edges share the same question as Vue.
* ══════════════════════════════════════════════════════════════════ */
/* Svelte: single-file component indexes without crash. */
TEST(probe_svelte_no_crash) {
GpeMetrics m = gpe_metrics("App.svelte", "<script>\n"
" let name = 'World';\n"
" function greet() { return `Hello ${name}`; }\n"
"</script>\n"
"<h1>{greet()}</h1>\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash and must produce at least 1 node. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Svelte: <script> with imports — extraction-level behavior. */
TEST(probe_svelte_script_imports) {
GpeMetrics m =
gpe_metrics("Counter.svelte", "<script>\n"
" import { onMount } from 'svelte';\n"
" import Button from './Button.svelte';\n"
"\n"
" let count = 0;\n"
" onMount(() => { count = 1; });\n"
"</script>\n"
"<Button on:click={() => count++}>{count}</Button>\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline indexes without crash. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Svelte: the exact component dependency is a counted embedded-proof gap. */
TEST(probe_svelte_cross_component_import) {
static const GpeFile files[] = {{"Button.svelte", "<script>\n"
" export let label = 'Click';\n"
"</script>\n"
"<button>{label}</button>\n"},
{"App.svelte", "<script>\n"
" import Button from './Button.svelte';\n"
"</script>\n"
"<Button label=\"Go\" />\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
const GpeFile quality_files[] = {files[0], files[1], {"decoy/Button.svelte", files[0].content}};
RQ_Source sources[sizeof(quality_files) / sizeof(quality_files[0])];
for (size_t i = 0; i < sizeof(sources) / sizeof(sources[0]); i++)
sources[i] = (RQ_Source){.path = quality_files[i].name, .text = quality_files[i].content};
const RQ_Expected expected = {
.case_id = "probe_svelte_cross_component_import-exact-dependency",
.site = {.file = "App.svelte",
.caller = "App.svelte.__file__",
.needle = "import Button from './Button.svelte';"},
.relation = "IMPORTS",
.target = {.file = "Button.svelte",
.suffix = "Button.svelte.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "<script>"},
.mode = RQ_KNOWN_GAP,
.gap_id = "svelte-embedded-component-import-proof",
.why =
"Svelte script imports need an audited embedded-source origin and raw literal proof.",
.tried = "Preserved original sources and indexing checks; exact target, wrong-target "
"control, and required ESM call use full production indexing."};
const RQ_Forbidden forbidden = {.case_id = "probe_svelte_cross_component_import-invalid-target",
.site = {.file = "App.svelte",
.caller = "App.svelte.__file__",
.needle = "import Button from './Button.svelte';"},
.relation = "IMPORTS",
.target = {.file = "decoy/Button.svelte",
.suffix = "decoy.Button.svelte.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "<script>"}};
RQ_Report report;
ASSERT_TRUE(gpe_import_quality(quality_files, sizeof(quality_files) / sizeof(quality_files[0]),
sources, sizeof(sources) / sizeof(sources[0]),
"probe_svelte_cross_component_import", &expected, &forbidden,
&report));
ASSERT_EQ(report.genuine, 2);
ASSERT_EQ(report.found, 1);
ASSERT_EQ(report.missing, 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 10 — Astro (.astro)
*
* Label histogram: Module:1 only.
* IMPORTS: frontmatter imports ARE extracted at the extraction level
* (test_grammar_imports confirms 2 imports for an astro fixture).
* Cross-component IMPORTS edges share the same question as Vue/Svelte.
* ══════════════════════════════════════════════════════════════════ */
/* Astro: single-file component indexes without crash. */
TEST(probe_astro_no_crash) {
GpeMetrics m = gpe_metrics("index.astro", "---\n"
"const greeting = 'Hello World';\n"
"---\n"
"<html>\n"
" <body><h1>{greeting}</h1></body>\n"
"</html>\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash and must produce at least 1 node. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Astro: frontmatter with imports. */
TEST(probe_astro_frontmatter_imports) {
GpeMetrics m = gpe_metrics("Page.astro", "---\n"
"import confetti from 'canvas-confetti';\n"
"import dayjs from 'dayjs';\n"
"\n"
"confetti();\n"
"const now = dayjs().format();\n"
"---\n"
"<div>{now}</div>\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline indexes without crash. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Astro: the exact frontmatter dependency is a counted embedded-proof gap. */
TEST(probe_astro_cross_component_import) {
static const GpeFile files[] = {{"Card.astro", "---\n"
"const { title } = Astro.props;\n"
"---\n"
"<div class=\"card\"><h2>{title}</h2></div>\n"},
{"index.astro", "---\n"
"import Card from './Card.astro';\n"
"---\n"
"<Card title=\"Hello\" />\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
const GpeFile quality_files[] = {files[0], files[1], {"decoy/Card.astro", files[0].content}};
RQ_Source sources[sizeof(quality_files) / sizeof(quality_files[0])];
for (size_t i = 0; i < sizeof(sources) / sizeof(sources[0]); i++)
sources[i] = (RQ_Source){.path = quality_files[i].name, .text = quality_files[i].content};
const RQ_Expected expected = {
.case_id = "probe_astro_cross_component_import-exact-dependency",
.site = {.file = "index.astro",
.caller = "index.astro.__file__",
.needle = "import Card from './Card.astro';"},
.relation = "IMPORTS",
.target = {.file = "Card.astro",
.suffix = "Card.astro.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "---"},
.mode = RQ_KNOWN_GAP,
.gap_id = "astro-frontmatter-component-import-proof",
.why = "Astro frontmatter imports need an audited embedded-source origin and raw literal "
"proof.",
.tried = "Preserved original sources and indexing checks; exact target, wrong-target "
"control, and required ESM call use full production indexing."};
const RQ_Forbidden forbidden = {.case_id = "probe_astro_cross_component_import-invalid-target",
.site = {.file = "index.astro",
.caller = "index.astro.__file__",
.needle = "import Card from './Card.astro';"},
.relation = "IMPORTS",
.target = {.file = "decoy/Card.astro",
.suffix = "decoy.Card.astro.__file__",
.label = "File",
.declaration_line = 1,
.declaration_text = "---"}};
RQ_Report report;
ASSERT_TRUE(gpe_import_quality(quality_files, sizeof(quality_files) / sizeof(quality_files[0]),
sources, sizeof(sources) / sizeof(sources[0]),
"probe_astro_cross_component_import", &expected, &forbidden,
&report));
ASSERT_EQ(report.genuine, 2);
ASSERT_EQ(report.found, 1);
ASSERT_EQ(report.missing, 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 11 — Blade (.blade.php)
*
* Label histogram: Module:1 only.
* Blade templates use @extends / @include directives but these are not
* modeled as graph IMPORTS edges by the grammar-only pipeline.
* The .blade.php compound extension is registered via the compound
* extension table (language.c line 882).
* ══════════════════════════════════════════════════════════════════ */
/* Blade: basic template indexes without crash. */
TEST(probe_blade_no_crash) {
GpeMetrics m = gpe_metrics("welcome.blade.php", "@extends('layouts.app')\n"
"\n"
"@section('content')\n"
"<h1>Welcome</h1>\n"
"@endsection\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash and must produce at least 1 node. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Blade: @include directive — not modeled as IMPORTS. */
TEST(probe_blade_include_no_imports_edge) {
static const GpeFile files[] = {
{"partials/header.blade.php", "<header><nav>Menu</nav></header>\n"},
{"home.blade.php", "@extends('layouts.app')\n"
"@section('content')\n"
"@include('partials.header')\n"
"<main>Home</main>\n"
"@endsection\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
ASSERT_TRUE(m.total_nodes >= 1);
/* GREEN: Blade @include is not modeled as a graph IMPORTS edge. */
ASSERT_TRUE(m.imports == 0);
PASS();
}
/* Blade: conditional and loop directives — pipeline stability. */
TEST(probe_blade_directives_no_crash) {
GpeMetrics m = gpe_metrics("list.blade.php", "@extends('layouts.app')\n"
"@section('content')\n"
"@if($items->count() > 0)\n"
" @foreach($items as $item)\n"
" <div>{{ $item->name }}</div>\n"
" @endforeach\n"
"@else\n"
" <p>No items found.</p>\n"
"@endif\n"
"@endsection\n");
ASSERT_TRUE(m.ok);
/* GREEN: complex directives must not crash the pipeline. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 12 — Liquid (.liquid)
*
* Label histogram: Module:1 only.
* Liquid uses `{% render 'snippet' %}` / `{% include 'snippet' %}` but
* these are not modeled as graph IMPORTS edges.
* ══════════════════════════════════════════════════════════════════ */
/* Liquid: basic template indexes without crash. */
TEST(probe_liquid_no_crash) {
GpeMetrics m = gpe_metrics("index.liquid", "{% if customer %}\n"
" <p>Welcome, {{ customer.name }}!</p>\n"
"{% else %}\n"
" <p>Hello, visitor!</p>\n"
"{% endif %}\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash and must produce at least 1 node. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Liquid: for-loop + filters — pipeline stability. */
TEST(probe_liquid_loop_and_filters) {
GpeMetrics m = gpe_metrics("products.liquid", "{% for product in collection.products %}\n"
" <div class=\"product\">\n"
" <h2>{{ product.title | upcase }}</h2>\n"
" <p>{{ product.price | money }}</p>\n"
" </div>\n"
"{% endfor %}\n");
ASSERT_TRUE(m.ok);
/* GREEN: for-loop with filters must not crash. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Liquid: render/include — not modeled as IMPORTS. */
TEST(probe_liquid_render_no_imports_edge) {
static const GpeFile files[] = {
{"snippets/card.liquid", "<div class=\"card\">{{ card.title }}</div>\n"},
{"sections/featured.liquid", "<section>\n"
" {% render 'snippets/card', card: featured_product %}\n"
"</section>\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
ASSERT_TRUE(m.total_nodes >= 1);
/* GREEN: Liquid `render` is not modeled as a graph IMPORTS edge. */
ASSERT_TRUE(m.imports == 0);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 13 — Jinja2 (.j2 / .jinja2)
*
* Label histogram: Module:1 only.
* Jinja2 uses `{% extends "base.html" %}` / `{% include "header.html" %}`
* but these are not modeled as graph IMPORTS edges.
* ══════════════════════════════════════════════════════════════════ */
/* Jinja2: basic template indexes without crash (.j2 extension). */
TEST(probe_jinja2_no_crash_j2) {
GpeMetrics m = gpe_metrics("config.j2", "{% if env == 'production' %}\n"
"database_url: {{ db_url }}\n"
"{% else %}\n"
"database_url: sqlite:///dev.db\n"
"{% endif %}\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash on a .j2 file. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Jinja2: extends and block directives (.jinja2 extension). */
TEST(probe_jinja2_extends_no_imports_edge) {
static const GpeFile files[] = {
{"base.html.jinja2", "<!DOCTYPE html>\n"
"<html>\n"
"<head>{% block head %}{% endblock %}</head>\n"
"<body>{% block body %}{% endblock %}</body>\n"
"</html>\n"},
{"page.html.jinja2", "{% extends \"base.html.jinja2\" %}\n"
"{% block head %}<title>Page</title>{% endblock %}\n"
"{% block body %}<h1>Hello</h1>{% endblock %}\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
ASSERT_TRUE(m.total_nodes >= 1);
/* GREEN: Jinja2 `extends` is not modeled as a graph IMPORTS edge. */
ASSERT_TRUE(m.imports == 0);
PASS();
}
/* Jinja2: macro definition — does the extractor produce a node? */
TEST(probe_jinja2_macro_no_crash) {
GpeMetrics m =
gpe_metrics("macros.jinja", "{% macro render_field(field, label='') %}\n"
" <div class=\"field\">\n"
" {% if label %}<label>{{ label }}</label>{% endif %}\n"
" {{ field }}\n"
" </div>\n"
"{% endmacro %}\n"
"\n"
"{% macro render_errors(field) %}\n"
" {% for error in field.errors %}\n"
" <span class=\"error\">{{ error }}</span>\n"
" {% endfor %}\n"
"{% endmacro %}\n");
ASSERT_TRUE(m.ok);
/* GREEN: macro-heavy template must not crash the pipeline. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 14 — Go Template (.gotmpl / .tmpl / .tpl)
*
* Label histogram: Module:1 only.
* Go templates use `{{ template "name" . }}` for includes but these are
* not modeled as graph IMPORTS edges.
* ══════════════════════════════════════════════════════════════════ */
/* Go Template: basic template indexes without crash (.gotmpl). */
TEST(probe_gotemplate_no_crash_gotmpl) {
GpeMetrics m = gpe_metrics("page.gotmpl", "{{ define \"page\" }}\n"
"<!DOCTYPE html>\n"
"<html>\n"
"<body>\n"
" <h1>{{ .Title }}</h1>\n"
" {{ if .Items }}\n"
" <ul>\n"
" {{ range .Items }}<li>{{ . }}</li>{{ end }}\n"
" </ul>\n"
" {{ end }}\n"
"</body>\n"
"</html>\n"
"{{ end }}\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash on a .gotmpl file. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Go Template: Helm-style .tpl with named template definitions. */
TEST(probe_gotemplate_helm_tpl) {
GpeMetrics m = gpe_metrics("_helpers.tpl", "{{/*\n"
"Expand the name of the chart.\n"
"*/}}\n"
"{{- define \"myapp.name\" -}}\n"
"{{- .Chart.Name | trunc 63 | trimSuffix \"-\" }}\n"
"{{- end }}\n"
"\n"
"{{- define \"myapp.labels\" -}}\n"
"helm.sh/chart: {{ include \"myapp.name\" . }}\n"
"{{- end }}\n");
ASSERT_TRUE(m.ok);
/* GREEN: Helm helper template must not crash. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Go Template: `template` call — not modeled as IMPORTS. */
TEST(probe_gotemplate_template_call_no_imports) {
static const GpeFile files[] = {{"layout.tmpl",
"{{ define \"layout\" }}\n"
"<html><body>{{ template \"content\" . }}</body></html>\n"
"{{ end }}\n"},
{"page.tmpl", "{{ define \"content\" }}\n"
"<h1>{{ .Title }}</h1>\n"
"{{ end }}\n"
"{{ template \"layout\" . }}\n"}};
GpeMetrics m = gpe_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
ASSERT_TRUE(m.total_nodes >= 1);
/* GREEN: Go template `template` calls are not modeled as IMPORTS edges. */
ASSERT_TRUE(m.imports == 0);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 15 — Mermaid (.mmd / .mermaid)
*
* Label histogram: Module:1 only.
* Mermaid diagrams describe graph/flow/sequence diagrams — no function or
* type definitions; no cross-file imports.
* ══════════════════════════════════════════════════════════════════ */
/* Mermaid: flowchart diagram indexes without crash (.mmd). */
TEST(probe_mermaid_flowchart_no_crash) {
GpeMetrics m = gpe_metrics("flow.mmd", "graph TD\n"
" A[Start] --> B{Is it?}\n"
" B -- Yes --> C[OK]\n"
" B -- No --> D[End]\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash on a .mmd file. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Mermaid: sequence diagram (.mermaid extension). */
TEST(probe_mermaid_sequence_no_crash) {
GpeMetrics m = gpe_metrics("auth.mermaid", "sequenceDiagram\n"
" participant Client\n"
" participant Server\n"
" Client->>Server: POST /login\n"
" Server-->>Client: 200 OK + token\n");
ASSERT_TRUE(m.ok);
/* GREEN: sequence diagram must not crash. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* Mermaid: entity-relationship diagram. */
TEST(probe_mermaid_er_diagram_no_crash) {
GpeMetrics m = gpe_metrics("schema.mmd", "erDiagram\n"
" USER {\n"
" int id PK\n"
" string name\n"
" string email\n"
" }\n"
" POST {\n"
" int id PK\n"
" int authorId FK\n"
" string title\n"
" }\n"
" USER ||--o{ POST : \"writes\"\n");
ASSERT_TRUE(m.ok);
/* GREEN: ER diagram must not crash the pipeline. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* GROUP 16 — JSDoc (.jsdoc)
*
* Label histogram: Module:1 only.
* JSDoc files contain documentation comment blocks — `@param`, `@returns`,
* `@module`, `@typedef` etc. No executable code; no cross-file imports.
* ══════════════════════════════════════════════════════════════════ */
/* JSDoc: basic parameter documentation indexes without crash. */
TEST(probe_jsdoc_params_no_crash) {
GpeMetrics m = gpe_metrics("api.jsdoc", "/**\n"
" * @param {number} x - First operand\n"
" * @param {number} y - Second operand\n"
" * @returns {number} The sum\n"
" */\n");
ASSERT_TRUE(m.ok);
/* GREEN: pipeline must not crash on a .jsdoc file. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* JSDoc: @module and @typedef annotations. */
TEST(probe_jsdoc_module_typedef_no_crash) {
GpeMetrics m = gpe_metrics("types.jsdoc", "/**\n"
" * @module utils\n"
" */\n"
"\n"
"/**\n"
" * @typedef {Object} User\n"
" * @property {number} id - User identifier\n"
" * @property {string} name - User full name\n"
" * @property {string} email - User email address\n"
" */\n"
"\n"
"/**\n"
" * @callback Predicate\n"
" * @param {User} user\n"
" * @returns {boolean}\n"
" */\n");
ASSERT_TRUE(m.ok);
/* GREEN: module + typedef annotations must not crash the pipeline. */
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
}
/* JSDoc: no function or type nodes expected — markup-only label set. */
TEST(probe_jsdoc_no_function_or_type_nodes) {
GpeMetrics m = gpe_metrics("helpers.jsdoc", "/**\n"
" * @function formatDate\n"
" * @param {Date} date\n"
" * @returns {string}\n"
" */\n");
ASSERT_TRUE(m.ok);
/* GREEN: JSDoc produces only Module:1 — no Function or Class nodes. */
ASSERT_TRUE(m.types == 0);
ASSERT_TRUE(m.functions == 0);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* SUITE wiring
* ══════════════════════════════════════════════════════════════════ */
SUITE(grammar_probe_e) {
/* Protobuf (4 cases) */
RUN_TEST(probe_proto_message_node);
RUN_TEST(probe_proto_service_node);
RUN_TEST(probe_proto_enum_node);
RUN_TEST(probe_proto_import_edge);
/* Thrift (4 cases) */
RUN_TEST(probe_thrift_service_function);
RUN_TEST(probe_thrift_struct_node);
RUN_TEST(probe_thrift_exception_node);
RUN_TEST(probe_thrift_include_edge);
/* Cap'n Proto (4 cases) */
RUN_TEST(probe_capnp_struct_node);
RUN_TEST(probe_capnp_interface_node);
RUN_TEST(probe_capnp_enum_node);
RUN_TEST(probe_capnp_import_edge);
/* WIT (5 cases) */
RUN_TEST(probe_wit_record_enum_function);
RUN_TEST(probe_wit_world_node);
RUN_TEST(probe_wit_type_alias);
RUN_TEST(probe_wit_variant_node);
RUN_TEST(probe_wit_use_edge);
/* Smithy (4 cases) */
RUN_TEST(probe_smithy_structure_node);
RUN_TEST(probe_smithy_service_node);
RUN_TEST(probe_smithy_enum_node);
RUN_TEST(probe_smithy_use_edge);
/* Prisma (4 cases) */
RUN_TEST(probe_prisma_model_node);
RUN_TEST(probe_prisma_multiple_models);
RUN_TEST(probe_prisma_enum_node);
RUN_TEST(probe_prisma_datasource_only);
/* GraphQL (4 cases) */
RUN_TEST(probe_graphql_type_node);
RUN_TEST(probe_graphql_multiple_types);
RUN_TEST(probe_graphql_interface_node);
RUN_TEST(probe_graphql_input_type);
/* Vue (3 cases) */
RUN_TEST(probe_vue_no_crash);
RUN_TEST(probe_vue_script_imports);
RUN_TEST(probe_vue_cross_component_import);
/* Svelte (3 cases) */
RUN_TEST(probe_svelte_no_crash);
RUN_TEST(probe_svelte_script_imports);
RUN_TEST(probe_svelte_cross_component_import);
/* Astro (3 cases) */
RUN_TEST(probe_astro_no_crash);
RUN_TEST(probe_astro_frontmatter_imports);
RUN_TEST(probe_astro_cross_component_import);
/* Blade (3 cases) */
RUN_TEST(probe_blade_no_crash);
RUN_TEST(probe_blade_include_no_imports_edge);
RUN_TEST(probe_blade_directives_no_crash);
/* Liquid (3 cases) */
RUN_TEST(probe_liquid_no_crash);
RUN_TEST(probe_liquid_loop_and_filters);
RUN_TEST(probe_liquid_render_no_imports_edge);
/* Jinja2 (3 cases) */
RUN_TEST(probe_jinja2_no_crash_j2);
RUN_TEST(probe_jinja2_extends_no_imports_edge);
RUN_TEST(probe_jinja2_macro_no_crash);
/* Go Template (3 cases) */
RUN_TEST(probe_gotemplate_no_crash_gotmpl);
RUN_TEST(probe_gotemplate_helm_tpl);
RUN_TEST(probe_gotemplate_template_call_no_imports);
/* Mermaid (3 cases) */
RUN_TEST(probe_mermaid_flowchart_no_crash);
RUN_TEST(probe_mermaid_sequence_no_crash);
RUN_TEST(probe_mermaid_er_diagram_no_crash);
/* JSDoc (3 cases) */
RUN_TEST(probe_jsdoc_params_no_crash);
RUN_TEST(probe_jsdoc_module_typedef_no_crash);
RUN_TEST(probe_jsdoc_no_function_or_type_nodes);
}