feat: 通过加密会话和冲突设置连接桌面 Sync

This commit is contained in:
2026-09-08 16:03:49 +08:00
parent 3a4d6e5586
commit 91ef49442d
27 changed files with 1005 additions and 9 deletions
@@ -0,0 +1,37 @@
// @vitest-environment happy-dom
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, expect, it, vi } from 'vitest'
import { hostInvoke } from '@/services/platform/desktop'
import SyncSettings from './SyncSettings.vue'
vi.mock('@/services/platform/desktop', () => ({ hostInvoke: vi.fn() }))
const confirm = vi.hoisted(() => vi.fn())
vi.mock('@/composables/useActionDialog', () => ({ useActionDialog: () => ({ actionDialog: null, resolveAction: vi.fn(), askConfirm: confirm }) }))
afterEach(() => { vi.useRealTimers(); vi.resetAllMocks() })
const empty = () => ({ vault_id: 'local', binding: null, paused: false, pending: 0, conflicts: [], credential_state: 'unbound', running: false, error: null, retry_in: null })
it('clears login password, explicitly opts into HTTP and stops polling on unmount', async () => {
vi.useFakeTimers()
vi.mocked(hostInvoke).mockImplementation(async command => command === 'sync_status' ? empty() : command === 'sync_login' ? { endpoint: 'http://test.example:18080/', account: 'test' } : { items: [] })
const wrapper = mount(SyncSettings); await flushPromises()
await wrapper.get('input[type=url]').setValue('http://test.example:18080')
await wrapper.get('input[autocomplete=username]').setValue('test')
await wrapper.get('input[type=password]').setValue('private-fixture')
await wrapper.get('input[type=checkbox]').setValue(true)
await wrapper.get('form').trigger('submit'); await flushPromises()
expect(hostInvoke).toHaveBeenCalledWith('sync_login', { request: { endpoint: 'http://test.example:18080', account: 'test', password: 'private-fixture', device_name: 'OpenNexus Desktop', allow_test_http: true } })
expect((wrapper.get('input[type=password]').element as HTMLInputElement).value).toBe('')
expect(wrapper.text()).not.toContain('private-fixture')
wrapper.unmount(); const count = vi.mocked(hostInvoke).mock.calls.length
await vi.advanceTimersByTimeAsync(6000); expect(hostInvoke).toHaveBeenCalledTimes(count)
})
it('cancels destructive choices and binds accepted conflict decisions to their snapshot', async () => {
const state = { ...empty(), binding: { id: 'binding', endpoint: 'https://test.example/', account: 'test', remote_vault: 'remote', cursor: 7 }, credential_state: 'ready', conflicts: [{ sequence: 7, local_path: 'note.md', local_hash: 'original-hash', remote: { path: 'note.md', operation: 'put' } }] }
vi.mocked(hostInvoke).mockResolvedValue(state)
const wrapper = mount(SyncSettings); await flushPromises()
const button = wrapper.findAll('button').find(button => button.text() === '采用远端')!
confirm.mockResolvedValue(false); await button.trigger('click'); await flushPromises()
expect(vi.mocked(hostInvoke).mock.calls.some(([command]) => command === 'sync_resolve')).toBe(false)
confirm.mockResolvedValue(true); await button.trigger('click'); await flushPromises()
expect(hostInvoke).toHaveBeenCalledWith('sync_resolve', { bindingId: 'binding', sequence: 7, choice: 'remote', destination: '', expected: 'original-hash' })
expect(wrapper.get('input[type=url]').attributes('disabled')).toBeDefined()
wrapper.unmount()
})