fix(sync): guard delete actions by default
Follow-up to #2574 andf95f1ab18, which guarded the archive path. The delete path had the same two defects and worse consequences. `useSessionUIStore.deleteSession`/`deleteSessions` declared an `options` parameter and discarded it on both paths, so a caller-supplied runtime key was a silent no-op. `SessionDialogs.tsx:416` already passes options today and they never reach the action. The delete path also never rechecked the runtime. Session IDs are not unique across runtimes, so a response produced by a previous runtime could commit `finalizeConfirmedSessionDeletion` against the runtime the user switched to: evicting an unrelated session from the live and global stores and calling `cleanupPersistedSessionState`, which erases queued messages, todos, folder membership, inline-comment drafts, chat draft, and pins. That is user data loss, not stale cache. `cleanupPersistedSessionState` already rejects an identity whose runtime is no longer active, but `finalizeConfirmedSessionDeletion` defeated that check by passing the live `getRuntimeKey()` at commit time, comparing a value with itself. It now forwards the captured key. Adopt the default-on shape fromf95f1ab18: `expectedRuntimeKey` defaults to the active runtime in `deleteSession`, `deleteSessionInDirectory` and the new canonical `deleteSessions` action, and is rechecked before the request and before every reconciliation. A `404` still means "already deleted" and commits cleanup, but only while the captured runtime is active; after a switch it describes the wrong runtime and the action reports failure instead of committing. Also documents the throw contract of `patchSessionMetadata`, a non-blocking nit raised by the review bot on #2574.
This commit is contained in:
@@ -484,3 +484,42 @@ describe('archiveSessions option forwarding', () => {
|
||||
expect(updateSessionCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSessions option forwarding', () => {
|
||||
let originalDeleteSession;
|
||||
let deleteSessionCalls;
|
||||
|
||||
beforeEach(() => {
|
||||
deleteSessionCalls = [];
|
||||
originalDeleteSession = opencodeClient.deleteSession;
|
||||
opencodeClient.deleteSession = (sessionId) => {
|
||||
deleteSessionCalls.push(sessionId);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
opencodeClient.deleteSession = originalDeleteSession;
|
||||
});
|
||||
|
||||
// The store accepted an options object and dropped it on both the single and
|
||||
// batch delete paths. A key that cannot match the active runtime must abort
|
||||
// before any SDK call rather than deleting and erasing persisted state.
|
||||
test('honors expectedRuntimeKey on the batch delete instead of discarding options', async () => {
|
||||
const result = await useSessionUIStore.getState().deleteSessions(['session-x', 'session-y'], {
|
||||
expectedRuntimeKey: 'runtime-that-is-not-active',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ deletedIds: [], failedIds: ['session-x', 'session-y'] });
|
||||
expect(deleteSessionCalls).toEqual([]);
|
||||
});
|
||||
|
||||
test('honors expectedRuntimeKey on the single delete instead of discarding options', async () => {
|
||||
const deleted = await useSessionUIStore.getState().deleteSession('session-x', {
|
||||
expectedRuntimeKey: 'runtime-that-is-not-active',
|
||||
});
|
||||
|
||||
expect(deleted).toBe(false);
|
||||
expect(deleteSessionCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user