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
29 changes: 17 additions & 12 deletions src/harness/cursor_desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1281,22 +1281,27 @@ 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 OR key LIKE ?4",
params![
format!("composerData:{reference}"),
format!("bubbleId:{reference}:%"),
format!("%:{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
129 changes: 129 additions & 0 deletions tests/integration/cursor_desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,20 @@ 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());
}

#[test]
fn edit_tool_with_replace_all_survives_fixpoint() {
let mut common = fixpoint_common();
Expand Down Expand Up @@ -674,6 +688,121 @@ fn edit_and_write_tools_normalize_and_denormalize_losslessly() {
assert_eq!(back.body, common.body);
}

#[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());
}

#[cfg(feature = "opencode")]
#[test]
fn delete_exact_session_id_does_not_delete_prefixed_sibling() {
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 composerHeaders VALUES ('session-1', 100);
INSERT INTO composerHeaders VALUES ('session-10', 200);
INSERT INTO cursorDiskKV VALUES ('composerData:session-1', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:session-1:b1', '{}');
INSERT INTO cursorDiskKV VALUES ('checkpointId:session-1:c1', '{}');
INSERT INTO cursorDiskKV VALUES ('composerData:session-10', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:session-10:b1', '{}');
INSERT INTO cursorDiskKV VALUES ('checkpointId:session-10:c1', '{}');",
)
.unwrap();

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

let count_s10_headers: i64 = conn
.query_row(
"SELECT count(*) FROM composerHeaders WHERE composerId = 'session-10'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(count_s10_headers, 1);

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

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

#[cfg(feature = "opencode")]
#[test]
fn store_resolves_workspace_id_with_percent_encoding_and_spaces() {
Expand Down
Loading