diff --git a/benchmarks/aft-search/slice-descriptors/land-flake-batch-3.json b/benchmarks/aft-search/slice-descriptors/land-flake-batch-3.json new file mode 100644 index 000000000..564a47658 --- /dev/null +++ b/benchmarks/aft-search/slice-descriptors/land-flake-batch-3.json @@ -0,0 +1,12 @@ +{ + "slice_class": "non_ranking", + "targeted_mechanism": "none", + "kind": "harness", + "fixtures": [ + "harness-goldens" + ], + "slices": [ + "flake-batch-3" + ], + "note": "Test-only: timing and race fixes for merge-queue flakes (generous positive waits, waits on real signals). No product search behavior changed and no changed path matches RANKING_FENCE_PREFIXES." +} diff --git a/crates/aft/src/context.rs b/crates/aft/src/context.rs index e5098e8ad..ce47b29e7 100644 --- a/crates/aft/src/context.rs +++ b/crates/aft/src/context.rs @@ -12076,6 +12076,20 @@ impl AppContext { true } + /// Test hook: hold back query-triggered trigram reloads for the current + /// configure generation, as the retry cooldown in + /// `search_index_query_reload_allowed` does. A test can then observe a + /// root whose saved trigram cache is valid but not resident, without its + /// own search request starting a reload whose completion races the reply. + #[doc(hidden)] + pub fn defer_query_search_index_reloads_for_test(&self) { + const HOLD: Duration = Duration::from_secs(60 * 60); + let generation = self.configure_generation(); + let mut state = self.search_index_disconnect_reschedule.lock(); + let replacements = if state.0 == generation { state.1 } else { 0 }; + *state = (generation, replacements, Some(Instant::now() + HOLD)); + } + pub(crate) fn note_search_index_load_succeeded(&self) { let generation = self.configure_generation(); let mut state = self.search_index_disconnect_reschedule.lock(); diff --git a/crates/aft/tests/integration/aft_search_contract_test.rs b/crates/aft/tests/integration/aft_search_contract_test.rs index fe58e37d5..144752c36 100644 --- a/crates/aft/tests/integration/aft_search_contract_test.rs +++ b/crates/aft/tests/integration/aft_search_contract_test.rs @@ -3462,6 +3462,12 @@ fn issue_826_lane_footer_agrees_with_a_degraded_answer() { /// The trigram lane is not resident but its saved cache is valid. Planning /// treats that lane as still loading and answers from a filesystem scan; the /// footer used to call it ready beside `Search status: fully degraded`. +/// +/// The search request would otherwise start reloading the saved cache itself. +/// That reload usually finishes inside the request, but on a loaded machine it +/// can still be running when the footer is written, which then truthfully +/// says `trigram: building`. The test holds the reload back so the footer +/// always describes a valid cache that is not resident. #[test] fn issue_826_trigram_footer_agrees_with_a_scan_answer() { let _git_env = crate::test_helpers::hermetic_git_env_guard(); @@ -3473,12 +3479,25 @@ fn issue_826_trigram_footer_agrees_with_a_scan_answer() { let canonical = std::fs::canonicalize(project.path()).expect("canonical root"); ctx.set_canonical_cache_root(canonical.clone()); ctx.memoized_artifact_cache_key(&canonical); + ctx.defer_query_search_index_reloads_for_test(); install_ready_semantic_lane(&ctx, project.path()); let (rendered, response) = render_search(handle_semantic_search(&request("needle_symbol"), &ctx)); handle.join().expect("embedding server thread"); + assert!( + ctx.search_index_rx() + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_none() + && ctx + .search_index() + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_none(), + "the request must not have started loading the saved trigram cache" + ); assert_eq!(response["success"], true, "{response}"); assert_eq!(response["fully_degraded"], true, "{response}"); assert!( diff --git a/crates/aft/tests/integration/lsp_diagnostics_test.rs b/crates/aft/tests/integration/lsp_diagnostics_test.rs index 8ed3001a5..e0c71fe5e 100644 --- a/crates/aft/tests/integration/lsp_diagnostics_test.rs +++ b/crates/aft/tests/integration/lsp_diagnostics_test.rs @@ -909,8 +909,10 @@ fn move_file_reports_deleted_source_and_created_destination_for_config_file() { let json = serde_json::to_value(&response).expect("response serializes"); assert_eq!(json["success"], true, "move failed: {json}"); + // Both notifications arrive asynchronously from the move; a loaded runner + // can deliver the second one well after 2 s, so wait generously for it. let mut events = drain_watched_file_events_from_ctx(&ctx); - let deadline = Instant::now() + Duration::from_secs(2); + let deadline = Instant::now() + Duration::from_secs(30); while events.len() < 2 && Instant::now() < deadline { thread::sleep(Duration::from_millis(25)); events.extend(drain_watched_file_events_from_ctx(&ctx));