Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions app/src/cloud_object/model/model_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<GenericStringObjectId, AIFactObjectModel>(&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::<GenericStringObjectId, AIFactObjectModel>(&sync_id)
.is_none(),
"rule should be gone after delete_object_by_user"
);
});
});
}
14 changes: 5 additions & 9 deletions app/src/cloud_object/update_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1440,11 +1440,6 @@ impl UpdateManager {
initiated_by: InitiatedBy,
ctx: &mut ModelContext<Self>,
) {
// 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)) =
Expand All @@ -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),
},
});
Expand Down