feat(v2.4): 切换计划与开发任务主写

This commit is contained in:
2026-07-08 12:34:33 +08:00
parent 5f5a211c99
commit 1bf630a7bc
21 changed files with 1612 additions and 28 deletions

View File

@@ -0,0 +1,153 @@
import { NotFoundException } from '@nestjs/common';
import { DevTaskService } from './dev-task.service';
describe('DevTaskService domain writes', () => {
const makeService = () => {
const workActivity = {
record: jest.fn().mockResolvedValue({ id: 'activity-1' }),
};
const prisma = {
version: {
findUnique: jest.fn(),
},
devTask: {
create: jest.fn(),
delete: jest.fn(),
findFirst: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
},
};
return {
prisma,
workActivity,
service: new DevTaskService(prisma as any, workActivity as any),
};
};
it('creates dev tasks directly under a version partition', async () => {
const { prisma, workActivity, service } = makeService();
prisma.version.findUnique.mockResolvedValue({ id: 'version-1', productId: 'product-1', projectId: 'project-1' });
prisma.devTask.create.mockResolvedValue({ id: 'task-1', versionId: 'version-1', title: '开发登录' });
await service.create('version-1', {
requirementId: 'req-1',
requirementProductId: 'product-1',
taskNo: 'DEV-001',
title: '开发登录',
categoryId: 'cat-fe',
assigneeId: 'member-1',
priority: 'P1',
expectedStartAt: '2026-07-08T09:00:00.000Z',
expectedEndAt: '2026-07-08T18:00:00.000Z',
estimateHours: 8,
references: [{ type: 'requirement', id: 'REQ-001', label: 'REQ-001 登录' }],
createdBy: 'member-pm',
} as any);
expect(prisma.devTask.create).toHaveBeenCalledWith({
data: expect.objectContaining({
versionId: 'version-1',
productId: 'product-1',
projectId: 'project-1',
requirementId: 'req-1',
requirementProductId: 'product-1',
code: 'DEV-001',
title: '开发登录',
categoryId: 'cat-fe',
assigneeId: 'member-1',
priority: 1,
expectedStartAt: new Date('2026-07-08T09:00:00.000Z'),
expectedEndAt: new Date('2026-07-08T18:00:00.000Z'),
estimateHours: 8,
}),
});
expect(workActivity.record).toHaveBeenCalledWith(expect.objectContaining({
versionId: 'version-1',
sourceType: 'dev_task',
sourceId: 'task-1',
action: 'dev_task_created',
}));
});
it('changes status by id plus version id and records activity evidence', async () => {
const { prisma, workActivity, service } = makeService();
prisma.devTask.findFirst.mockResolvedValue({
id: 'task-1',
versionId: 'version-1',
productId: 'product-1',
projectId: 'project-1',
status: 'todo',
title: '开发登录',
assigneeId: 'member-1',
});
prisma.devTask.update.mockResolvedValue({
id: 'task-1',
versionId: 'version-1',
productId: 'product-1',
projectId: 'project-1',
status: 'in_progress',
title: '开发登录',
assigneeId: 'member-1',
});
await service.updateStatus('version-1', 'task-1', 'in_progress');
expect(prisma.devTask.update).toHaveBeenCalledWith({
where: { id_versionId: { id: 'task-1', versionId: 'version-1' } },
data: expect.objectContaining({
status: 'in_progress',
startDate: expect.any(Date),
}),
});
expect(workActivity.record).toHaveBeenCalledWith(expect.objectContaining({
versionId: 'version-1',
sourceType: 'dev_task',
sourceId: 'task-1',
action: 'dev_task_started',
}));
});
it('blocks and unblocks tasks inside the version partition', async () => {
const { prisma, workActivity, service } = makeService();
prisma.devTask.findFirst.mockResolvedValue({
id: 'task-1',
versionId: 'version-1',
productId: 'product-1',
projectId: 'project-1',
status: 'in_progress',
title: '开发登录',
assigneeId: 'member-1',
});
prisma.devTask.update.mockResolvedValue({
id: 'task-1',
versionId: 'version-1',
productId: 'product-1',
projectId: 'project-1',
isBlocked: true,
blockReason: '接口未就绪',
title: '开发登录',
assigneeId: 'member-1',
});
await service.setBlocked('version-1', 'task-1', true, '接口未就绪');
expect(prisma.devTask.update).toHaveBeenCalledWith({
where: { id_versionId: { id: 'task-1', versionId: 'version-1' } },
data: { isBlocked: true, blockReason: '接口未就绪' },
});
expect(workActivity.record).toHaveBeenCalledWith(expect.objectContaining({
action: 'dev_task_blocked',
metadata: expect.objectContaining({ blocker: '接口未就绪' }),
}));
});
it('rejects updates outside the version partition', async () => {
const { prisma, service } = makeService();
prisma.devTask.findFirst.mockResolvedValue(null);
await expect(service.update('version-1', 'missing-task', { title: 'Ghost' })).rejects.toBeInstanceOf(NotFoundException);
expect(prisma.devTask.update).not.toHaveBeenCalled();
});
});