mirror of
https://github.com/storytold/craft-ai-integrations.git
synced 2026-11-02 18:06:49 +00:00
ai-egui: drop a test's answer once the address has moved on (#33)
Only the Test button is disabled while a test runs; the address field stays editable. Edit it mid-test and the answer still lands, so the panel reported "Connected. Models found: N" for an address that was never tested and carried those models onto the saved connection. The form already treats an edit as invalidating: it clears the models and the result line, saying an earlier test no longer describes what is typed. The answer simply arrived afterwards and put them back. The panel now remembers the address each test was started against and ignores an answer that no longer matches, leaving the form as untested. The runner's generation counter does not cover this: it discards answers from superseded tests, and editing the address starts no new test.
This commit is contained in:
committed by
GitHub
parent
aea912bb55
commit
8081df7f1d
+29
-21
@@ -39,7 +39,9 @@ pub struct ConnectionPanel {
|
||||
manager: ConnectionManager,
|
||||
runner: TestRunner,
|
||||
form: ConnectionFormState,
|
||||
testing: bool,
|
||||
/// The address the running test was started against, so an answer arriving after the address
|
||||
/// changed can be told from one that still describes it. `None` while no test is running.
|
||||
testing: Option<String>,
|
||||
}
|
||||
|
||||
impl ConnectionPanel {
|
||||
@@ -53,42 +55,48 @@ impl ConnectionPanel {
|
||||
runner: TestRunner::new(manager.clone()),
|
||||
manager,
|
||||
form,
|
||||
testing: false,
|
||||
testing: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the panel and handles what was pressed.
|
||||
pub fn show(&mut self, ui: &mut Ui) -> Option<PanelEvent> {
|
||||
if self.testing
|
||||
if self.testing.is_some()
|
||||
&& let Some(answer) = self.runner.poll()
|
||||
&& let Some(tested) = self.testing.take()
|
||||
{
|
||||
self.testing = false;
|
||||
match answer {
|
||||
Ok(report) => {
|
||||
self.form.test = TestLine::Passed {
|
||||
models: report.models.len(),
|
||||
millis: u64::try_from(report.elapsed.as_millis()).unwrap_or(u64::MAX),
|
||||
};
|
||||
self.form.draft.set_models(report.models);
|
||||
// The address stays editable while a test runs, so this answer may describe an
|
||||
// address the form no longer holds. Editing already cleared the models and the
|
||||
// result line, leaving the form as untested; taking the answer now would put one
|
||||
// endpoint's models under another, so it is dropped instead.
|
||||
if tested.trim() == self.form.draft.endpoint.trim() {
|
||||
match answer {
|
||||
Ok(report) => {
|
||||
self.form.test = TestLine::Passed {
|
||||
models: report.models.len(),
|
||||
millis: u64::try_from(report.elapsed.as_millis()).unwrap_or(u64::MAX),
|
||||
};
|
||||
self.form.draft.set_models(report.models);
|
||||
}
|
||||
Err(e)
|
||||
if e.kind == ErrorKind::Unsupported
|
||||
&& self.form.draft.provider == "vertex-express" =>
|
||||
{
|
||||
self.form.test = TestLine::Unverified(e.message);
|
||||
}
|
||||
Err(e) => self.form.test = TestLine::Failed(e.message),
|
||||
}
|
||||
Err(e)
|
||||
if e.kind == ErrorKind::Unsupported
|
||||
&& self.form.draft.provider == "vertex-express" =>
|
||||
{
|
||||
self.form.test = TestLine::Unverified(e.message);
|
||||
}
|
||||
Err(e) => self.form.test = TestLine::Failed(e.message),
|
||||
}
|
||||
}
|
||||
if self.testing {
|
||||
if self.testing.is_some() {
|
||||
ui.ctx().request_repaint_after(Duration::from_millis(100));
|
||||
}
|
||||
match connection_form(ui, &mut self.form, self.testing)? {
|
||||
match connection_form(ui, &mut self.form, self.testing.is_some())? {
|
||||
FormEvent::Cancel => Some(PanelEvent::Closed),
|
||||
FormEvent::Test => {
|
||||
self.form.test = TestLine::Running;
|
||||
self.form.error = None;
|
||||
self.testing = true;
|
||||
self.testing = Some(self.form.draft.endpoint.trim().to_owned());
|
||||
self.runner
|
||||
.start(TestTarget::Draft(self.form.draft.clone()));
|
||||
None
|
||||
|
||||
@@ -222,3 +222,73 @@ fn models_found_at_one_address_are_not_saved_for_another() {
|
||||
);
|
||||
assert!(saved.connections[0].models.is_empty());
|
||||
}
|
||||
|
||||
/// Replaces what is in a text box while a test is running: the panel asks for a repaint every
|
||||
/// frame then, so the `run()` in `replace_text` would never settle.
|
||||
fn replace_text_mid_test(h: &mut Harness<'_, State>, nth: usize, text: &str) {
|
||||
h.get_all_by_role(Role::TextInput)
|
||||
.nth(nth)
|
||||
.expect("field")
|
||||
.focus();
|
||||
h.step();
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.step();
|
||||
h.get_all_by_role(Role::TextInput)
|
||||
.nth(nth)
|
||||
.expect("field")
|
||||
.type_text(text);
|
||||
h.step();
|
||||
}
|
||||
|
||||
/// Answers slowly, so the address can be edited while the answer is still on its way.
|
||||
fn slow_models_server() -> Server {
|
||||
Server::start(|_| {
|
||||
std::thread::sleep(Duration::from_millis(600));
|
||||
Reply::Json(
|
||||
200,
|
||||
r#"{"object":"list","data":[{"id":"gpt-4o"},{"id":"gpt-image-1"}]}"#.into(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_from_a_test_that_was_still_running_are_not_saved_for_the_new_address() {
|
||||
// The sibling test above edits the address *after* the answer is in. This is the other
|
||||
// order: the address is edited while the test is still running, so the answer arrives
|
||||
// describing an address the form no longer holds.
|
||||
let server = slow_models_server();
|
||||
let mut r = rig();
|
||||
r.h.run();
|
||||
r.h.get_by_role(Role::ComboBox).click();
|
||||
r.h.run();
|
||||
r.h.get_by_label("Ollama").click();
|
||||
r.h.run();
|
||||
replace_text(&mut r.h, 1, &server.url);
|
||||
r.h.get_by_label("Test").click();
|
||||
r.h.step();
|
||||
replace_text_mid_test(&mut r.h, 1, "http://127.0.0.1:9/v1");
|
||||
// Let the answer land and be handled.
|
||||
let end = Instant::now() + Duration::from_secs(5);
|
||||
while Instant::now() < end {
|
||||
r.h.step();
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
assert_eq!(server.count(), 1, "the test did reach the server");
|
||||
assert!(
|
||||
r.h.query_by_label_contains("Connected. Models found")
|
||||
.is_none(),
|
||||
"an answer for an address the form no longer holds is not reported as this one's"
|
||||
);
|
||||
r.h.get_by_label("Save").click();
|
||||
r.h.run();
|
||||
assert!(matches!(r.h.state().event, Some(PanelEvent::Saved(_))));
|
||||
let saved = r.shared.load().unwrap();
|
||||
assert_eq!(
|
||||
saved.connections[0].endpoint.as_deref(),
|
||||
Some("http://127.0.0.1:9/v1")
|
||||
);
|
||||
assert!(
|
||||
saved.connections[0].models.is_empty(),
|
||||
"those models were found at the address that was replaced"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user