- Adds syncAllMutation calling trpc.syncAllProjects once for all cards - Shows Sync All button in header when ≥1 project exists (hidden otherwise) - Propagates isPending to ProjectCard.syncAllPending to disable per-card sync - On success: toasts all-success or failure count; invalidates listProjects + getProjectSyncStatus per project - On network error: toasts Sync failed with err.message - Adds unit tests for ProjectSyncManager.syncAllProjects: empty list, all succeed, partial failure, result shape, and failure counting logic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
173 lines
5.1 KiB
TypeScript
173 lines
5.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { ProjectSyncManager, type SyncResult } from './remote-sync.js'
|
|
import type { ProjectRepository } from '../db/repositories/project-repository.js'
|
|
|
|
vi.mock('simple-git', () => ({
|
|
simpleGit: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('./project-clones.js', () => ({
|
|
ensureProjectClone: vi.fn().mockResolvedValue('/tmp/fake-clone'),
|
|
}))
|
|
|
|
vi.mock('../logger/index.js', () => ({
|
|
createModuleLogger: () => ({
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
}),
|
|
}))
|
|
|
|
function makeRepo(overrides: Partial<ProjectRepository> = {}): ProjectRepository {
|
|
return {
|
|
findAll: vi.fn().mockResolvedValue([]),
|
|
findById: vi.fn().mockResolvedValue(null),
|
|
create: vi.fn(),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
delete: vi.fn(),
|
|
findProjectsByInitiativeId: vi.fn().mockResolvedValue([]),
|
|
setInitiativeProjects: vi.fn().mockResolvedValue(undefined),
|
|
...overrides,
|
|
} as unknown as ProjectRepository
|
|
}
|
|
|
|
const project1 = {
|
|
id: 'proj-1',
|
|
name: 'alpha',
|
|
url: 'https://github.com/org/alpha',
|
|
defaultBranch: 'main',
|
|
lastFetchedAt: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
}
|
|
|
|
const project2 = {
|
|
id: 'proj-2',
|
|
name: 'beta',
|
|
url: 'https://github.com/org/beta',
|
|
defaultBranch: 'main',
|
|
lastFetchedAt: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
}
|
|
|
|
describe('ProjectSyncManager', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let simpleGitMock: any
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import('simple-git')
|
|
simpleGitMock = vi.mocked(mod.simpleGit)
|
|
simpleGitMock.mockReset()
|
|
})
|
|
|
|
describe('syncAllProjects', () => {
|
|
it('returns empty array when no projects exist', async () => {
|
|
const repo = makeRepo({ findAll: vi.fn().mockResolvedValue([]) })
|
|
const manager = new ProjectSyncManager(repo, '/workspace')
|
|
const results = await manager.syncAllProjects()
|
|
expect(results).toEqual([])
|
|
})
|
|
|
|
it('returns success result for each project when all succeed', async () => {
|
|
const mockGit = {
|
|
fetch: vi.fn().mockResolvedValue({}),
|
|
raw: vi.fn().mockResolvedValue(''),
|
|
}
|
|
simpleGitMock.mockReturnValue(mockGit)
|
|
|
|
const repo = makeRepo({
|
|
findAll: vi.fn().mockResolvedValue([project1, project2]),
|
|
findById: vi.fn()
|
|
.mockResolvedValueOnce(project1)
|
|
.mockResolvedValueOnce(project2),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
})
|
|
const manager = new ProjectSyncManager(repo, '/workspace')
|
|
const results = await manager.syncAllProjects()
|
|
|
|
expect(results).toHaveLength(2)
|
|
expect(results[0]).toMatchObject({
|
|
projectId: 'proj-1',
|
|
projectName: 'alpha',
|
|
success: true,
|
|
fetched: true,
|
|
})
|
|
expect(results[1]).toMatchObject({
|
|
projectId: 'proj-2',
|
|
projectName: 'beta',
|
|
success: true,
|
|
fetched: true,
|
|
})
|
|
})
|
|
|
|
it('returns partial failure when the second project fetch throws', async () => {
|
|
const mockGitSuccess = {
|
|
fetch: vi.fn().mockResolvedValue({}),
|
|
raw: vi.fn().mockResolvedValue(''),
|
|
}
|
|
const mockGitFail = {
|
|
fetch: vi.fn().mockRejectedValue(new Error('network error')),
|
|
raw: vi.fn().mockResolvedValue(''),
|
|
}
|
|
simpleGitMock
|
|
.mockReturnValueOnce(mockGitSuccess)
|
|
.mockReturnValueOnce(mockGitFail)
|
|
|
|
const repo = makeRepo({
|
|
findAll: vi.fn().mockResolvedValue([project1, project2]),
|
|
findById: vi.fn()
|
|
.mockResolvedValueOnce(project1)
|
|
.mockResolvedValueOnce(project2),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
})
|
|
const manager = new ProjectSyncManager(repo, '/workspace')
|
|
const results = await manager.syncAllProjects()
|
|
|
|
expect(results).toHaveLength(2)
|
|
expect(results[0]).toMatchObject({ projectId: 'proj-1', success: true })
|
|
expect(results[1]).toMatchObject({
|
|
projectId: 'proj-2',
|
|
success: false,
|
|
error: expect.any(String),
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('SyncResult shape', () => {
|
|
it('result always contains projectId and success fields', async () => {
|
|
const mockGit = {
|
|
fetch: vi.fn().mockResolvedValue({}),
|
|
raw: vi.fn().mockResolvedValue(''),
|
|
}
|
|
simpleGitMock.mockReturnValue(mockGit)
|
|
|
|
const repo = makeRepo({
|
|
findAll: vi.fn().mockResolvedValue([project1]),
|
|
findById: vi.fn().mockResolvedValue(project1),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
})
|
|
const manager = new ProjectSyncManager(repo, '/workspace')
|
|
const results = await manager.syncAllProjects()
|
|
|
|
expect(results[0]).toMatchObject({
|
|
projectId: expect.any(String),
|
|
success: expect.any(Boolean),
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('failure counting logic', () => {
|
|
it('counts failures from SyncResult array', () => {
|
|
const results: Pick<SyncResult, 'success'>[] = [
|
|
{ success: true },
|
|
{ success: false },
|
|
{ success: true },
|
|
{ success: false },
|
|
]
|
|
const failed = results.filter(r => !r.success)
|
|
expect(failed.length).toBe(2)
|
|
})
|
|
})
|
|
})
|