fix(desktop): keep memory edits visible after an in-flight save

This commit is contained in:
程序员阿江(Relakkes)
2026-09-24 17:56:05 +08:00
parent 45435921b3
commit 589da89f6e
2 changed files with 94 additions and 3 deletions
+86 -1
View File
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import { MemorySettings } from '../pages/MemorySettings'
@@ -273,6 +273,91 @@ describe('MemorySettings', () => {
confirmSpy.mockRestore()
})
it('keeps edits made during a save in the editor until they are saved', async () => {
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false)
memoryApiMock.listFiles.mockResolvedValue({
files: [
{
path: 'MEMORY.md',
name: 'MEMORY.md',
title: 'MEMORY.md',
bytes: 18,
updatedAt: '2026-05-01T00:00:00.000Z',
isIndex: true,
},
{
path: 'notes/manual.md',
name: 'manual.md',
title: 'Manual',
bytes: 18,
updatedAt: '2026-05-01T00:00:00.000Z',
isIndex: false,
},
],
})
memoryApiMock.readFile.mockImplementation((_projectId: string, path: string) => Promise.resolve({
file: {
path,
content: path === 'MEMORY.md' ? '# Project Memory\n' : '# Manual\n',
updatedAt: '2026-05-01T00:00:00.000Z',
bytes: 18,
},
}))
let resolveFirstSave!: (value: { ok: true; file: { path: string; updatedAt: string; bytes: number } }) => void
memoryApiMock.saveFile.mockReturnValueOnce(new Promise((resolve) => { resolveFirstSave = resolve }))
render(<MemorySettings />)
fireEvent.click(await screen.findByRole('button', { name: 'Edit' }))
fireEvent.change(screen.getByLabelText('Editor'), { target: { value: '# First edit' } })
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
fireEvent.change(screen.getByLabelText('Editor'), { target: { value: '# First edit\nContinued typing' } })
await act(async () => {
resolveFirstSave({
ok: true,
file: { path: 'MEMORY.md', updatedAt: '2026-05-01T00:01:00.000Z', bytes: 12 },
})
})
expect(screen.getByLabelText('Editor')).toHaveValue('# First edit\nContinued typing')
expect(screen.getByText('Unsaved')).toBeInTheDocument()
fireEvent.click(screen.getByText('Manual'))
expect(confirmSpy).toHaveBeenCalled()
expect(memoryApiMock.readFile).not.toHaveBeenCalledWith('-workspace-demo', 'notes/manual.md')
expect(useMemoryStore.getState().draftContent).toBe('# First edit\nContinued typing')
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
await waitFor(() => expect(memoryApiMock.saveFile).toHaveBeenLastCalledWith({
projectId: '-workspace-demo',
path: 'MEMORY.md',
content: '# First edit\nContinued typing',
expectedUpdatedAt: '2026-05-01T00:01:00.000Z',
expectedBytes: 12,
}))
await waitFor(() => expect(screen.queryByLabelText('Editor')).not.toBeInTheDocument())
expect(screen.queryByText('Unsaved')).not.toBeInTheDocument()
confirmSpy.mockRestore()
})
it('asks before leaving a dirty file even when showing its preview', async () => {
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false)
memoryApiMock.listFiles.mockResolvedValue({ files: [
{ path: 'MEMORY.md', name: 'MEMORY.md', title: 'MEMORY.md', bytes: 18, updatedAt: '2026-05-01T00:00:00.000Z', isIndex: true },
{ path: 'notes/manual.md', name: 'manual.md', title: 'Manual', bytes: 18, updatedAt: '2026-05-01T00:00:00.000Z', isIndex: false },
] })
render(<MemorySettings />)
await screen.findByTestId('markdown-preview')
act(() => useMemoryStore.setState({ draftContent: '# Unsaved preview' }))
expect(screen.getByText('Unsaved')).toBeInTheDocument()
fireEvent.click(screen.getByText('Manual'))
expect(confirmSpy).toHaveBeenCalled()
expect(memoryApiMock.readFile).not.toHaveBeenCalledWith('-workspace-demo', 'notes/manual.md')
expect(useMemoryStore.getState().draftContent).toBe('# Unsaved preview')
confirmSpy.mockRestore()
})
it('saves with the platform shortcut while editing and returns to preview mode', async () => {
render(<MemorySettings />)
+8 -2
View File
@@ -124,7 +124,7 @@ export function MemorySettings() {
])
const canLeaveDirtyEdit = () => {
if (!isEditing || !isDirty) return true
if (!isDirty) return true
return window.confirm(t('settings.memory.discardUnsavedConfirm'))
}
@@ -161,7 +161,13 @@ export function MemorySettings() {
return
}
const saved = await saveFile()
if (saved) {
const current = useMemoryStore.getState()
if (
saved &&
current.selectedProjectId === selectedProjectId &&
current.selectedFile?.path === selectedFile.path &&
current.draftContent === current.selectedFile.content
) {
setIsEditing(false)
}
}