fix(desktop): invalidate filtered catalog on changed hosted head

This commit is contained in:
2026-09-06 08:15:31 +03:00
parent 00ac6eb86c
commit 14b8e187ea
2 changed files with 99 additions and 0 deletions
+13
View File
@@ -396,6 +396,19 @@ impl App {
let catalog_changed = match &result {
Ok(Outcome::Remote(remote)) => {
matches!(request.operation, Operation::Publish | Operation::Save)
// A changed bound head can enter/leave the active
// filter or page even when no row was displayed.
|| self.hosted.session.as_ref().is_some_and(|session| {
session.connection.base() == request.connection.base()
&& session.resource_id == remote.resource_id
&& session.etag != remote.etag
})
|| self.hosted.uncertain.as_ref().is_some_and(|failed| {
failed.operation == Operation::Save
&& failed.connection.base() == request.connection.base()
&& failed.resource_id == Some(remote.resource_id)
&& failed.etag.as_ref().is_some_and(|etag| etag != &remote.etag)
})
|| self.hosted.catalog.iter().any(|item| {
item.resource_id == remote.resource_id
&& item.revision != remote.revision
+86
View File
@@ -729,3 +729,89 @@ fn hosted_catalog_invalidation_follows_service_writes_and_observed_revision_chan
"Leaving the hosted destination cannot make old results fresh"
);
}
#[test]
fn hosted_new_revision_invalidates_filtered_catalog_even_without_a_displayed_row() {
let mut app = App::with_project(Project::empty());
rename(&mut app, "Original");
let resource = bind(&mut app);
send(&mut app, Event::Query("Renamed".into()));
send(&mut app, Event::Connect);
complete(
&mut app,
Ok(Outcome::Catalog(crate::hosted::client::Catalog {
items: vec![],
next_after: None,
})),
);
assert!(!app.hosted.catalog_stale);
assert!(
app.hosted
.catalog_message()
.unwrap()
.starts_with("No models found")
);
let mut unchanged = remote(app.document.snapshot(), resource, 1);
unchanged.etag = app.hosted.session.as_ref().unwrap().etag.clone();
send(&mut app, Event::Check);
complete(&mut app, Ok(Outcome::Remote(unchanged)));
assert!(
!app.hosted.catalog_stale,
"An unchanged bound head keeps the filtered snapshot fresh"
);
// A different engineer changes the hosted model so it now matches the filter.
let mut changed = Document::from_bundle(app.document.bundle().clone(), LIMITS).unwrap();
changed
.execute(Command::Rename(changed.project().id, "Renamed".into()))
.unwrap();
send(&mut app, Event::Check);
complete(
&mut app,
Ok(Outcome::Remote(remote(changed.snapshot(), resource, 2))),
);
assert_eq!(app.hosted.review.as_ref().unwrap().remote.revision, 2);
assert!(
app.hosted.request.is_none(),
"No polling or automatic catalog request"
);
assert!(
app.hosted.catalog_stale,
"Known newer revision can change membership in an empty filtered catalog"
);
assert!(
!app.hosted
.catalog_message()
.unwrap()
.starts_with("No models found")
);
}
#[test]
fn hosted_changed_bound_head_does_not_invalidate_another_services_empty_filter() {
let mut app = App::with_project(Project::empty());
let resource = bind(&mut app);
send(&mut app, Event::Base("http://127.0.0.1:9876".into()));
send(&mut app, Event::Query("Renamed".into()));
send(&mut app, Event::Connect);
complete(
&mut app,
Ok(Outcome::Catalog(crate::hosted::client::Catalog {
items: vec![],
next_after: None,
})),
);
send(&mut app, Event::Check);
let changed = remote(app.document.snapshot(), resource, 2);
complete(&mut app, Ok(Outcome::Remote(changed)));
assert!(app.hosted.review.is_some());
assert!(!app.hosted.catalog_stale);
assert!(
app.hosted
.catalog_message()
.unwrap()
.starts_with("No models found")
);
assert!(app.hosted.request.is_none());
}