@@ -223,3 +223,68 @@ func TestNewNormalizesRelationshipDeterministically(t *testing.T) {
223223 require .Equal (t , r1 .Integrity .RecordHash , r2 .Integrity .RecordHash )
224224 require .Equal (t , []string {"rule-a" , "rule-b" }, r1 .Relationship .PolicyRef .MatchedRuleIDs )
225225}
226+
227+ func TestFirstRelationshipSelection (t * testing.T ) {
228+ primary := & Relationship {ParentRecordID : "primary" }
229+ alias := & Relations {ParentRecordID : "alias" }
230+
231+ require .Equal (t , primary , firstRelationship (RecordOpts {Relationship : primary , Relations : alias }))
232+ require .Equal (t , alias , firstRelationship (RecordOpts {Relations : alias }))
233+ require .Nil (t , firstRelationship (RecordOpts {}))
234+ }
235+
236+ func TestNormalizeDigestRefAndHexValidation (t * testing.T ) {
237+ require .Equal (t , "" , normalizeDigestRef ("" ))
238+ require .Equal (t , "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" , normalizeDigestRef ("SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ))
239+ require .Equal (t , "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" , normalizeDigestRef ("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ))
240+ require .Equal (t , "policy-ref-v2" , normalizeDigestRef ("policy-ref-v2" ))
241+ require .False (t , isLowerHexLen ("nothex" , 64 ))
242+ require .False (t , isLowerHexLen ("abc" , 64 ))
243+ }
244+
245+ func TestNormalizedEdgesStableSortAndDedup (t * testing.T ) {
246+ edges := []RelationshipEdge {
247+ {Kind : "calls" , From : RelationshipRef {Kind : "agent" , ID : "a" }, To : RelationshipRef {Kind : "tool" , ID : "b" }},
248+ {Kind : "Calls" , From : RelationshipRef {Kind : "agent" , ID : "a" }, To : RelationshipRef {Kind : "tool" , ID : "b" }},
249+ {Kind : "targets" , From : RelationshipRef {Kind : "agent" , ID : "a" }, To : RelationshipRef {Kind : "resource" , ID : "z" }},
250+ {Kind : "calls" , From : RelationshipRef {Kind : "agent" , ID : "a" }, To : RelationshipRef {Kind : "tool" , ID : "a" }},
251+ }
252+ normalized := normalizedEdges (edges )
253+ require .Len (t , normalized , 3 )
254+ require .Equal (t , "calls" , normalized [0 ].Kind )
255+ require .Equal (t , "a" , normalized [0 ].To .ID )
256+ require .Equal (t , "calls" , normalized [1 ].Kind )
257+ require .Equal (t , "b" , normalized [1 ].To .ID )
258+ require .Equal (t , "targets" , normalized [2 ].Kind )
259+ require .Nil (t , normalizedEdges (nil ))
260+ }
261+
262+ func TestComputeHashIncludesRelationshipAndLegacyAlias (t * testing.T ) {
263+ base := & Record {
264+ RecordID : "prf-test" ,
265+ RecordVersion : SchemaVersion ,
266+ Timestamp : time .Date (2026 , 2 , 17 , 12 , 0 , 0 , 0 , time .UTC ),
267+ Source : "gait" ,
268+ SourceProduct : "gait" ,
269+ RecordType : "policy_enforcement" ,
270+ Event : map [string ]any {"verdict" : "allow" },
271+ }
272+ r1 := Clone (base )
273+ r1 .Relationship = & Relationship {ParentRef : & RelationshipRef {Kind : "trace" , ID : "t1" }}
274+ h1 , err := ComputeHash (r1 )
275+ require .NoError (t , err )
276+
277+ r2 := Clone (base )
278+ r2 .Relations = & Relations {ParentRecordID : "prf-prev" }
279+ h2 , err := ComputeHash (r2 )
280+ require .NoError (t , err )
281+
282+ r3 := Clone (base )
283+ r3 .Relationship = & Relationship {ParentRef : & RelationshipRef {Kind : "trace" , ID : "t1" }}
284+ r3 .Relations = & Relations {ParentRecordID : "prf-prev" }
285+ h3 , err := ComputeHash (r3 )
286+ require .NoError (t , err )
287+
288+ require .NotEqual (t , h1 , h2 )
289+ require .NotEqual (t , h2 , h3 )
290+ }
0 commit comments