From eaca68f263eeb9531ae1e2496f7206ddaeb8a6bd Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 4 Nov 2026 21:07:03 +0800 Subject: [PATCH] test: retry first edit-path calls and wait for effects in flaky tests (#1271) A provider call that starts a plugin instance (or reads files) can run past the edit-path soft deadline on a slow runner and returns nothing; the tests now ask again, as the editor does. The AI completion test cancels the slow request only once the server has it, and measures from the cancel. The SourcePreview test waits for the effect that marks the selection scope. Closes #1269 --- crates/ide-host/tests/plugins.rs | 34 +++++++++++++++++++++----------- src/ui/SourcePreview.test.tsx | 5 +++-- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index 324add15..d303fd52 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -609,8 +609,9 @@ fn providers_of_several_plugins_run_concurrently() { text: text.into(), }; let at = ide_core::Pos::new(0, 0); - // Activation is not what is measured. - assert_eq!(host.provide_completion(&doc("fast"), at, None).len(), 2); + // Activation is not what is measured: a first call that starts an instance + // can run past the edit-path budget, and its answer is dropped. + assert!(eventually(|| host.provide_completion(&doc("fast"), at, None).len() == 2)); let started = Instant::now(); let items = host.provide_completion(&doc("sleep:500"), at, None); @@ -1676,22 +1677,21 @@ fn official_ai_completion_cancels_superseded_requests() { }; let at = ide_core::Pos::new(1, 4); let host = Arc::new(f); - // A slow request, superseded 200 ms later by the next keystroke's. + // A slow request, superseded by the next keystroke's once the server has it + // (starting the instance comes first, and takes longer on a slow runner). let first = ide_core::CancellationToken::new(); let slow = { let (host, doc, first) = (host.clone(), doc.clone(), first.clone()); - std::thread::spawn(move || { - let started = Instant::now(); - let r = host.host.provide_inline_completion(&doc, at, &first); - (r, started.elapsed()) - }) + std::thread::spawn(move || (host.host.provide_inline_completion(&doc, at, &first), Instant::now())) }; - std::thread::sleep(Duration::from_millis(200)); + assert!(eventually(|| seen.lock().len() == 1), "the first request reaches the server"); + let cancelled = Instant::now(); first.cancel(); let started = Instant::now(); let second = host.host.provide_inline_completion(&doc, at, &ide_core::CancellationToken::new()); let took = started.elapsed(); - let (old, old_took) = slow.join().unwrap(); + let (old, old_done) = slow.join().unwrap(); + let old_took = old_done.saturating_duration_since(cancelled); assert!(old.is_none(), "the superseded request yields nothing"); assert!(old_took < Duration::from_secs(1), "cancelled, not waited out: {old_took:?}"); assert_eq!(second.map(|(_, s)| s.insert_text).as_deref(), Some("a + b")); @@ -1869,7 +1869,12 @@ fn official_mybatis_navigates_between_interface_and_xml() { assert_eq!((target.path, target.range.start), (xml_path.clone(), ide_core::Pos::new(1, 14))); let xdoc = doc(&xml_path, "xml", xml); - let to_java = f.host.provide_definition(&xdoc, ide_core::Pos::new(1, 16)); + // Reads the interface from disk: on a slow machine that too can run past the budget. + let mut to_java = Vec::new(); + assert!(eventually(|| { + to_java = f.host.provide_definition(&xdoc, ide_core::Pos::new(1, 16)); + !to_java.is_empty() + })); assert_eq!( to_java.iter().map(|l| (l.path.clone(), l.range.start)).collect::>(), [(java_path.clone(), ide_core::Pos::new(3, 9))] @@ -2437,7 +2442,12 @@ fn completion_quick_fixes_problems_and_event_kinds_of_api_1() { }; let at = ide_core::Pos::new(0, 0); - let items = f.host.provide_completion(&doc("rich"), at, None); + // The first call starts the instance, which may take longer than the edit-path budget. + let mut items = Vec::new(); + assert!(eventually(|| { + items = f.host.provide_completion(&doc("rich"), at, None); + !items.is_empty() + })); let item = &items[0].1; assert_eq!( (item.insert_text.as_str(), item.snippet, item.filter_text.as_deref()), diff --git a/src/ui/SourcePreview.test.tsx b/src/ui/SourcePreview.test.tsx index 46e9a408..15f442f3 100644 --- a/src/ui/SourcePreview.test.tsx +++ b/src/ui/SourcePreview.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { SourcePreview } from "./SourcePreview"; /** A stable loader reference (a new function reloads, like the real module-level ones). */ @@ -24,6 +24,7 @@ describe("SourcePreview", () => { const { container } = render(); await screen.findByText("// TODO"); // The scroll pane is a selection scope: drags stay inside, text stays copyable. - expect(container.querySelector("[data-selection-scope]")).toBeTruthy(); + // Set by an effect, which can run after the text is on screen. + await waitFor(() => expect(container.querySelector("[data-selection-scope]")).toBeTruthy()); }); });