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
28 changes: 16 additions & 12 deletions src/harness/cursor_desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,22 +1138,26 @@ impl Store for CursorDesktopStore {
fn delete(&self, reference: &String) -> Result<()> {
super::checked_id_component(CursorDesktop::NAME, reference)?;
let conn = Connection::open(self.db_path()).map_err(sqlite_err)?;
let deleted: usize = conn
.execute(
let deleted_headers = if has_table(&conn, "composerHeaders")? {
conn.execute(
"DELETE FROM composerHeaders WHERE composerId = ?1",
params![reference],
)
.map_err(sqlite_err)?
} else {
0
};
let deleted_kv: usize = conn
.execute(
"DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 OR key LIKE ?3",
params![
format!("composerData:{reference}"),
format!("bubbleId:{reference}:%"),
format!("%:{reference}%"),
],
)
.map_err(sqlite_err)?;
conn.execute(
"DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 OR key LIKE ?3",
params![
format!("composerData:{reference}"),
format!("bubbleId:{reference}:%"),
format!("%:{reference}%"),
],
)
.map_err(sqlite_err)?;
if deleted == 0 {
if deleted_headers == 0 && deleted_kv == 0 {
return Err(Error::Malformed {
harness: CursorDesktop::NAME,
detail: format!("no such session: {reference}"),
Expand Down
76 changes: 76 additions & 0 deletions tests/integration/cursor_desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,3 +548,79 @@ fn pending_tool_call_yields_no_result_message() {
}]
);
}

#[cfg(feature = "opencode")]
#[test]
fn delete_roundtrip_removes_session_and_fails_on_duplicate() {
let dir = tempfile::tempdir().unwrap();
let store = CursorDesktopStore::new(dir.path());
let saved = store.save(&sample_transcript()).unwrap();

assert_eq!(store.discover().unwrap().len(), 1);
store.delete(&saved.reference).unwrap();
assert_eq!(store.discover().unwrap().len(), 0);

assert!(store.delete(&saved.reference).is_err());
}

#[cfg(feature = "opencode")]
#[test]
fn delete_succeeds_on_database_without_composer_headers_table() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("globalStorage").join("state.vscdb");
std::fs::create_dir_all(db_path.parent().unwrap()).unwrap();

let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE cursorDiskKV (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO cursorDiskKV VALUES ('composerData:pre-table-1', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:pre-table-1:b1', '{}');",
)
.unwrap();

let store = CursorDesktopStore::new(dir.path());
store.delete(&"pre-table-1".to_string()).unwrap();

let count: i64 = conn
.query_row(
"SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%pre-table-1%'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(count, 0);

// Deleting again should fail with no such session
assert!(store.delete(&"pre-table-1".to_string()).is_err());
}

#[cfg(feature = "opencode")]
#[test]
fn delete_succeeds_on_kv_only_session_in_modern_database() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("globalStorage").join("state.vscdb");
std::fs::create_dir_all(db_path.parent().unwrap()).unwrap();

let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE composerHeaders (composerId TEXT PRIMARY KEY, recency INTEGER);
CREATE TABLE cursorDiskKV (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO cursorDiskKV VALUES ('composerData:kv-only-1', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:kv-only-1:b1', '{}');",
)
.unwrap();

let store = CursorDesktopStore::new(dir.path());
store.delete(&"kv-only-1".to_string()).unwrap();

let count: i64 = conn
.query_row(
"SELECT count(*) FROM cursorDiskKV WHERE key LIKE '%kv-only-1%'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(count, 0);

assert!(store.delete(&"kv-only-1".to_string()).is_err());
}
Loading