cc-haha/desktop/src/lib/persistenceMigrations.test.ts
程序员阿江(Relakkes) 7ae885a114 fix: add safe Doctor rescue path for desktop upgrades
Online upgrades can strand users on stale desktop UI state or malformed local persistence. This adds a deny-by-default Doctor path that resets only regenerable desktop UI state, reports protected local files with redacted metadata, and keeps protected repair as a dry-run no-op until a reviewed backup-first flow exists.

Constraint: Chat transcripts, model/provider config, Skills, MCP, IM bindings, adapter sessions, OAuth tokens, plugins, and team/session records are user-owned protected state.
Rejected: Automatically rewrite malformed protected JSON | unsafe without schema-specific migrations and backups.
Rejected: Continue relying only on startup migrations | users need an explicit recovery action after a white screen.
Confidence: high
Scope-risk: moderate
Directive: Keep Doctor repair deny-by-default; do not mutate protected state without an explicit reviewed backup-first manual repair flow.
Tested: bun test src/server/__tests__/doctor-service.test.ts
Tested: cd desktop && bun run test src/components/ErrorBoundary.test.tsx src/lib/doctorRepair.test.ts src/__tests__/diagnosticsSettings.test.tsx src/components/layout/StartupErrorView.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:server
Tested: bun run verify (failed only existing agent-utils coverage baseline; changed-lines coverage 97.62%)
Not-tested: Packaged desktop manual Doctor click path.
2026-05-07 00:04:06 +08:00

99 lines
3.6 KiB
TypeScript

import { beforeEach, describe, expect, test } from 'vitest'
import {
CURRENT_DESKTOP_PERSISTENCE_SCHEMA_VERSION,
DESKTOP_PERSISTENCE_VERSION_KEY,
runDesktopPersistenceMigrations,
} from './persistenceMigrations'
describe('desktop persistence migrations', () => {
beforeEach(() => {
window.localStorage.clear()
})
test('migrates legacy open-tab arrays into the current tab persistence shape', () => {
window.localStorage.setItem('cc-haha-open-tabs', JSON.stringify([
{ sessionId: 'session-1', title: 'Old tab' },
{ sessionId: '__terminal__legacy', title: 'Terminal 1', type: 'terminal' },
{ sessionId: 123, title: 'bad' },
]))
const report = runDesktopPersistenceMigrations()
expect(report.migratedKeys).toContain('cc-haha-open-tabs')
expect(JSON.parse(window.localStorage.getItem('cc-haha-open-tabs') || '{}')).toEqual({
openTabs: [{ sessionId: 'session-1', title: 'Old tab', type: 'session' }],
activeTabId: 'session-1',
})
expect(window.localStorage.getItem(DESKTOP_PERSISTENCE_VERSION_KEY)).toBe(String(CURRENT_DESKTOP_PERSISTENCE_SCHEMA_VERSION))
})
test('filters stale session runtime selections without clearing unrelated keys', () => {
window.localStorage.setItem('unrelated-user-key', 'keep')
window.localStorage.setItem('cc-haha-session-runtime', JSON.stringify({
good: { providerId: null, modelId: 'claude-sonnet' },
alsoGood: { providerId: 'provider-1', modelId: 'gpt-5.4' },
bad: { providerId: 'provider-2' },
}))
runDesktopPersistenceMigrations()
expect(JSON.parse(window.localStorage.getItem('cc-haha-session-runtime') || '{}')).toEqual({
alsoGood: { providerId: 'provider-1', modelId: 'gpt-5.4' },
good: { providerId: null, modelId: 'claude-sonnet' },
})
expect(window.localStorage.getItem('unrelated-user-key')).toBe('keep')
})
test('removes malformed known keys without throwing during startup', () => {
window.localStorage.setItem('cc-haha-open-tabs', '{"openTabs":')
window.localStorage.setItem('cc-haha-theme', 'sepia')
const report = runDesktopPersistenceMigrations()
expect(report.migratedKeys).toContain('cc-haha-open-tabs')
expect(report.migratedKeys).toContain('cc-haha-theme')
expect(window.localStorage.getItem('cc-haha-open-tabs')).toBeNull()
expect(window.localStorage.getItem('cc-haha-theme')).toBeNull()
})
test('does not throw if schema version persistence is blocked', () => {
const storage = {
getItem: window.localStorage.getItem.bind(window.localStorage),
removeItem: window.localStorage.removeItem.bind(window.localStorage),
setItem: (key: string, value: string) => {
if (key === DESKTOP_PERSISTENCE_VERSION_KEY) {
throw new Error('storage blocked')
}
window.localStorage.setItem(key, value)
},
}
expect(() => runDesktopPersistenceMigrations(storage)).not.toThrow()
expect(runDesktopPersistenceMigrations(storage).migratedKeys).toContain(DESKTOP_PERSISTENCE_VERSION_KEY)
})
test('does not throw if storage reads and writes are blocked', () => {
const storage = {
getItem: () => {
throw new Error('storage unavailable')
},
removeItem: () => {
throw new Error('storage unavailable')
},
setItem: () => {
throw new Error('storage unavailable')
},
}
const report = runDesktopPersistenceMigrations(storage)
expect(report.migratedKeys).toEqual(expect.arrayContaining([
'cc-haha-open-tabs',
'cc-haha-session-runtime',
'cc-haha-theme',
'cc-haha-locale',
DESKTOP_PERSISTENCE_VERSION_KEY,
]))
})
})