@@ -174,6 +174,88 @@ TEST(gbuf_edge_dedup) {
174174 PASS ();
175175}
176176
177+ /* Properties on a deduped edge must not depend on arrival order.
178+ *
179+ * confidence/strategy/via are not part of the edge key, and the same logical
180+ * CALLS edge is legitimately minted by two strategies (LSP resolution and
181+ * registry-textual matching) carrying different values. Per-worker edge buffers
182+ * merge in worker-slot order, so an arrival-order-dependent merge makes the
183+ * stored attributes a function of thread scheduling. Insert the same pair of
184+ * blobs in both orders; the stored properties must match. */
185+ TEST (gbuf_edge_props_merge_is_order_independent ) {
186+ const char * lsp = "{\"callee\":\"f\",\"confidence\":0.95,\"strategy\":\"lsp\"}" ;
187+ const char * txt = "{\"callee\":\"f\",\"confidence\":0.40,\"strategy\":\"registry\"}" ;
188+
189+ cbm_gbuf_t * fwd = cbm_gbuf_new ("test" , "/tmp" );
190+ int64_t a1 = cbm_gbuf_upsert_node (fwd , "Function" , "a" , "pkg.a" , "f.go" , 1 , 5 , "{}" );
191+ int64_t b1 = cbm_gbuf_upsert_node (fwd , "Function" , "b" , "pkg.b" , "f.go" , 6 , 10 , "{}" );
192+ cbm_gbuf_insert_edge (fwd , a1 , b1 , "CALLS" , lsp );
193+ cbm_gbuf_insert_edge (fwd , a1 , b1 , "CALLS" , txt );
194+
195+ cbm_gbuf_t * rev = cbm_gbuf_new ("test" , "/tmp" );
196+ int64_t a2 = cbm_gbuf_upsert_node (rev , "Function" , "a" , "pkg.a" , "f.go" , 1 , 5 , "{}" );
197+ int64_t b2 = cbm_gbuf_upsert_node (rev , "Function" , "b" , "pkg.b" , "f.go" , 6 , 10 , "{}" );
198+ cbm_gbuf_insert_edge (rev , a2 , b2 , "CALLS" , txt );
199+ cbm_gbuf_insert_edge (rev , a2 , b2 , "CALLS" , lsp );
200+
201+ const cbm_gbuf_edge_t * * fe = NULL ;
202+ const cbm_gbuf_edge_t * * re = NULL ;
203+ int fc = 0 ;
204+ int rc = 0 ;
205+ cbm_gbuf_find_edges_by_type (fwd , "CALLS" , & fe , & fc );
206+ cbm_gbuf_find_edges_by_type (rev , "CALLS" , & re , & rc );
207+ ASSERT_EQ (fc , 1 );
208+ ASSERT_EQ (rc , 1 );
209+ ASSERT_STR_EQ (fe [0 ]-> properties_json , re [0 ]-> properties_json );
210+
211+ cbm_gbuf_free (fwd );
212+ cbm_gbuf_free (rev );
213+ PASS ();
214+ }
215+
216+ /* The order-independent winner is also the semantically right one: a
217+ * higher-confidence discovery outranks a lower-confidence one regardless of
218+ * which arrived first. */
219+ TEST (gbuf_edge_props_merge_prefers_higher_confidence ) {
220+ const char * lsp = "{\"callee\":\"f\",\"confidence\":0.95,\"strategy\":\"lsp\"}" ;
221+ const char * txt = "{\"callee\":\"f\",\"confidence\":0.40,\"strategy\":\"registry\"}" ;
222+
223+ cbm_gbuf_t * gb = cbm_gbuf_new ("test" , "/tmp" );
224+ int64_t a = cbm_gbuf_upsert_node (gb , "Function" , "a" , "pkg.a" , "f.go" , 1 , 5 , "{}" );
225+ int64_t b = cbm_gbuf_upsert_node (gb , "Function" , "b" , "pkg.b" , "f.go" , 6 , 10 , "{}" );
226+ cbm_gbuf_insert_edge (gb , a , b , "CALLS" , lsp );
227+ cbm_gbuf_insert_edge (gb , a , b , "CALLS" , txt ); /* lower confidence, arrives last */
228+
229+ const cbm_gbuf_edge_t * * edges = NULL ;
230+ int count = 0 ;
231+ cbm_gbuf_find_edges_by_type (gb , "CALLS" , & edges , & count );
232+ ASSERT_EQ (count , 1 );
233+ ASSERT_TRUE (strstr (edges [0 ]-> properties_json , "\"strategy\":\"lsp\"" ) != NULL );
234+
235+ cbm_gbuf_free (gb );
236+ PASS ();
237+ }
238+
239+ /* An empty incoming blob must never displace real stored properties. */
240+ TEST (gbuf_edge_props_merge_keeps_existing_on_empty ) {
241+ const char * lsp = "{\"callee\":\"f\",\"confidence\":0.95,\"strategy\":\"lsp\"}" ;
242+
243+ cbm_gbuf_t * gb = cbm_gbuf_new ("test" , "/tmp" );
244+ int64_t a = cbm_gbuf_upsert_node (gb , "Function" , "a" , "pkg.a" , "f.go" , 1 , 5 , "{}" );
245+ int64_t b = cbm_gbuf_upsert_node (gb , "Function" , "b" , "pkg.b" , "f.go" , 6 , 10 , "{}" );
246+ cbm_gbuf_insert_edge (gb , a , b , "CALLS" , lsp );
247+ cbm_gbuf_insert_edge (gb , a , b , "CALLS" , "{}" );
248+
249+ const cbm_gbuf_edge_t * * edges = NULL ;
250+ int count = 0 ;
251+ cbm_gbuf_find_edges_by_type (gb , "CALLS" , & edges , & count );
252+ ASSERT_EQ (count , 1 );
253+ ASSERT_TRUE (strstr (edges [0 ]-> properties_json , "\"strategy\":\"lsp\"" ) != NULL );
254+
255+ cbm_gbuf_free (gb );
256+ PASS ();
257+ }
258+
177259/* #768: two named imports from the same specifier (same source, same target
178260 * file) must produce two distinct IMPORTS edges, keyed apart by local_name --
179261 * not collapse into one edge that silently drops whichever import lost the
@@ -1067,6 +1149,11 @@ SUITE(graph_buffer) {
10671149 RUN_TEST (gbuf_merge_into_store_preserves );
10681150 RUN_TEST (gbuf_flush_skips_orphan_edges );
10691151
1152+ /* Edge property merge determinism */
1153+ RUN_TEST (gbuf_edge_props_merge_is_order_independent );
1154+ RUN_TEST (gbuf_edge_props_merge_prefers_higher_confidence );
1155+ RUN_TEST (gbuf_edge_props_merge_keeps_existing_on_empty );
1156+
10701157 /* Shared ID tests */
10711158 RUN_TEST (gbuf_shared_ids_unique );
10721159 RUN_TEST (gbuf_shared_ids_null_fallback );
0 commit comments