@@ -2572,3 +2572,107 @@ def test_p_success_matches_blob(self, clojure_blob, dataset_name):
25722572 assert mismatches .empty , (
25732573 f"[{ dataset_name } ] { len (mismatches )} /{ len (df )} p-success mismatches:\n "
25742574 + mismatches .head (10 ).to_string (index = False ))
2575+
2576+
2577+ class TestD11D12Serialization :
2578+ """Round-trip tests for the D11/D12 plumb-through in to_dict / to_dynamo_dict.
2579+
2580+ Investigation B (2026-06-11) discovered that both serializers were hardcoding
2581+ ``result['consensus']`` to an empty dict regardless of
2582+ ``self.repness['consensus_comments']``, so the D11 consensus dict never
2583+ reached client-report's Majority view and never landed in the DynamoDB
2584+ math blob. ``comment_priorities`` (D12) was already conditionally plumbed
2585+ via ``hasattr/if`` guards; we lock that in with a regression test so a
2586+ future cleanup doesn't silently revert to the empty-default shape.
2587+ """
2588+
2589+ @staticmethod
2590+ def _make_conversation_with_repness (consensus_comments , priorities ):
2591+ """Build a Conversation with just enough state to exercise the
2592+ serializers. Empty rating matrices and empty group_clusters mean the
2593+ rest of to_dict/to_dynamo_dict iterates over zero rows/cols (cheap)
2594+ while the consensus + priorities fields still flow through end-to-end.
2595+ """
2596+ conv = Conversation (conversation_id = 'ztest-serialization' )
2597+ conv .repness = {
2598+ 'comment_ids' : [],
2599+ 'group_repness' : {},
2600+ 'comment_repness' : [],
2601+ 'consensus_comments' : consensus_comments ,
2602+ }
2603+ conv .comment_priorities = priorities
2604+ return conv
2605+
2606+ def test_to_dict_surfaces_consensus_comments (self ):
2607+ """``to_dict()`` must surface ``self.repness['consensus_comments']`` into
2608+ ``result['consensus']``. Pre-fix this slot was hardcoded
2609+ ``{'agree': [], 'disagree': [], 'comment-stats': {}}`` and the D11
2610+ selection was silently dropped on the floor."""
2611+ consensus = {
2612+ 'agree' : [
2613+ {'tid' : 1 , 'n-success' : 3 , 'n-trials' : 4 ,
2614+ 'p-success' : 0.7 , 'p-test' : 1.5 }
2615+ ],
2616+ 'disagree' : [
2617+ {'tid' : 2 , 'n-success' : 2 , 'n-trials' : 5 ,
2618+ 'p-success' : 0.42 , 'p-test' : 1.1 }
2619+ ],
2620+ }
2621+ conv = self ._make_conversation_with_repness (consensus , {})
2622+
2623+ result = conv .to_dict ()
2624+
2625+ assert result ['consensus' ] == consensus , (
2626+ "to_dict() must plumb self.repness['consensus_comments'] into "
2627+ "result['consensus']; got " + repr (result ['consensus' ]))
2628+
2629+ def test_to_dict_surfaces_comment_priorities (self ):
2630+ """``to_dict()`` must surface ``self.comment_priorities`` (D12). This is
2631+ a regression lock: the field is currently conditionally plumbed via
2632+ ``hasattr/if``; a future cleanup must not revert to the hardcoded
2633+ empty default."""
2634+ priorities = {1 : 0.42 , 2 : 1.7 , 3 : 0.0 }
2635+ conv = self ._make_conversation_with_repness (
2636+ {'agree' : [], 'disagree' : []}, priorities )
2637+
2638+ result = conv .to_dict ()
2639+
2640+ # The to_dict key uses underscore form (see line ~1706); no rename
2641+ # happens on the way out, unlike most Clojure-format fields.
2642+ assert 'comment_priorities' in result , (
2643+ "to_dict() must emit 'comment_priorities' when "
2644+ "self.comment_priorities is populated; keys = "
2645+ + repr (sorted (result .keys ())))
2646+ assert result ['comment_priorities' ] == priorities
2647+
2648+ def test_to_dynamo_dict_surfaces_both (self ):
2649+ """``to_dynamo_dict()`` must surface BOTH consensus comments (D11) and
2650+ comment priorities (D12). The DynamoDB shape uses underscore keys
2651+ (``consensus``, ``comment_priorities``); the consensus inner shape
2652+ matches whatever ``self.repness['consensus_comments']`` holds
2653+ (Clojure-style ``agree``/``disagree`` lists)."""
2654+ consensus = {
2655+ 'agree' : [
2656+ {'tid' : 11 , 'n-success' : 8 , 'n-trials' : 10 ,
2657+ 'p-success' : 0.83 , 'p-test' : 2.1 }
2658+ ],
2659+ 'disagree' : [],
2660+ }
2661+ # Priorities use comment-id keys; the serializer coerces to int when
2662+ # possible and emits int values (see lines 2447-2456 of conversation.py).
2663+ priorities = {7 : 1.5 , 9 : 0.25 }
2664+ conv = self ._make_conversation_with_repness (consensus , priorities )
2665+
2666+ result = conv .to_dynamo_dict ()
2667+
2668+ assert result ['consensus' ] == consensus , (
2669+ "to_dynamo_dict() must plumb self.repness['consensus_comments'] "
2670+ "into result['consensus']; got " + repr (result ['consensus' ]))
2671+ assert 'comment_priorities' in result , (
2672+ "to_dynamo_dict() must emit 'comment_priorities' when "
2673+ "self.comment_priorities is populated; keys = "
2674+ + repr (sorted (result .keys ())))
2675+ # to_dynamo_dict coerces values to int via int(priority), so 1.5 -> 1
2676+ # and 0.25 -> 0. We assert the post-coercion shape rather than the
2677+ # raw input to lock in what actually lands in DynamoDB.
2678+ assert result ['comment_priorities' ] == {7 : 1 , 9 : 0 }
0 commit comments