mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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.
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const doctorApiMock = vi.hoisted(() => ({
|
|
reportAndRepair: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../api/doctor', () => ({
|
|
doctorApi: doctorApiMock,
|
|
}))
|
|
|
|
import { SAFE_DOCTOR_STORAGE_KEYS, runLocalDoctorRepair, runDoctorRepair } from './doctorRepair'
|
|
|
|
describe('doctorRepair', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('clears only the safe desktop UI storage keys', () => {
|
|
window.localStorage.clear()
|
|
for (const key of SAFE_DOCTOR_STORAGE_KEYS) {
|
|
window.localStorage.setItem(key, `${key}-value`)
|
|
}
|
|
window.localStorage.setItem('cc-haha-chat-history', 'preserve')
|
|
window.localStorage.setItem('cc-haha-provider-config', 'preserve')
|
|
|
|
const result = runLocalDoctorRepair(window.localStorage)
|
|
|
|
expect(result.removedKeys).toEqual(expect.arrayContaining([...SAFE_DOCTOR_STORAGE_KEYS]))
|
|
expect(result.failedKeys).toEqual([])
|
|
for (const key of SAFE_DOCTOR_STORAGE_KEYS) {
|
|
expect(window.localStorage.getItem(key)).toBeNull()
|
|
}
|
|
expect(window.localStorage.getItem('cc-haha-chat-history')).toBe('preserve')
|
|
expect(window.localStorage.getItem('cc-haha-provider-config')).toBe('preserve')
|
|
})
|
|
|
|
it('keeps local repair non-throwing when storage access is blocked', () => {
|
|
const storage = {
|
|
getItem: () => {
|
|
throw new Error('storage unavailable')
|
|
},
|
|
removeItem: () => {
|
|
throw new Error('storage unavailable')
|
|
},
|
|
}
|
|
|
|
const result = runLocalDoctorRepair(storage)
|
|
|
|
expect(result.removedKeys).toEqual([])
|
|
expect(result.failedKeys).toEqual(expect.arrayContaining([...SAFE_DOCTOR_STORAGE_KEYS]))
|
|
})
|
|
|
|
it('keeps local repair successful when the server doctor endpoint is unavailable', async () => {
|
|
window.localStorage.clear()
|
|
window.localStorage.setItem('cc-haha-theme', 'dark')
|
|
doctorApiMock.reportAndRepair.mockRejectedValueOnce(new Error('Failed to fetch'))
|
|
|
|
const result = await runDoctorRepair({ storage: window.localStorage })
|
|
|
|
expect(doctorApiMock.reportAndRepair).toHaveBeenCalled()
|
|
expect(result.local.removedKeys).toContain('cc-haha-theme')
|
|
expect(result.server).toBeNull()
|
|
expect(result.serverError).toBe('Failed to fetch')
|
|
})
|
|
})
|