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
42 changes: 30 additions & 12 deletions src/harness/cursor_desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,22 +1283,28 @@ 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 escaped = escape_like_pattern(reference);
let deleted_kv: usize = conn
.execute(
"DELETE FROM cursorDiskKV WHERE key = ?1 OR key LIKE ?2 ESCAPE '\\' OR key LIKE ?3 ESCAPE '\\' OR key LIKE ?4 ESCAPE '\\'",
params![
format!("composerData:{reference}"),
format!("bubbleId:{escaped}:%"),
format!("%:{escaped}"),
format!("%:{escaped}:%"),
],
)
.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 Expand Up @@ -1439,6 +1445,18 @@ fn discover_from_headers(conn: &Connection) -> Result<Vec<Discovered<String>>> {
.collect())
}

#[cfg(feature = "opencode")]
fn escape_like_pattern(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if matches!(c, '%' | '_' | '\\') {
out.push('\\');
}
out.push(c);
}
out
}

/// Half-open key range covering every key that starts with `prefix`: the
/// upper bound is the prefix with its last character stepped up by one
/// code point, which sorts after every extension of the prefix under
Expand Down
195 changes: 195 additions & 0 deletions tests/integration/cursor_desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,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 @@ -676,6 +690,187 @@ 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 delete_escapes_like_wildcards_in_session_id() {
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:session_1', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:session_1:b1', '{}');
INSERT INTO cursorDiskKV VALUES ('composerData:sessionX1', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:sessionX1:b1', '{}');
INSERT INTO cursorDiskKV VALUES ('composerData:session%1', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:session%1:b1', '{}');
INSERT INTO cursorDiskKV VALUES ('composerData:sessionANYTHING1', '{}');
INSERT INTO cursorDiskKV VALUES ('bubbleId:sessionANYTHING1:b1', '{}');",
)
.unwrap();

let store = CursorDesktopStore::new(dir.path());

// Delete session_1: should NOT delete sessionX1 or sessionANYTHING1 or session%1
store.delete(&"session_1".to_string()).unwrap();

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

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

// Delete session%1: should NOT delete sessionANYTHING1
store.delete(&"session%1".to_string()).unwrap();

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

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

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