@@ -1129,6 +1129,146 @@ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
11291129 PASS ();
11301130}
11311131
1132+ /* Native `fetch()` (#856), sequential path (< 50 files → pass_calls.c). A bare
1133+ * unqualified call to the global fetch API has no import and no local
1134+ * definition anywhere in this project, so registry resolution comes back
1135+ * empty; classify it as HTTP_CALLS via the same #523-style empty-resolution
1136+ * fallback used for axios/requests. A member call on an unrelated receiver
1137+ * (`repo.fetch()`) must NOT be swept in — its callee_name is "repo.fetch",
1138+ * not the bare "fetch" this check matches exactly. */
1139+ TEST (pipeline_native_fetch_classified_as_http_calls ) {
1140+ char tmp [256 ];
1141+ snprintf (tmp , sizeof (tmp ), "/tmp/cbm_fetch_XXXXXX" );
1142+ if (!cbm_mkdtemp (tmp )) {
1143+ FAIL ("tmpdir" );
1144+ }
1145+
1146+ write_temp_file (tmp , "src/api.ts" ,
1147+ "export function loadData(): unknown {\n"
1148+ " return fetch('/api/data');\n"
1149+ "}\n" );
1150+ /* `repo.fetch()` — a method call, not the global. Must not over-match. */
1151+ write_temp_file (tmp , "src/repo.ts" ,
1152+ "export function useRepo(repo: { fetch: () => unknown }): unknown {\n"
1153+ " return repo.fetch();\n"
1154+ "}\n" );
1155+
1156+ char db_path [512 ];
1157+ snprintf (db_path , sizeof (db_path ), "%s/fetch.db" , tmp );
1158+ cbm_pipeline_t * p = cbm_pipeline_new (tmp , db_path , CBM_MODE_FULL );
1159+ ASSERT_NOT_NULL (p );
1160+ ASSERT_EQ (cbm_pipeline_run (p ), 0 );
1161+ const char * project = cbm_pipeline_project_name (p );
1162+
1163+ cbm_store_t * s = cbm_store_open_path (db_path );
1164+ ASSERT_NOT_NULL (s );
1165+
1166+ ASSERT_GTE (cbm_store_count_edges_by_type (s , project , "HTTP_CALLS" ), 1 );
1167+ /* Exactly the bare call, not the method call too. */
1168+ ASSERT_EQ (cbm_store_count_edges_by_type (s , project , "HTTP_CALLS" ), 1 );
1169+
1170+ cbm_store_close (s );
1171+ cbm_pipeline_free (p );
1172+ th_rmtree (tmp );
1173+ PASS ();
1174+ }
1175+
1176+ /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's
1177+ * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls
1178+ * but forces the parallel resolver, since the empty-resolution fallback is a
1179+ * separate implementation there (resolve_file_calls calls
1180+ * emit_http_async_service_edge directly rather than through emit_service_edge,
1181+ * which would otherwise re-derive CBM_SVC_NONE for "fetch" and silently fall
1182+ * through to a plain CALLS edge). */
1183+ TEST (pipeline_native_fetch_parallel_classified_as_http_calls ) {
1184+ char tmp [256 ];
1185+ snprintf (tmp , sizeof (tmp ), "/tmp/cbm_fetch_par_XXXXXX" );
1186+ if (!cbm_mkdtemp (tmp )) {
1187+ FAIL ("tmpdir" );
1188+ }
1189+
1190+ write_temp_file (tmp , "src/api.ts" ,
1191+ "export function loadData(): unknown {\n"
1192+ " return fetch('/api/data');\n"
1193+ "}\n" );
1194+ for (int i = 0 ; i < 52 ; i ++ ) {
1195+ char name [64 ];
1196+ char body [128 ];
1197+ snprintf (name , sizeof (name ), "src/filler%d.ts" , i );
1198+ snprintf (body , sizeof (body ), "export function filler%d(): number {\n return %d;\n}\n" , i ,
1199+ i );
1200+ write_temp_file (tmp , name , body );
1201+ }
1202+
1203+ char * old_workers = getenv ("CBM_WORKERS" );
1204+ char * saved = old_workers ? strdup (old_workers ) : NULL ;
1205+ cbm_setenv ("CBM_WORKERS" , "4" , 1 ); /* force parallel regardless of host cores */
1206+
1207+ char db_path [512 ];
1208+ snprintf (db_path , sizeof (db_path ), "%s/fetch_par.db" , tmp );
1209+ cbm_pipeline_t * p = cbm_pipeline_new (tmp , db_path , CBM_MODE_FULL );
1210+ ASSERT_NOT_NULL (p );
1211+ ASSERT_EQ (cbm_pipeline_run (p ), 0 );
1212+ const char * project = cbm_pipeline_project_name (p );
1213+
1214+ cbm_store_t * s = cbm_store_open_path (db_path );
1215+ ASSERT_NOT_NULL (s );
1216+
1217+ ASSERT_GTE (cbm_store_count_edges_by_type (s , project , "HTTP_CALLS" ), 1 );
1218+
1219+ cbm_store_close (s );
1220+ cbm_pipeline_free (p );
1221+ if (saved ) {
1222+ cbm_setenv ("CBM_WORKERS" , saved , 1 );
1223+ free (saved );
1224+ } else {
1225+ cbm_unsetenv ("CBM_WORKERS" );
1226+ }
1227+ th_rmtree (tmp );
1228+ PASS ();
1229+ }
1230+
1231+ /* Native `fetch()` (#856): a LOCAL `fetch` definition must win over the
1232+ * global-API guess. Registry resolution finds this project's own top-level
1233+ * `fetch` function, so the call is a plain CALLS edge to it — never
1234+ * HTTP_CALLS. Isolated in its own project: the registry's project-wide
1235+ * unique-name fallback means a local `fetch` anywhere in a project can
1236+ * legitimately capture bare `fetch()` calls project-wide, so this must not
1237+ * share a project with the genuine-native-fetch test above. */
1238+ TEST (pipeline_local_fetch_shadow_not_classified_as_http ) {
1239+ char tmp [256 ];
1240+ snprintf (tmp , sizeof (tmp ), "/tmp/cbm_fetch_shadow_XXXXXX" );
1241+ if (!cbm_mkdtemp (tmp )) {
1242+ FAIL ("tmpdir" );
1243+ }
1244+
1245+ write_temp_file (tmp , "src/local_fetch.ts" ,
1246+ "function fetch(url: string): string {\n"
1247+ " return 'mock:' + url;\n"
1248+ "}\n"
1249+ "export function useLocalFetch(): string {\n"
1250+ " return fetch('/api/data');\n"
1251+ "}\n" );
1252+
1253+ char db_path [512 ];
1254+ snprintf (db_path , sizeof (db_path ), "%s/fetch_shadow.db" , tmp );
1255+ cbm_pipeline_t * p = cbm_pipeline_new (tmp , db_path , CBM_MODE_FULL );
1256+ ASSERT_NOT_NULL (p );
1257+ ASSERT_EQ (cbm_pipeline_run (p ), 0 );
1258+ const char * project = cbm_pipeline_project_name (p );
1259+
1260+ cbm_store_t * s = cbm_store_open_path (db_path );
1261+ ASSERT_NOT_NULL (s );
1262+
1263+ ASSERT_EQ (cbm_store_count_edges_by_type (s , project , "HTTP_CALLS" ), 0 );
1264+ ASSERT_TRUE (cross_file_call_exists (s , project , "useLocalFetch" , "fetch" ));
1265+
1266+ cbm_store_close (s );
1267+ cbm_pipeline_free (p );
1268+ th_rmtree (tmp );
1269+ PASS ();
1270+ }
1271+
11321272/* ── Git history pass tests ─────────────────────────────────────── */
11331273
11341274TEST (githistory_is_trackable ) {
@@ -6734,6 +6874,9 @@ SUITE(pipeline) {
67346874 RUN_TEST (pipeline_incremental_preserves_cross_file_calls );
67356875 RUN_TEST (pipeline_tsjs_receiver_suppresses_weak_method_edge );
67366876 RUN_TEST (pipeline_tsjs_receiver_parallel_keeps_service_edges );
6877+ RUN_TEST (pipeline_native_fetch_classified_as_http_calls );
6878+ RUN_TEST (pipeline_native_fetch_parallel_classified_as_http_calls );
6879+ RUN_TEST (pipeline_local_fetch_shadow_not_classified_as_http );
67376880 /* Git history pass */
67386881 RUN_TEST (githistory_is_trackable );
67396882 RUN_TEST (githistory_compute_coupling );
0 commit comments