@@ -2143,6 +2143,100 @@ TEST(tool_bad_project_name_no_overflow_issue235) {
21432143}
21442144#undef ISSUE235_DBNAME
21452145
2146+ /* Issue #576: ingest_dbt_manifest parses a compiled dbt manifest.json and
2147+ * upserts model/seed/snapshot as graph nodes, source as Source nodes, and each
2148+ * node's depends_on.nodes as DEPENDS_ON edges, keyed by dbt unique_id. The
2149+ * handler opens (and creates) the project's on-disk store at
2150+ * <CBM_CACHE_DIR>/<project>.db, so an isolated cache dir plus a small fixture
2151+ * manifest exercises the whole path. The fixture carries 3 models + 1 seed +
2152+ * 1 snapshot + 2 sources and one `test` node that must be skipped, so the
2153+ * reported counts pin both the resource-type filter and the lineage edges. */
2154+ static const char * DBT_MANIFEST_FIXTURE =
2155+ "{\"nodes\":{"
2156+ "\"model.shop.stg_orders\":{\"resource_type\":\"model\",\"name\":\"stg_orders\","
2157+ "\"depends_on\":{\"nodes\":[\"source.shop.raw.orders\"]}},"
2158+ "\"model.shop.stg_customers\":{\"resource_type\":\"model\",\"name\":\"stg_customers\","
2159+ "\"depends_on\":{\"nodes\":[\"source.shop.raw.customers\"]}},"
2160+ "\"model.shop.fct_orders\":{\"resource_type\":\"model\",\"name\":\"fct_orders\","
2161+ "\"depends_on\":{\"nodes\":[\"model.shop.stg_orders\",\"model.shop.stg_customers\"]}},"
2162+ "\"seed.shop.country_codes\":{\"resource_type\":\"seed\",\"name\":\"country_codes\","
2163+ "\"depends_on\":{\"nodes\":[]}},"
2164+ "\"snapshot.shop.orders_snap\":{\"resource_type\":\"snapshot\",\"name\":\"orders_snap\","
2165+ "\"depends_on\":{\"nodes\":[\"model.shop.fct_orders\"]}},"
2166+ "\"test.shop.not_null_fct_orders\":{\"resource_type\":\"test\",\"name\":\"not_null\","
2167+ "\"depends_on\":{\"nodes\":[\"model.shop.fct_orders\"]}}"
2168+ "},\"sources\":{"
2169+ "\"source.shop.raw.orders\":{\"resource_type\":\"source\",\"name\":\"orders\"},"
2170+ "\"source.shop.raw.customers\":{\"resource_type\":\"source\",\"name\":\"customers\"}"
2171+ "}}" ;
2172+
2173+ TEST (tool_ingest_dbt_manifest_issue576 ) {
2174+ char cache [256 ];
2175+ snprintf (cache , sizeof (cache ), "/tmp/cbm-dbt-manifest-XXXXXX" );
2176+ if (!cbm_mkdtemp (cache )) {
2177+ PASS (); /* skip if mkdtemp fails */
2178+ }
2179+
2180+ const char * saved = getenv ("CBM_CACHE_DIR" );
2181+ char * saved_copy = saved ? strdup (saved ) : NULL ;
2182+ cbm_setenv ("CBM_CACHE_DIR" , cache , 1 );
2183+
2184+ /* Write the fixture manifest into the isolated cache dir. */
2185+ char manifest_path [512 ];
2186+ snprintf (manifest_path , sizeof (manifest_path ), "%s/manifest.json" , cache );
2187+ FILE * fp = fopen (manifest_path , "w" );
2188+ ASSERT_NOT_NULL (fp );
2189+ fputs (DBT_MANIFEST_FIXTURE , fp );
2190+ fclose (fp );
2191+
2192+ /* The handler opens (and creates) <CBM_CACHE_DIR>/<project>.db itself. */
2193+ const char * project = "test-dbt-manifest" ;
2194+ char req [2048 ];
2195+ snprintf (req , sizeof (req ),
2196+ "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":"
2197+ "\"ingest_dbt_manifest\",\"arguments\":{\"project\":\"%s\",\"manifest_path\":\"%s\"}}}" ,
2198+ project , manifest_path );
2199+
2200+ cbm_mcp_server_t * srv = cbm_mcp_server_new (NULL );
2201+ ASSERT_NOT_NULL (srv );
2202+ char * resp = cbm_mcp_server_handle (srv , req );
2203+ ASSERT_NOT_NULL (resp );
2204+
2205+ char * inner = extract_text_content (resp );
2206+ ASSERT_NOT_NULL (inner );
2207+ /* 3 models + 1 seed + 1 snapshot = 5 lineage-bearing nodes; the `test`
2208+ * node is skipped (so models is 5, not 6). */
2209+ ASSERT_NOT_NULL (strstr (inner , "\"status\":\"ingested\"" ));
2210+ ASSERT_NOT_NULL (strstr (inner , "\"models\":5" ));
2211+ ASSERT_NOT_NULL (strstr (inner , "\"sources\":2" ));
2212+ /* stg_orders/stg_customers -> source, fct_orders -> 2 stg, snap -> fct = 5
2213+ * DEPENDS_ON edges; the skipped test node contributes none. */
2214+ ASSERT_NOT_NULL (strstr (inner , "\"edges\":5" ));
2215+
2216+ free (inner );
2217+ free (resp );
2218+ cbm_mcp_server_free (srv );
2219+
2220+ if (saved_copy ) {
2221+ cbm_setenv ("CBM_CACHE_DIR" , saved_copy , 1 );
2222+ free (saved_copy );
2223+ } else {
2224+ cbm_unsetenv ("CBM_CACHE_DIR" );
2225+ }
2226+ char dbpath [512 ];
2227+ snprintf (dbpath , sizeof (dbpath ), "%s/%s.db" , cache , project );
2228+ char dbwal [576 ];
2229+ char dbshm [576 ];
2230+ snprintf (dbwal , sizeof (dbwal ), "%s-wal" , dbpath );
2231+ snprintf (dbshm , sizeof (dbshm ), "%s-shm" , dbpath );
2232+ cbm_unlink (dbwal );
2233+ cbm_unlink (dbshm );
2234+ cbm_unlink (dbpath );
2235+ cbm_unlink (manifest_path );
2236+ cbm_rmdir (cache );
2237+ PASS ();
2238+ }
2239+
21462240/* ══════════════════════════════════════════════════════════════════
21472241 * SUITE
21482242 * ══════════════════════════════════════════════════════════════════ */
@@ -2217,6 +2311,7 @@ SUITE(mcp) {
22172311 RUN_TEST (tool_search_graph_includes_node_properties );
22182312 RUN_TEST (tool_search_graph_query_honors_file_pattern_issue552 );
22192313 RUN_TEST (tool_query_graph_basic );
2314+ RUN_TEST (tool_ingest_dbt_manifest_issue576 );
22202315 RUN_TEST (tool_index_status_no_project );
22212316 RUN_TEST (tool_index_status_includes_git_metadata );
22222317
0 commit comments