Files
Codewalkers/apps/server/git/remote-sync.test.ts
Lukas May 28521e1c20 chore: merge main into cw/small-change-flow
Integrates main branch changes (headquarters dashboard, task retry count,
agent prompt persistence, remote sync improvements) with the initiative's
errand agent feature. Both features coexist in the merged result.

Key resolutions:
- Schema: take main's errands table (nullable projectId, no conflictFiles,
  with errandsRelations); migrate to 0035_faulty_human_fly
- Router: keep both errandProcedures and headquartersProcedures
- Errand prompt: take main's simpler version (no question-asking flow)
- Manager: take main's status check (running|idle only, no waiting_for_input)
- Tests: update to match removed conflictFiles field and undefined vs null
2026-03-06 16:48:12 +01:00

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)
})
})
})