fix: avoid history reclamation during structure-only upgrades

Ordinary database schema upgrades previously ran VACUUM on free pages and
showed misleading history-cleanup progress. Separate structure upgrades from
required legacy conversions, retaining reclamation intent only for retries
within the same startup. Preserve released data without new schema versions
or persistent migration markers; Agent and normal writes remain unchanged.

Add real SQLite and Worker regressions, bilingual operation-specific startup
copy, and migration documentation. Synchronize native Electron drag input
with actual resize state without weakening layout assertions.

Validation: 47 focused migration tests passed; typecheck, lint and diff checks
passed. Final full suite: 5137 passed, 74 skipped, 2 unrelated Electron failures.
Both failing Electron tests passed an isolated rerun; full CI remains unverified.

Release note: 修复普通数据库结构升级再次整理历史和回收空间的问题;仅在需要转换旧数据时执行相关处理,启动页准确显示当前升级操作。
This commit is contained in:
mesalogo
2026-09-26 12:42:08 +08:00
parent e6ac0734de
commit 4c7193e1ea
21 changed files with 441 additions and 61 deletions
+31
View File
@@ -2,9 +2,40 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
import { afterEach, expect, it, vi } from 'vitest'
import { StorageUpgrade } from './StorageUpgrade'
import type { DesktopApi } from '../../shared/contracts'
import { UiLocaleProvider } from './i18n/UiLocaleProvider'
afterEach(() => { cleanup(); vi.unstubAllGlobals() })
it.each(['zh-CN', 'en-US'] as const)('describes schema-only upgrades without claiming history conversion or reclamation (%s)', async (locale) => {
vi.stubGlobal('goodbuddy', { storageUpgrade: {
getProgress: vi.fn(async () => ({
stage: 'upgrading', processed: 0, total: 0, bytesBefore: 100_000
})),
act: vi.fn(async () => undefined)
} } as unknown as DesktopApi)
render(<UiLocaleProvider initialPreference={locale}><StorageUpgrade /></UiLocaleProvider>)
expect(await screen.findByRole('status')).toHaveTextContent(locale === 'zh-CN'
? '正在更新数据库结构' : 'Applying database structure updates')
expect(screen.getByRole('heading')).toHaveTextContent(locale === 'zh-CN'
? '正在升级本地数据结构' : 'Upgrading local data structures')
expect(screen.queryByText(/清理子任务进度的重复副本|removes repeated copies of subagent progress/)).not.toBeInTheDocument()
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
})
it.each(['zh-CN', 'en-US'] as const)('only describes space reclamation when it is actually running (%s)', async (locale) => {
vi.stubGlobal('goodbuddy', { storageUpgrade: {
getProgress: vi.fn(async () => ({
stage: 'compacting', processed: 0, total: 0, bytesBefore: 100_000
})),
act: vi.fn(async () => undefined)
} } as unknown as DesktopApi)
render(<UiLocaleProvider initialPreference={locale}><StorageUpgrade /></UiLocaleProvider>)
await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent(locale === 'zh-CN'
? '正在回收磁盘空间' : 'Reclaiming disk space'))
expect(screen.getByRole('heading')).toHaveTextContent(locale === 'zh-CN'
? '正在回收数据库空间' : 'Reclaiming database space')
})
it('shows real progress and lets the user quit without deleting records', async () => {
const act = vi.fn(async () => undefined)
vi.stubGlobal('goodbuddy', { storageUpgrade: {
+23 -8
View File
@@ -40,17 +40,24 @@ export function StorageUpgrade(): React.JSX.Element {
void poll()
return () => { active = false; clearTimeout(timer) }
}, [])
const title = en ? 'Optimizing saved execution history' : '正在优化历史执行记录'
const stage = progress?.stage
const convertingHistory = stage === 'scanning' || stage === 'converting'
const title = stage === 'compacting'
? en ? 'Reclaiming database space' : '正在回收数据库空间'
: convertingHistory
? en ? 'Upgrading saved history' : '正在转换旧版历史数据'
: en ? 'Upgrading local data structures' : '正在升级本地数据结构'
const status = stage === 'failed'
? progress?.error
: stage === 'compacting'
? en ? 'Reclaiming disk space…' : '正在回收磁盘空间…'
: stage === 'complete'
? en ? 'Optimization complete. Starting GoodBuddy…' : '优化完成,正在启动 GoodBuddy…'
? en ? 'Data upgrade complete. Starting GoodBuddy…' : '数据升级完成,正在启动 GoodBuddy…'
: progress?.stage === 'converting'
? `${en ? 'Processed' : '已处理'} ${progress.processed.toLocaleString()} / ${progress.total.toLocaleString()}`
: en ? 'Checking saved execution history…' : '正在检查历史执行记录…'
: stage === 'scanning'
? en ? 'Checking saved history…' : '正在检查旧版历史数据…'
: en ? 'Applying database structure updates…' : '正在更新数据库结构…'
const act = async (action: 'retry' | 'quit'): Promise<void> => {
try {
setActionError('')
@@ -65,9 +72,17 @@ export function StorageUpgrade(): React.JSX.Element {
<PageHeader
headingId="storage-upgrade-title"
title={title}
description={en
? 'This update removes repeated copies of subagent progress. Your chats, execution details and results are preserved. Time depends on the history size and disk speed.'
: '本次更新清理子任务进度的重复副本,保留聊天、执行详情和结果。处理时间取决于历史数据量和磁盘速度。'}
description={convertingHistory
? en
? 'Converting older stored data to the current format. Your chats, execution details and results are preserved. Time depends on the data size and disk speed.'
: '正在将旧版存储数据转换为当前格式,保留聊天、执行详情和结果。处理时间取决于数据量和磁盘速度。'
: stage === 'compacting'
? en
? 'Reclaiming space after a required data conversion. Your chats, execution details and results are preserved.'
: '正在回收数据转换后释放的空间,保留聊天、执行详情和结果。'
: en
? 'Updating the local database structure while preserving your chats, execution details and results. A structure update does not automatically reorganize history or reclaim disk space.'
: '正在更新本地数据库结构,保留聊天、执行详情和结果。结构升级不会自动整理历史或回收磁盘空间。'}
/>
<p role={stage === 'failed' ? 'alert' : 'status'}>{status}</p>
{progress?.stage === 'converting' && progress.total > 0 && (
@@ -77,8 +92,8 @@ export function StorageUpgrade(): React.JSX.Element {
<p>{en ? 'Processing speed' : '处理速度'}:{Math.round(rate).toLocaleString()} {en ? 'events/s' : '条/秒'}</p>
)}
<p>{en
? 'You can quit and retry on the next launch. Completed batches are preserved.'
: '可以退出并在下次启动时继续处理,已完成的批次会保留。'}</p>
? 'You can quit and continue any remaining required upgrades on the next launch. Committed changes are preserved.'
: '可以退出,下次启动时继续尚未完成的必要升级;已提交的变更会保留。'}</p>
{actionError && <p role="alert">{actionError}</p>}
{stage === 'failed' && (
<button className="primary-button" onClick={() => void act('retry')}>