diff --git a/app/src/cloud_object/model/model_test.rs b/app/src/cloud_object/model/model_test.rs index 1fefaf64758..215ef4b18c4 100644 --- a/app/src/cloud_object/model/model_test.rs +++ b/app/src/cloud_object/model/model_test.rs @@ -3,6 +3,7 @@ use lazy_static::lazy_static; use settings::{RespectUserSyncSetting, SyncToCloud}; use warpui::{App, ModelHandle}; +use crate::ai::facts::{AIFact, AIFactObjectModel, AIMemory}; use crate::auth::AuthManager; use crate::auth::AuthStateProvider; use crate::auth::UserUid; @@ -938,3 +939,57 @@ fn active_object_uids_matches_naive_with_empty_model() { }); }); } + +// Regression test for #311: deleting a locally-created object (a `SyncId::ClientId`, +// which is what every object gets now that cloud sync is gone) used to silently no-op +// because `delete_object_with_initiated_by` required `id.server_id()` to be `Some`. +#[test] +fn test_delete_local_object_removes_it() { + App::test((), |mut app| async move { + initialize_app(&mut app, Vec::new()); + + let client_id = ClientId::default(); + let sync_id = SyncId::ClientId(client_id); + UpdateManager::handle(&app).update(&mut app, |update_manager, ctx| { + update_manager.create_ai_fact( + AIFact::Memory(AIMemory { + is_autogenerated: false, + name: Some("test rule".to_string()), + content: "test content".to_string(), + suggested_logging_id: None, + }), + client_id, + Owner::mock_current_user(), + ctx, + ); + }); + + ObjectStoreModel::handle(&app).read(&app, |model, _| { + assert!( + model + .get_object_of_type::(&sync_id) + .is_some(), + "rule should exist after creation" + ); + }); + + UpdateManager::handle(&app).update(&mut app, |update_manager, ctx| { + update_manager.delete_object_by_user( + ObjectTypeAndId::GenericStringObject { + object_type: GenericStringObjectFormat::Json(JsonObjectType::AIFact), + id: sync_id, + }, + ctx, + ); + }); + + ObjectStoreModel::handle(&app).read(&app, |model, _| { + assert!( + model + .get_object_of_type::(&sync_id) + .is_none(), + "rule should be gone after delete_object_by_user" + ); + }); + }); +} diff --git a/app/src/cloud_object/update_manager.rs b/app/src/cloud_object/update_manager.rs index 58030022164..b16b080fda3 100644 --- a/app/src/cloud_object/update_manager.rs +++ b/app/src/cloud_object/update_manager.rs @@ -1440,11 +1440,6 @@ impl UpdateManager { initiated_by: InitiatedBy, ctx: &mut ModelContext, ) { - // If the object isn't known to the server yet, we can't delete it. - let Some(server_id) = id.server_id() else { - return; - }; - let uid = id.uid(); // If there's a pending online-only operation for this object, don't delete it. let Some((has_pending_online_only_operation, has_pending_delete)) = @@ -1464,15 +1459,16 @@ impl UpdateManager { return; } - // Zap:云端 delete RPC 已删除,这里折叠为本地直接清除。 - let num_deleted_objects = - self.on_object_delete_success(vec![SyncId::ServerId(server_id)], ctx); + // Zap:云端 delete RPC 已删除,这里折叠为本地直接清除。本地对象(SyncId::ClientId) + // 没有 server_id;之前这里要求 `id.server_id()` 才能继续,导致本地创建的对象 + // (例如 AI Rule)永远无法删除,点击 Delete 按钮悄无声息地提前 return。 + let num_deleted_objects = self.on_object_delete_success(vec![id.sync_id()], ctx); ctx.emit(UpdateManagerEvent::ObjectOperationComplete { result: ObjectOperationResult { success_type: OperationSuccessType::Success, operation: ObjectOperation::Delete { initiated_by }, client_id: None, - server_id: Some(ServerId::from_string_lossy(&uid)), + server_id: id.server_id(), num_objects: Some(num_deleted_objects), }, });