@@ -889,6 +889,7 @@ func TestSetPackageAliasContext(t *testing.T) {
889889func TestCollectMetrics_WithoutPackageAlias (t * testing.T ) {
890890 ClearAllMetrics ()
891891 globalMetricsCollector .packageAliasContext = ""
892+ globalMetricsCollector .packageManagerContext = ""
892893
893894 commandName := "rt_upload"
894895 CollectMetrics (commandName , nil )
@@ -905,6 +906,62 @@ func TestCollectMetrics_WithoutPackageAlias(t *testing.T) {
905906 }
906907}
907908
909+ func TestSetPackageManagerContext (t * testing.T ) {
910+ ClearAllMetrics ()
911+ globalMetricsCollector .packageAliasContext = ""
912+ globalMetricsCollector .packageManagerContext = ""
913+
914+ commandName := "npm_install"
915+ flags := []string {"save-dev" }
916+
917+ // Set the package manager context (simulates a direct `jf npm install` invocation).
918+ SetPackageManagerContext ("npm" )
919+ CollectMetrics (commandName , flags )
920+
921+ metrics := GetCollectedMetrics (commandName )
922+ if metrics == nil {
923+ t .Fatal ("Expected metrics to be collected" )
924+ }
925+ if metrics .PackageAlias {
926+ t .Error ("Expected PackageAlias to be false for direct invocation" )
927+ }
928+ if metrics .PackageManager != "npm" {
929+ t .Errorf ("Expected PackageManager 'npm', got '%s'" , metrics .PackageManager )
930+ }
931+
932+ // packageManagerContext must be cleared after CollectMetrics.
933+ if globalMetricsCollector .packageManagerContext != "" {
934+ t .Errorf ("Expected packageManagerContext to be cleared, got '%s'" , globalMetricsCollector .packageManagerContext )
935+ }
936+ }
937+
938+ func TestSetPackageManagerContext_AliasWinsWhenBothSet (t * testing.T ) {
939+ ClearAllMetrics ()
940+ globalMetricsCollector .packageAliasContext = ""
941+ globalMetricsCollector .packageManagerContext = ""
942+
943+ // Both contexts set: alias must take precedence to preserve existing
944+ // alias-dispatch semantics. PackageAlias is true and PackageManager uses
945+ // the alias tool name.
946+ SetPackageAliasContext ("npm" )
947+ SetPackageManagerContext ("pip" )
948+ CollectMetrics ("npm_install" , nil )
949+
950+ metrics := GetCollectedMetrics ("npm_install" )
951+ if metrics == nil {
952+ t .Fatal ("Expected metrics to be collected" )
953+ }
954+ if ! metrics .PackageAlias {
955+ t .Error ("Expected PackageAlias to be true when alias context set" )
956+ }
957+ if metrics .PackageManager != "npm" {
958+ t .Errorf ("Expected PackageManager 'npm' (alias wins), got '%s'" , metrics .PackageManager )
959+ }
960+ if globalMetricsCollector .packageAliasContext != "" || globalMetricsCollector .packageManagerContext != "" {
961+ t .Error ("Expected both context fields to be cleared after CollectMetrics" )
962+ }
963+ }
964+
908965func TestMetricsIntegrationFlow (t * testing.T ) {
909966 // Clear any existing metrics
910967 globalMetricsCollector .mu .Lock ()
@@ -1039,3 +1096,118 @@ func TestAgentContextEndToEnd(t *testing.T) {
10391096 t .Errorf ("wire JSON missing is_interactive: %s" , wire )
10401097 }
10411098}
1099+
1100+ // TestExecWithPackageManager verifies that ExecWithPackageManager stamps the
1101+ // package_manager label in the collected metrics before the command runs.
1102+ func TestExecWithPackageManager (t * testing.T ) {
1103+ ClearAllMetrics ()
1104+ globalMetricsCollector .packageAliasContext = ""
1105+ globalMetricsCollector .packageManagerContext = ""
1106+
1107+ commandName := "rt_go"
1108+ cmd := & MockCommand {name : commandName }
1109+
1110+ if err := ExecWithPackageManager (cmd , "go" ); err != nil {
1111+ t .Fatalf ("ExecWithPackageManager returned unexpected error: %v" , err )
1112+ }
1113+
1114+ m := GetCollectedMetrics (commandName )
1115+ if m == nil {
1116+ t .Fatal ("expected metrics to be collected after ExecWithPackageManager" )
1117+ }
1118+ if m .PackageManager != "go" {
1119+ t .Errorf ("PackageManager: got %q want %q" , m .PackageManager , "go" )
1120+ }
1121+ if m .PackageAlias {
1122+ t .Errorf ("PackageAlias should be false for direct ExecWithPackageManager call, got true" )
1123+ }
1124+ }
1125+
1126+ // TestExecWithPackageManager_AliasWins verifies that when both alias context and
1127+ // package manager are set, alias wins (is_package_alias=true, package_manager=alias value).
1128+ func TestExecWithPackageManager_AliasWins (t * testing.T ) {
1129+ ClearAllMetrics ()
1130+ globalMetricsCollector .packageAliasContext = ""
1131+ globalMetricsCollector .packageManagerContext = ""
1132+
1133+ commandName := "rt_npm_install"
1134+ cmd := & MockCommand {name : commandName }
1135+
1136+ // Simulate alias path setting alias context first
1137+ SetPackageAliasContext ("npm" )
1138+ // Then ExecWithPackageManager also called (shouldn't override alias)
1139+ if err := ExecWithPackageManager (cmd , "npm" ); err != nil {
1140+ t .Fatalf ("ExecWithPackageManager returned unexpected error: %v" , err )
1141+ }
1142+
1143+ m := GetCollectedMetrics (commandName )
1144+ if m == nil {
1145+ t .Fatal ("expected metrics to be collected" )
1146+ }
1147+ if ! m .PackageAlias {
1148+ t .Error ("PackageAlias should be true when alias context was set" )
1149+ }
1150+ if m .PackageManager != "npm" {
1151+ t .Errorf ("PackageManager: got %q want %q" , m .PackageManager , "npm" )
1152+ }
1153+ }
1154+
1155+ // TestExec_NonPMCommand_NoPackageManagerFields verifies that running a plain
1156+ // non-PM command via Exec (e.g. rt_ping, rt_upload) does NOT set package_manager
1157+ // or package_alias in the collected metrics.
1158+ func TestExec_NonPMCommand_NoPackageManagerFields (t * testing.T ) {
1159+ ClearAllMetrics ()
1160+ globalMetricsCollector .packageAliasContext = ""
1161+ globalMetricsCollector .packageManagerContext = ""
1162+
1163+ cmd := & MockCommand {name : "rt_ping" }
1164+ if err := Exec (cmd ); err != nil {
1165+ t .Fatalf ("Exec returned unexpected error: %v" , err )
1166+ }
1167+
1168+ m := GetCollectedMetrics ("rt_ping" )
1169+ if m == nil {
1170+ t .Fatal ("expected metrics to be collected" )
1171+ }
1172+ if m .PackageAlias {
1173+ t .Errorf ("PackageAlias should be false for non-PM command, got true" )
1174+ }
1175+ if m .PackageManager != "" {
1176+ t .Errorf ("PackageManager should be empty for non-PM command, got %q" , m .PackageManager )
1177+ }
1178+ }
1179+
1180+ // TestExec_NonPMAfterPM_NoLeakage verifies that running a non-PM command after
1181+ // a PM command does NOT inherit the previous command's package_manager.
1182+ // This guards against context leakage between sequential command executions.
1183+ func TestExec_NonPMAfterPM_NoLeakage (t * testing.T ) {
1184+ ClearAllMetrics ()
1185+ globalMetricsCollector .packageAliasContext = ""
1186+ globalMetricsCollector .packageManagerContext = ""
1187+
1188+ // First: run a PM command
1189+ pmCmd := & MockCommand {name : "rt_go" }
1190+ if err := ExecWithPackageManager (pmCmd , "go" ); err != nil {
1191+ t .Fatalf ("ExecWithPackageManager: %v" , err )
1192+ }
1193+ pmMetrics := GetCollectedMetrics ("rt_go" )
1194+ if pmMetrics == nil || pmMetrics .PackageManager != "go" {
1195+ t .Fatalf ("PM command should have PackageManager=go, got: %+v" , pmMetrics )
1196+ }
1197+
1198+ // Second: run a non-PM command immediately after
1199+ nonPMCmd := & MockCommand {name : "rt_ping" }
1200+ if err := Exec (nonPMCmd ); err != nil {
1201+ t .Fatalf ("Exec: %v" , err )
1202+ }
1203+ nonPMMetrics := GetCollectedMetrics ("rt_ping" )
1204+ if nonPMMetrics == nil {
1205+ t .Fatal ("expected metrics to be collected for non-PM command" )
1206+ }
1207+ if nonPMMetrics .PackageManager != "" {
1208+ t .Errorf ("non-PM command leaked package_manager=%q from previous PM command" , nonPMMetrics .PackageManager )
1209+ }
1210+ if nonPMMetrics .PackageAlias {
1211+ t .Errorf ("non-PM command leaked package_alias=true from previous PM command" )
1212+ }
1213+ }
0 commit comments