/* * 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 #include #include #include #include #include #include #include #include /* ══════════════════════════════════════════════════════════════════ * 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); } /* ══════════════════════════════════════════════════════════════════ * 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(); } /* Protobuf: two-file `import` → IMPORTS edge. * RED: grammar-only protobuf has no import-resolver pass in the pipeline; * `import "other.proto"` statements are not turned into IMPORTS edges. */ 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); /* RED: protobuf `import` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no proto import resolver */ 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(); } /* Thrift: two-file `include` → IMPORTS edge. * RED: grammar-only Thrift has no include-resolver in the pipeline. */ 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); /* RED: Thrift `include` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Thrift include resolver */ 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(); } /* WIT: two-file `use` → IMPORTS edge. * RED: grammar-only WIT has no use-resolver in the pipeline. */ 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); /* RED: WIT `use` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no WIT use resolver */ 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(); } /* Smithy: two-file `use` → IMPORTS edge. * RED: grammar-only Smithy has no use-resolver in the pipeline. */ 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); /* RED: Smithy `use` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Smithy use resolver */ 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 \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: \n" "\n" "\n"); ASSERT_TRUE(m.ok); /* GREEN: pipeline indexes without crash. */ ASSERT_TRUE(m.total_nodes >= 1); PASS(); } /* Vue: two-file cross-component import → IMPORTS graph edge. * The JS/TS resolver handles .vue imports via the same path-resolution * logic used for TypeScript. With matching files on disk the resolver * should produce an IMPORTS edge. * RED: .vue extension may not be handled by the JS/TS import resolver. */ TEST(probe_vue_cross_component_import) { static const GpeFile files[] = { {"BaseButton.vue", "\n" "\n"}, {"App.vue", "\n" "\n"} }; GpeMetrics m = gpe_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: .vue cross-component IMPORTS edge not confirmed by the pipeline. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — .vue paths not in JS resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 9 — Svelte (.svelte) * * Label histogram: Module:1 only. * IMPORTS: \n" "

{greet()}

\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: \n" "\n"); ASSERT_TRUE(m.ok); /* GREEN: pipeline indexes without crash. */ ASSERT_TRUE(m.total_nodes >= 1); PASS(); } /* Svelte: two-file cross-component import → IMPORTS graph edge. * RED: .svelte extension may not be handled by the JS/TS import resolver. */ TEST(probe_svelte_cross_component_import) { static const GpeFile files[] = { {"Button.svelte", "\n" "\n"}, {"App.svelte", "\n" "