Files
ftb-project-management/apps/web/lib/version-data-scope.test.ts
2e595c7e72
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
refactor(data): 收口关系表运行时数据源
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读
- 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移
- 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
2026-07-09 14:59:49 +08:00

268 lines
9.2 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { buildVersionDataScope, buildVersionDataScopeMap, selectVersionDataScope } from './version-data-scope';
import type { Bug } from './bug';
import type { DevTask } from './dev-task';
import type { OvertimeRecord } from './overtime';
import type { Requirement } from './requirement';
import type { TestCase } from './test-case';
import type { VersionPlan } from './version-plan';
function requirement(id: string, versionId: string | undefined): Requirement {
return {
id,
code: id.toUpperCase(),
title: `Requirement ${id}`,
description: '',
productId: 'product-1',
projectId: 'project-1',
versionId,
sourceType: 'internal',
sourceTarget: 'product',
platforms: ['web'],
typeId: 'type-1',
status: 'planned',
priority: 'P1',
effort: 'M',
creator: 'PM',
createdAt: '2026-07-02T09:00:00.000Z',
};
}
function plan(id: string, versionId: string, type: VersionPlan['type']): VersionPlan {
return {
id,
versionId,
type,
title: `Plan ${id}`,
owner: 'PM',
startTime: '2026-07-02T09:00:00.000Z',
endTime: '2026-07-02T18:00:00.000Z',
status: 'pending',
createdAt: '2026-07-02T09:00:00.000Z',
addedBy: 'PM',
};
}
function devTask(id: string, patch: Partial<DevTask>): DevTask {
return {
id,
taskNo: id.toUpperCase(),
versionId: undefined,
requirementId: '',
title: `Task ${id}`,
categoryId: 'frontend',
assigneeId: 'Alice',
priority: 'P1',
expectedStartAt: '',
expectedEndAt: '',
status: 'todo',
isBlocked: false,
createdBy: 'PM',
createdAt: '2026-07-02T09:00:00.000Z',
updatedAt: '2026-07-02T09:00:00.000Z',
...patch,
};
}
function testCase(id: string, versionId: string): TestCase {
return {
id,
caseNo: id.toUpperCase(),
versionId,
title: `Case ${id}`,
categoryId: 'testing',
priority: 'P1',
status: 'pending',
createdBy: 'QA',
createdAt: '2026-07-02T09:00:00.000Z',
updatedAt: '2026-07-02T09:00:00.000Z',
};
}
function bug(id: string, versionId: string): Bug {
return {
id,
bugNo: id.toUpperCase(),
versionId,
testCaseId: 'tc-1',
title: `Bug ${id}`,
description: '',
severity: 'major',
priority: 'P1',
reportedBy: 'QA',
assigneeId: 'Bob',
status: 'open',
createdAt: '2026-07-02T09:00:00.000Z',
updatedAt: '2026-07-02T09:00:00.000Z',
};
}
function overtime(id: string, versionId: string): OvertimeRecord {
return {
id,
projectId: 'project-1',
versionId,
person: 'Alice',
startTime: '2026-07-02T19:00:00.000Z',
endTime: '2026-07-02T21:00:00.000Z',
duration: 2,
reasonId: 'reason-4',
createdAt: '2026-07-02T21:00:00.000Z',
};
}
test('buildVersionDataScope scopes version detail data once', () => {
const scope = buildVersionDataScope({
versionId: 'version-1',
requirements: [requirement('req-1', 'version-1'), requirement('req-2', 'version-2')],
plans: [plan('plan-1', 'version-1', 'research'), plan('plan-2', 'version-1', 'product'), plan('plan-3', 'version-2', 'ui')],
devTasks: [
devTask('dev-direct', { versionId: 'version-1', requirementId: '' }),
devTask('dev-legacy', { requirementId: 'req-1' }),
devTask('dev-other', { versionId: 'version-2', requirementId: 'req-2' }),
],
testCases: [testCase('tc-1', 'version-1'), testCase('tc-2', 'version-2')],
bugs: [bug('bug-1', 'version-1'), bug('bug-2', 'version-2')],
overtimeRecords: [overtime('ot-1', 'version-1'), overtime('ot-2', 'version-2')],
});
assert.deepEqual(scope.requirementIds, ['req-1']);
assert.deepEqual(scope.plans.map((item) => item.id), ['plan-1', 'plan-2']);
assert.deepEqual(scope.plansByType.research.map((item) => item.id), ['plan-1']);
assert.deepEqual(scope.plansByType.product.map((item) => item.id), ['plan-2']);
assert.deepEqual(scope.devTasks.map((item) => item.id), ['dev-direct', 'dev-legacy']);
assert.deepEqual(scope.testCases.map((item) => item.id), ['tc-1']);
assert.deepEqual(scope.bugs.map((item) => item.id), ['bug-1']);
assert.deepEqual(scope.overtimeRecords.map((item) => item.id), ['ot-1']);
});
test('buildVersionDataScopeMap indexes many versions in one pass', () => {
const scopeMap = buildVersionDataScopeMap({
versionIds: ['version-1', 'version-2', 'version-empty'],
requirements: [
requirement('req-1', 'version-1'),
requirement('req-2', 'version-2'),
requirement('req-outside', 'version-3'),
],
plans: [
plan('plan-1', 'version-1', 'research'),
plan('plan-2', 'version-2', 'ui'),
plan('plan-outside', 'version-3', 'product'),
],
devTasks: [
devTask('dev-direct', { versionId: 'version-2', requirementId: 'req-1' }),
devTask('dev-legacy', { requirementId: 'req-1' }),
devTask('dev-outside', { requirementId: 'req-outside' }),
],
testCases: [testCase('tc-1', 'version-1'), testCase('tc-2', 'version-2'), testCase('tc-outside', 'version-3')],
bugs: [bug('bug-1', 'version-1'), bug('bug-2', 'version-2'), bug('bug-outside', 'version-3')],
overtimeRecords: [overtime('ot-1', 'version-1'), overtime('ot-2', 'version-2'), overtime('ot-outside', 'version-3')],
});
assert.deepEqual(scopeMap['version-1'].requirements.map((item) => item.id), ['req-1']);
assert.deepEqual(scopeMap['version-1'].devTasks.map((item) => item.id), ['dev-legacy']);
assert.deepEqual(scopeMap['version-2'].requirements.map((item) => item.id), ['req-2']);
assert.deepEqual(scopeMap['version-2'].devTasks.map((item) => item.id), ['dev-direct']);
assert.deepEqual(scopeMap['version-2'].plansByType.ui.map((item) => item.id), ['plan-2']);
assert.deepEqual(scopeMap['version-empty'].requirements, []);
assert.deepEqual(Object.keys(scopeMap).sort(), ['version-1', 'version-2', 'version-empty']);
});
test('selectVersionDataScope prefers non-empty V2.2 scope but keeps store overtime records', () => {
const storeScope = buildVersionDataScope({
versionId: 'version-1',
requirements: [requirement('req-app', 'version-1')],
plans: [plan('plan-app', 'version-1', 'research')],
devTasks: [devTask('dev-app', { versionId: 'version-1' })],
testCases: [testCase('tc-app', 'version-1')],
bugs: [bug('bug-app', 'version-1')],
overtimeRecords: [overtime('ot-app', 'version-1')],
});
const v22Scope = buildVersionDataScope({
versionId: 'version-1',
requirements: [requirement('req-v22', 'version-1')],
plans: [plan('plan-v22', 'version-1', 'product')],
devTasks: [devTask('dev-v22', { versionId: 'version-1' })],
testCases: [],
bugs: [],
overtimeRecords: [],
});
const selected = selectVersionDataScope({ storeScope, v22Scope });
assert.equal(selected?.requirements[0].id, 'req-v22');
assert.equal(selected?.plans[0].id, 'plan-v22');
assert.deepEqual(selected?.overtimeRecords.map((item) => item.id), ['ot-app']);
});
test('selectVersionDataScope overlays local store changes onto V2.2 rows by id', () => {
const storeScope = buildVersionDataScope({
versionId: 'version-1',
requirements: [
{ ...requirement('req-v22', 'version-1'), title: '本地标题' },
{ ...requirement('req-local', 'version-1'), title: '本地新增' },
],
plans: [
{
...plan('plan-v22', 'version-1', 'product'),
status: 'in_progress',
requirementCoverage: [
{
requirementId: 'req-v22',
status: 'not_started',
currentWorkStartedAt: '2026-07-09T09:30:00.000Z',
updatedAt: '2026-07-09T09:30:00.000Z',
updatedBy: 'PM',
},
],
},
],
devTasks: [devTask('dev-v22', { versionId: 'version-1', status: 'in_progress' })],
testCases: [testCase('tc-v22', 'version-1')],
bugs: [bug('bug-v22', 'version-1')],
overtimeRecords: [],
});
const v22Scope = buildVersionDataScope({
versionId: 'version-1',
requirements: [requirement('req-v22', 'version-1')],
plans: [plan('plan-v22', 'version-1', 'product')],
devTasks: [devTask('dev-v22', { versionId: 'version-1', status: 'todo' })],
testCases: [testCase('tc-v22', 'version-1')],
bugs: [bug('bug-v22', 'version-1')],
overtimeRecords: [],
});
const selected = selectVersionDataScope({ storeScope, v22Scope });
assert.equal(selected?.requirements.find((item) => item.id === 'req-v22')?.title, '本地标题');
assert.equal(selected?.requirements.some((item) => item.id === 'req-local'), true);
assert.equal(selected?.plans[0].status, 'in_progress');
assert.equal(selected?.plans[0].requirementCoverage?.[0]?.currentWorkStartedAt, '2026-07-09T09:30:00.000Z');
assert.equal(selected?.devTasks[0].status, 'in_progress');
});
test('selectVersionDataScope falls back to store scope when V2.2 scope is empty', () => {
const storeScope = buildVersionDataScope({
versionId: 'version-1',
requirements: [requirement('req-app', 'version-1')],
plans: [],
devTasks: [],
testCases: [],
bugs: [],
});
const emptyV22Scope = buildVersionDataScope({
versionId: 'version-1',
requirements: [],
plans: [],
devTasks: [],
testCases: [],
bugs: [],
});
const selected = selectVersionDataScope({ storeScope, v22Scope: emptyV22Scope });
assert.equal(selected?.requirements[0].id, 'req-app');
});