feat(v2.4): 统一领域状态契约

This commit is contained in:
2026-07-08 11:53:07 +08:00
parent aaaff6aa1f
commit d82e8729af
8 changed files with 401 additions and 22 deletions

View File

@@ -42,7 +42,7 @@ describe('RequirementService with V2.2 composite requirement key', () => {
prisma.requirement.findFirst.mockResolvedValue({
id: 'req-1',
productId: 'product-1',
status: 'draft',
status: 'pending_review',
});
prisma.requirement.update.mockResolvedValue({ id: 'req-1', productId: 'product-1' });
@@ -59,7 +59,7 @@ describe('RequirementService with V2.2 composite requirement key', () => {
prisma.requirement.findFirst.mockResolvedValue({
id: 'req-1',
productId: 'product-1',
status: 'draft',
status: 'pending_review',
});
prisma.requirement.delete.mockResolvedValue({ id: 'req-1', productId: 'product-1' });
@@ -69,4 +69,48 @@ describe('RequirementService with V2.2 composite requirement key', () => {
where: { id_productId: { id: 'req-1', productId: 'product-1' } },
});
});
it.each([
['pending_review', 'adopted'],
['pending_review', 'rejected'],
['rejected', 'pending_review'],
['adopted', 'planned'],
['planned', 'developing'],
['developing', 'testing'],
['testing', 'released'],
['released', 'closed'],
])('allows current requirement workflow transition %s -> %s', async (fromStatus, toStatus) => {
const { prisma, service } = makeService();
prisma.requirement.findFirst.mockResolvedValue({
id: 'req-1',
productId: 'product-1',
status: fromStatus,
});
prisma.requirement.update.mockResolvedValue({
id: 'req-1',
productId: 'product-1',
status: toStatus,
});
await service.updateStatus('product-1', 'req-1', toStatus);
expect(prisma.requirement.update).toHaveBeenCalledWith({
where: { id_productId: { id: 'req-1', productId: 'product-1' } },
data: { status: toStatus },
});
});
it('rejects skipping adopted requirements directly to development', async () => {
const { prisma, service } = makeService();
prisma.requirement.findFirst.mockResolvedValue({
id: 'req-1',
productId: 'product-1',
status: 'adopted',
});
await expect(service.updateStatus('product-1', 'req-1', 'developing')).rejects.toThrow(
'无法从 "adopted" 转换到 "developing"',
);
expect(prisma.requirement.update).not.toHaveBeenCalled();
});
});

View File

@@ -5,11 +5,14 @@ import { CreateRequirementDto } from './dto/create-requirement.dto';
import { UpdateRequirementDto } from './dto/update-requirement.dto';
const VALID_TRANSITIONS: Record<string, string[]> = {
[RequirementStatus.DRAFT]: [RequirementStatus.REVIEWING],
[RequirementStatus.REVIEWING]: [RequirementStatus.APPROVED, RequirementStatus.REJECTED],
[RequirementStatus.APPROVED]: [RequirementStatus.DELIVERED],
[RequirementStatus.REJECTED]: [RequirementStatus.DRAFT],
[RequirementStatus.DELIVERED]: [],
[RequirementStatus.PENDING_REVIEW]: [RequirementStatus.ADOPTED, RequirementStatus.REJECTED],
[RequirementStatus.REJECTED]: [RequirementStatus.PENDING_REVIEW],
[RequirementStatus.ADOPTED]: [RequirementStatus.PLANNED],
[RequirementStatus.PLANNED]: [RequirementStatus.DEVELOPING],
[RequirementStatus.DEVELOPING]: [RequirementStatus.TESTING],
[RequirementStatus.TESTING]: [RequirementStatus.RELEASED],
[RequirementStatus.RELEASED]: [RequirementStatus.CLOSED],
[RequirementStatus.CLOSED]: [],
};
function createFallbackRequirementCode() {
@@ -30,6 +33,7 @@ export class RequirementService {
code: dto.code?.trim() || createFallbackRequirementCode(),
title: dto.title,
description: dto.description || '',
status: RequirementStatus.PENDING_REVIEW,
priority: dto.priority ?? 0,
creatorId: dto.creatorId,
},