Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读 - 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移 - 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
219 lines
7.2 KiB
TypeScript
219 lines
7.2 KiB
TypeScript
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 = {
|
|
user: {
|
|
findFirst: createUserFindFirstMock(),
|
|
},
|
|
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('resolves assignee and creator names to user ids before relation writes', async () => {
|
|
const { prisma, service } = makeService();
|
|
prisma.user.findFirst = createUserFindFirstMock({ 张三: 'dev-1', 产品经理: 'pm-1' });
|
|
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', {
|
|
title: '开发登录',
|
|
assigneeId: '张三',
|
|
createdBy: '产品经理',
|
|
} as any);
|
|
|
|
expect(prisma.user.findFirst).toHaveBeenCalledTimes(2);
|
|
expect(prisma.devTask.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
assigneeId: 'dev-1',
|
|
creatorId: 'pm-1',
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('resolves transfer assignee names before updating foreign keys', async () => {
|
|
const { prisma, workActivity, service } = makeService();
|
|
prisma.user.findFirst = createUserFindFirstMock({ 李四: 'dev-2' });
|
|
prisma.devTask.findFirst.mockResolvedValue({
|
|
id: 'task-1',
|
|
versionId: 'version-1',
|
|
productId: 'product-1',
|
|
projectId: 'project-1',
|
|
title: '开发登录',
|
|
assigneeId: 'dev-1',
|
|
});
|
|
prisma.devTask.update.mockResolvedValue({
|
|
id: 'task-1',
|
|
versionId: 'version-1',
|
|
productId: 'product-1',
|
|
projectId: 'project-1',
|
|
title: '开发登录',
|
|
assigneeId: 'dev-2',
|
|
});
|
|
|
|
await service.transfer('version-1', 'task-1', '李四');
|
|
|
|
expect(prisma.devTask.update).toHaveBeenCalledWith({
|
|
where: { id_versionId: { id: 'task-1', versionId: 'version-1' } },
|
|
data: { assigneeId: 'dev-2' },
|
|
});
|
|
expect(workActivity.record).toHaveBeenCalledWith(expect.objectContaining({
|
|
metadata: expect.objectContaining({ fromAssigneeId: 'dev-1', toAssigneeId: 'dev-2' }),
|
|
}));
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
function createUserFindFirstMock(mapping: Record<string, string> = {}) {
|
|
return jest.fn(({ where }: any) => {
|
|
const refs = (where?.OR ?? [])
|
|
.flatMap((condition: Record<string, string>) => Object.values(condition))
|
|
.filter(Boolean);
|
|
const ref = refs[0];
|
|
return Promise.resolve(ref ? { id: mapping[ref] ?? ref } : null);
|
|
});
|
|
}
|