@@ -741,6 +741,141 @@ func TestWireProtocol_RegisterProjectRoot_EmptyRoot(t *testing.T) {
741741 }
742742}
743743
744+ // =============================================================================
745+ // Outgoing impact
746+ // =============================================================================
747+
748+ func TestWireProtocol_QueryOutgoingImpact_WithResult (t * testing.T ) {
749+ d := newTestDaemon (t , outgoingImpactResp {Result : & OutgoingImpactEntry {
750+ TargetURI : "lip://local//repo/foo.go#Caller" ,
751+ DirectItems : []BlastRadiusItem {
752+ {FileURI : "lip://local//repo/bar.go" , SymbolURI : "lip://local//repo/bar.go#Callee" ,
753+ Distance : 1 , Confidence : 0.95 },
754+ },
755+ TransitiveItems : []BlastRadiusItem {
756+ {FileURI : "lip://local//repo/baz.go" , SymbolURI : "lip://local//repo/baz.go#Deep" ,
757+ Distance : 2 , Confidence : 0.85 },
758+ },
759+ SemanticItems : []BlastRadiusSemanticItem {
760+ {FileURI : "lip://local//repo/similar.go" , SymbolURI : "...#Similar" ,
761+ Similarity : 0.82 , Source : "semantic" },
762+ },
763+ EdgesSource : "scip_with_tier1_edges" ,
764+ Truncated : false ,
765+ }})
766+
767+ got , err := QueryOutgoingImpact ("lip://local//repo/foo.go#Caller" , 0.6 )
768+ d .waitHandled (t )
769+
770+ if err != nil {
771+ t .Fatalf ("unexpected error: %v" , err )
772+ }
773+ if got == nil {
774+ t .Fatal ("got nil result" )
775+ }
776+ if got .TargetURI != "lip://local//repo/foo.go#Caller" {
777+ t .Errorf ("TargetURI = %q" , got .TargetURI )
778+ }
779+ if len (got .DirectItems ) != 1 || got .DirectItems [0 ].Distance != 1 {
780+ t .Errorf ("DirectItems = %+v" , got .DirectItems )
781+ }
782+ if len (got .TransitiveItems ) != 1 || got .TransitiveItems [0 ].Distance != 2 {
783+ t .Errorf ("TransitiveItems = %+v" , got .TransitiveItems )
784+ }
785+ if len (got .SemanticItems ) != 1 || got .SemanticItems [0 ].Source != "semantic" {
786+ t .Errorf ("SemanticItems = %+v" , got .SemanticItems )
787+ }
788+ if got .EdgesSource != "scip_with_tier1_edges" {
789+ t .Errorf ("EdgesSource = %q" , got .EdgesSource )
790+ }
791+
792+ req := d .req ()
793+ assertField (t , req , "type" , "query_outgoing_impact" )
794+ assertField (t , req , "symbol_uri" , "lip://local//repo/foo.go#Caller" )
795+ assertField (t , req , "min_score" , 0.6 )
796+ }
797+
798+ // Null result (target not indexed) must come back as (nil, nil).
799+ func TestWireProtocol_QueryOutgoingImpact_NullResult (t * testing.T ) {
800+ d := newTestDaemon (t , outgoingImpactResp {Result : nil })
801+
802+ got , err := QueryOutgoingImpact ("lip://local//repo/unknown.go#X" , 0.6 )
803+ d .waitHandled (t )
804+
805+ if err != nil {
806+ t .Fatalf ("unexpected error: %v" , err )
807+ }
808+ if got != nil {
809+ t .Errorf ("want nil result for unindexed target, got %+v" , got )
810+ }
811+ }
812+
813+ // min_score=0 must be omitted so the daemon applies its default.
814+ func TestWireProtocol_QueryOutgoingImpact_OmitMinScore (t * testing.T ) {
815+ d := newTestDaemon (t , outgoingImpactResp {})
816+
817+ _ , _ = QueryOutgoingImpact ("lip://x#X" , 0 )
818+ d .waitHandled (t )
819+
820+ req := d .req ()
821+ assertField (t , req , "type" , "query_outgoing_impact" )
822+ assertField (t , req , "symbol_uri" , "lip://x#X" )
823+ assertNoField (t , req , "min_score" )
824+ }
825+
826+ // Empty symbol URI must short-circuit — no socket call.
827+ func TestWireProtocol_QueryOutgoingImpact_EmptySymbol (t * testing.T ) {
828+ prev := os .Getenv ("LIP_SOCKET" )
829+ os .Setenv ("LIP_SOCKET" , "/tmp/lip-nonexistent-ckb-test.sock" )
830+ defer os .Setenv ("LIP_SOCKET" , prev )
831+
832+ got , err := QueryOutgoingImpact ("" , 0.6 )
833+ if got != nil || err != nil {
834+ t .Errorf ("empty symbol: want (nil, nil), got (%v, %v)" , got , err )
835+ }
836+ }
837+
838+ func TestOutgoingEntryToExternal (t * testing.T ) {
839+ entry := & OutgoingImpactEntry {
840+ TargetURI : "lip://x#X" ,
841+ DirectItems : []BlastRadiusItem {
842+ {FileURI : "f1" , SymbolURI : "f1#A" , Distance : 1 , Confidence : 0.9 },
843+ },
844+ TransitiveItems : []BlastRadiusItem {
845+ {FileURI : "f2" , SymbolURI : "f2#B" , Distance : 2 , Confidence : 0.8 },
846+ },
847+ SemanticItems : []BlastRadiusSemanticItem {
848+ {FileURI : "f3" , SymbolURI : "f3#C" , Similarity : 0.75 , Source : "both" },
849+ },
850+ EdgesSource : "tier1" ,
851+ }
852+ ext := OutgoingEntryToExternal (entry )
853+ if ext == nil {
854+ t .Fatal ("got nil" )
855+ }
856+ if len (ext .DirectItems ) != 1 || ext .DirectItems [0 ].SymbolURI != "f1#A" {
857+ t .Errorf ("DirectItems: %+v" , ext .DirectItems )
858+ }
859+ if len (ext .TransitiveItems ) != 1 || ext .TransitiveItems [0 ].Distance != 2 {
860+ t .Errorf ("TransitiveItems: %+v" , ext .TransitiveItems )
861+ }
862+ if len (ext .SemanticItems ) != 1 || ext .SemanticItems [0 ].Source != "both" {
863+ t .Errorf ("SemanticItems: %+v" , ext .SemanticItems )
864+ }
865+ if ext .EdgesSource != "tier1" {
866+ t .Errorf ("EdgesSource = %q" , ext .EdgesSource )
867+ }
868+ if ext .RiskLevel != "" {
869+ t .Errorf ("RiskLevel should be empty for outgoing, got %q" , ext .RiskLevel )
870+ }
871+ }
872+
873+ func TestOutgoingEntryToExternal_Nil (t * testing.T ) {
874+ if got := OutgoingEntryToExternal (nil ); got != nil {
875+ t .Errorf ("nil entry: want nil, got %+v" , got )
876+ }
877+ }
878+
744879func TestWireProtocol_Handshake (t * testing.T ) {
745880 d := newTestDaemon (t , handshakeResp {DaemonVersion : "2.0.0" , ProtocolVersion : 2 })
746881
0 commit comments