@@ -1137,6 +1137,109 @@ fn lock_exclude_newer_mixed_relative_global_absolute_package() -> Result<()> {
11371137 Ok ( ( ) )
11381138}
11391139
1140+ /// Test that negative durations produce the same timestamp as positive durations.
1141+ /// This ensures that `span.abs()` is applied correctly, so "-1 day" and "1 day" both
1142+ /// result in a cutoff 1 day in the past (not 1 day in the future for negative).
1143+ #[ test]
1144+ fn lock_exclude_newer_negative_duration_same_as_positive ( ) -> Result < ( ) > {
1145+ let context = TestContext :: new ( "3.12" ) ;
1146+ let pyproject_toml = context. temp_dir . child ( "pyproject.toml" ) ;
1147+ pyproject_toml. write_str (
1148+ r#"
1149+ [project]
1150+ name = "project"
1151+ version = "0.1.0"
1152+ requires-python = ">=3.12"
1153+ dependencies = ["iniconfig"]
1154+ "# ,
1155+ ) ?;
1156+
1157+ // Lock with positive duration "7 days"
1158+ uv_snapshot ! ( context. filters( ) , context
1159+ . lock( )
1160+ . env_remove( EnvVars :: UV_EXCLUDE_NEWER )
1161+ . env( EnvVars :: UV_TEST_CURRENT_TIMESTAMP , "2025-11-21T12:00:00Z" )
1162+ . arg( "--exclude-newer" )
1163+ . arg( "7 days" ) , @r###"
1164+ success: true
1165+ exit_code: 0
1166+ ----- stdout -----
1167+
1168+ ----- stderr -----
1169+ Resolved 2 packages in [TIME]
1170+ "### ) ;
1171+
1172+ let lock_positive = context. read ( "uv.lock" ) ;
1173+ let timestamp_positive = lock_positive
1174+ . lines ( )
1175+ . find ( |line| line. starts_with ( "exclude-newer = " ) )
1176+ . expect ( "Should find exclude-newer line" ) ;
1177+
1178+ let _ = fs_err:: remove_file ( context. temp_dir . child ( "uv.lock" ) ) ;
1179+
1180+ // Lock with negative ISO 8601 duration "-P7D" (should produce same timestamp)
1181+ // Note: We use --exclude-newer=-P7D to avoid the dash being interpreted as a flag
1182+ uv_snapshot ! ( context. filters( ) , context
1183+ . lock( )
1184+ . env_remove( EnvVars :: UV_EXCLUDE_NEWER )
1185+ . env( EnvVars :: UV_TEST_CURRENT_TIMESTAMP , "2025-11-21T12:00:00Z" )
1186+ . arg( "--exclude-newer=-P7D" ) , @r###"
1187+ success: true
1188+ exit_code: 0
1189+ ----- stdout -----
1190+
1191+ ----- stderr -----
1192+ Resolved 2 packages in [TIME]
1193+ "### ) ;
1194+
1195+ let lock_negative_iso = context. read ( "uv.lock" ) ;
1196+ let timestamp_negative_iso = lock_negative_iso
1197+ . lines ( )
1198+ . find ( |line| line. starts_with ( "exclude-newer = " ) )
1199+ . expect ( "Should find exclude-newer line" ) ;
1200+
1201+ let _ = fs_err:: remove_file ( context. temp_dir . child ( "uv.lock" ) ) ;
1202+
1203+ // Lock with "7 days ago" friendly format (should also produce same timestamp)
1204+ uv_snapshot ! ( context. filters( ) , context
1205+ . lock( )
1206+ . env_remove( EnvVars :: UV_EXCLUDE_NEWER )
1207+ . env( EnvVars :: UV_TEST_CURRENT_TIMESTAMP , "2025-11-21T12:00:00Z" )
1208+ . arg( "--exclude-newer" )
1209+ . arg( "7 days ago" ) , @r###"
1210+ success: true
1211+ exit_code: 0
1212+ ----- stdout -----
1213+
1214+ ----- stderr -----
1215+ Resolved 2 packages in [TIME]
1216+ "### ) ;
1217+
1218+ let lock_ago = context. read ( "uv.lock" ) ;
1219+ let timestamp_ago = lock_ago
1220+ . lines ( )
1221+ . find ( |line| line. starts_with ( "exclude-newer = " ) )
1222+ . expect ( "Should find exclude-newer line" ) ;
1223+
1224+ // All three should produce the same cutoff timestamp (7 days before 2025-11-21T12:00:00Z)
1225+ assert_eq ! (
1226+ timestamp_positive, timestamp_negative_iso,
1227+ "Negative ISO duration should produce the same timestamp as positive duration"
1228+ ) ;
1229+ assert_eq ! (
1230+ timestamp_positive, timestamp_ago,
1231+ "'7 days ago' should produce the same timestamp as '7 days'"
1232+ ) ;
1233+
1234+ // Verify the actual timestamp is correct (2025-11-14T12:00:00Z = 7 days before 2025-11-21T12:00:00Z)
1235+ assert ! (
1236+ timestamp_positive. contains( "2025-11-14T12:00:00Z" ) ,
1237+ "Expected timestamp to be 2025-11-14T12:00:00Z, got: {timestamp_positive}"
1238+ ) ;
1239+
1240+ Ok ( ( ) )
1241+ }
1242+
11401243/// Changing the span in pyproject.toml invalidates the lockfile.
11411244#[ test]
11421245fn lock_exclude_newer_span_change_invalidates ( ) -> Result < ( ) > {
0 commit comments