feat: implement sprint 014 hermes renewal

This commit is contained in:
2026-04-05 15:22:21 +09:00
parent 49f075ae7c
commit 20f1416ede
12 changed files with 755 additions and 982 deletions

View File

@@ -7,19 +7,9 @@ describe('SprintSyncService', () => {
let service: SprintSyncService;
const mockPrisma = {
project: {
findUnique: jest.fn(),
},
sprint: {
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
task: {
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
project: { findUnique: jest.fn(), findMany: jest.fn() },
sprint: { findFirst: jest.fn(), create: jest.fn(), update: jest.fn() },
task: { findFirst: jest.fn(), create: jest.fn(), update: jest.fn() },
};
const mockGitea = {
@@ -39,10 +29,8 @@ describe('SprintSyncService', () => {
service = module.get<SprintSyncService>(SprintSyncService);
jest.clearAllMocks();
mockPrisma.project.findUnique.mockResolvedValue({
id: 1,
repoUrl: 'https://gitea.example.com/hanarang/project-alpha',
});
mockPrisma.project.findUnique.mockResolvedValue({ id: 1, repoUrl: 'https://gitea.example.com/hanarang/project-alpha' });
mockPrisma.project.findMany.mockResolvedValue([{ id: 1, repoUrl: 'https://gitea.example.com/hanarang/project-alpha' }]);
mockPrisma.sprint.findFirst.mockResolvedValue(null);
mockPrisma.sprint.create.mockResolvedValue({ id: 101, status: 'pending' });
mockPrisma.sprint.update.mockResolvedValue({ id: 101, status: 'pending' });
@@ -51,54 +39,64 @@ describe('SprintSyncService', () => {
mockPrisma.task.update.mockResolvedValue({});
mockGitea.getRepoTree.mockImplementation(async (_repoName: string, path: string) => {
if (path === '.plans/sprints/') return ['.plans/sprints/SPRINT-005.md'];
if (path === '.plans/sprints/') return ['.plans/sprints/SPRINT-005.md', '.plans/sprints/HOTFIX-001.md'];
if (path === '.plans/qa/') return ['.plans/qa/SPRINT-005-review-darang.md'];
if (path === '.qa/') return [];
if (path === '.plans/hotfix/') return ['.plans/hotfix/HOTFIX-001.md'];
return [];
});
});
it('syncProjectSprints: 담당 라인 기준 assignee를 저장한다', async () => {
mockGitea.getRawFile.mockResolvedValue(`# SPRINT-005: Infra\n\n## 목표\n테스트\n\n## 태스크\n\n### TASK-019: 인프라 배포\n- **담당:** 이랑이\n- **상태:** pending\n- **설명:** Nginx 프록시 + PM2 + SSL\n\n### TASK-020: QA 자동화\n- **담당:** 다랑이\n- **상태:** pending\n- **설명:** smoke test\n`);
it('담당 라인을 읽어 assignee를 저장한다', async () => {
mockGitea.getRawFile.mockImplementation(async (_repoName: string, path: string) => {
if (path.includes('SPRINT-005.md')) {
return `# SPRINT-005: Infra\n\n## 목표\n테스트\n\n## 태스크\n\n### TASK-019: 인프라 배포\n- **담당:** 이랑이\n- **설명:** deploy\n\n### TASK-020: QA 자동화\n- **담당:** darang\n- **설명:** qa\n`;
}
if (path.includes('review')) return 'passed: true';
return '# HOTFIX-001: sample';
});
await service.syncProjectSprints(1);
expect(mockPrisma.task.create).toHaveBeenNthCalledWith(1, expect.objectContaining({
data: expect.objectContaining({
taskId: 'TASK-019',
assignee: 'erang',
}),
data: expect.objectContaining({ taskId: 'TASK-019', assignee: 'erang', status: 'done' }),
}));
expect(mockPrisma.task.create).toHaveBeenNthCalledWith(2, expect.objectContaining({
data: expect.objectContaining({
taskId: 'TASK-020',
assignee: 'darang',
}),
data: expect.objectContaining({ taskId: 'TASK-020', assignee: 'darang', status: 'done' }),
}));
});
it('syncProjectSprints: 기존 task도 assignee를 갱신한다', async () => {
mockPrisma.task.findFirst.mockResolvedValue({ id: 501, assignee: 'narang', status: 'pending' });
mockGitea.getRawFile.mockResolvedValue(`# SPRINT-005: Infra\n\n## 목표\n테스트\n\n## 태스크\n\n### TASK-019: 인프라 배포\n- **담당:** 하랑이\n- **상태:** pending\n- **설명:** sync\n`);
it('기존 task도 assignee를 갱신한다', async () => {
mockPrisma.task.findFirst.mockResolvedValue({ id: 500, assignee: 'narang', status: 'pending' });
mockGitea.getRawFile.mockImplementation(async (_repoName: string, path: string) => {
if (path.includes('SPRINT-005.md')) {
return `# SPRINT-005: Infra\n\n## 목표\n테스트\n\n## 태스크\n\n### TASK-019: 인프라 배포\n- **담당:** 하랑이\n`;
}
if (path.includes('review')) return 'passed: false';
return '# HOTFIX-001: sample';
});
await service.syncProjectSprints(1);
expect(mockPrisma.task.update).toHaveBeenCalledWith(expect.objectContaining({
where: { id: 501 },
data: expect.objectContaining({
assignee: 'harang',
}),
where: { id: 500 },
data: expect.objectContaining({ assignee: 'harang' }),
}));
});
it('syncProjectSprints: 담당 라인이 없으면 narang으로 fallback한다', async () => {
mockGitea.getRawFile.mockResolvedValue(`# SPRINT-005: Infra\n\n## 목표\n테스트\n\n## 태스크\n\n### TASK-019: 인프라 배포\n- **상태:** pending\n- **설명:** sync\n`);
it('담당 라인이 없으면 narang fallback', async () => {
mockGitea.getRawFile.mockImplementation(async (_repoName: string, path: string) => {
if (path.includes('SPRINT-005.md')) {
return `# SPRINT-005: Infra\n\n## 목표\n테스트\n\n## 태스크\n\n### TASK-019: 인프라 배포\n- 설명만 있음\n`;
}
if (path.includes('review')) return 'FAILED';
return '# HOTFIX-001: sample';
});
await service.syncProjectSprints(1);
expect(mockPrisma.task.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
assignee: 'narang',
}),
data: expect.objectContaining({ assignee: 'narang', status: 'pending' }),
}));
});
});