// Git 同步:设置解析、状态文案、定时租约互斥、侧栏同步条渲染。 import { describe, it, expect, vi } from 'vitest' import { createRoot } from 'react-dom/client' import { act } from 'react-dom/test-utils' import { parseGitSyncSettings, gitStatusLabel, gitSyncCommitMessage, acquireGitSyncLease, releaseGitSyncLease, } from '../src/services/gitSync' import type { GitStatusInfo } from '../src/services/platform' vi.mock('../src/services/platform', async (importOriginal) => { const orig = await importOriginal() return { ...orig, listDir: async () => [] } }) import Sidebar from '../src/components/Sidebar' const base: GitStatusInfo = { isRepo: true, branch: 'main', remoteUrl: 'https://example.com/n.git', dirtyCount: 0, ahead: 0, behind: 0, hasUpstream: true, } describe('parseGitSyncSettings', () => { it('空/坏数据回默认(手动)', () => { expect(parseGitSyncSettings(null).intervalMin).toBe(0) expect(parseGitSyncSettings('{bad').intervalMin).toBe(0) expect(parseGitSyncSettings('{"intervalMin":-3}').intervalMin).toBe(0) }) it('正常解析间隔', () => { expect(parseGitSyncSettings('{"intervalMin":15}').intervalMin).toBe(15) }) }) describe('gitStatusLabel', () => { it('未启用 / 同步中 / 已同步', () => { expect(gitStatusLabel({ ...base, isRepo: false }, false)).toBe('未启用 Git 同步') expect(gitStatusLabel(base, true)).toBe('正在同步…') expect(gitStatusLabel(base, false)).toBe('main · 已同步') }) it('待提交/待推送/待拉取计数', () => { expect(gitStatusLabel({ ...base, dirtyCount: 2, ahead: 1, behind: 3 }, false)) .toBe('main · 待提交 2 处 · 待推送 1 · 待拉取 3') }) it('无远程时提示未设置远程', () => { expect(gitStatusLabel({ ...base, remoteUrl: null, hasUpstream: false }, false)) .toBe('main · 未设置远程 · 已同步') }) }) describe('gitSyncCommitMessage', () => { it('带本地时间戳与手动/自动区分', () => { const d = new Date(2026, 10, 14, 9, 5) expect(gitSyncCommitMessage(true, d)).toBe('自动同步 2026-11-14 09:05(new-markdown)') expect(gitSyncCommitMessage(false, d)).toContain('手动同步 2026-11-14 09:05') }) }) describe('定时同步租约', () => { it('别的窗口新鲜占用时取不到,过期/释放后可取', () => { localStorage.clear() const t0 = 1_000_000 expect(acquireGitSyncLease('C:/ws', 'win-a', t0)).toBe(true) expect(acquireGitSyncLease('C:/ws', 'win-b', t0 + 5_000)).toBe(false) expect(acquireGitSyncLease('C:/ws', 'win-a', t0 + 6_000)).toBe(true) // 自己续租 expect(acquireGitSyncLease('C:/ws', 'win-b', t0 + 40_000)).toBe(true) // 已过期 releaseGitSyncLease('C:/ws', 'win-a') // 不是持有者,无效 expect(acquireGitSyncLease('C:/ws', 'win-c', t0 + 41_000)).toBe(false) releaseGitSyncLease('C:/ws', 'win-b') expect(acquireGitSyncLease('C:/ws', 'win-c', t0 + 42_000)).toBe(true) }) }) describe('侧栏 Git 同步条', () => { async function renderBar(gitSync: Record) { const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) await act(async () => { root.render( undefined} workspaceRoot="C:/ws" activePath={null} onOpenFile={() => undefined} onOpenFolder={() => undefined} outline={[]} onJumpHeading={() => undefined} tauriMode gitSync={gitSync as never} />, ) }) await act(async () => { await new Promise((r) => setTimeout(r, 30)) }) return container } it('未启用时显示状态与设置按钮,无立即同步', async () => { const c = await renderBar({ isRepo: false, label: '未启用 Git 同步', syncing: false, lastMessage: null, lastIsError: false, onSyncNow: () => undefined, onOpenSettings: () => undefined, }) expect(c.textContent).toContain('Git 同步') expect(c.textContent).toContain('未启用 Git 同步') expect(c.querySelector('[aria-label="Git 同步设置"]')).not.toBeNull() expect(c.querySelector('[aria-label="立即同步"]')).toBeNull() }) it('仓库状态下显示立即同步且可点,结果行可见', async () => { const onSyncNow = vi.fn() const c = await renderBar({ isRepo: true, label: 'main · 待提交 2 处', syncing: false, lastMessage: '已提交本地更改', lastIsError: false, onSyncNow, onOpenSettings: () => undefined, }) expect(c.textContent).toContain('待提交 2 处') expect(c.textContent).toContain('已提交本地更改') const btn = c.querySelector('[aria-label="立即同步"]') as HTMLButtonElement await act(async () => { btn.click() }) expect(onSyncNow).toHaveBeenCalledTimes(1) }) })