153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import type { VersionWithContext } from './derive';
|
||
import type { XiaobaoVersionRisk } from './xiaobao-risk';
|
||
import {
|
||
filterXiaobaoRiskWarnings,
|
||
filterXiaobaoWarningVersions,
|
||
formatRemainingWork,
|
||
getXiaobaoWarningRiskCount,
|
||
sanitizeRiskInsight,
|
||
} from './xiaobao-warning-view';
|
||
|
||
function version(patch: Partial<VersionWithContext> = {}): VersionWithContext {
|
||
return {
|
||
id: 'ver-1',
|
||
name: 'V1.0',
|
||
status: 'developing',
|
||
releaseDate: null,
|
||
createdAt: '2026-06-29T00:00:00.000Z',
|
||
productId: 'prod-1',
|
||
productName: 'FTB',
|
||
projectId: 'proj-1',
|
||
projectName: 'Project',
|
||
expectedReleaseDate: '2026-07-05T10:00:00.000Z',
|
||
members: [{ name: 'Alice', role: 'frontend' }],
|
||
...patch,
|
||
};
|
||
}
|
||
|
||
function risk(patch: Partial<XiaobaoVersionRisk> = {}): XiaobaoVersionRisk {
|
||
return {
|
||
versionId: 'ver-1',
|
||
versionName: 'V1.0',
|
||
productId: 'prod-1',
|
||
productName: 'Product A',
|
||
projectId: 'proj-1',
|
||
projectName: 'Project A',
|
||
riskScore: 68,
|
||
riskLevel: 'attention',
|
||
expectedReleaseDate: '2026-07-05',
|
||
confidence: 80,
|
||
confidenceLevel: 'high',
|
||
delayDays: 0,
|
||
remainingWorkHours: 8,
|
||
reasons: [],
|
||
silentRisks: [],
|
||
signals: {
|
||
unfinishedCount: 1,
|
||
openBugCount: 0,
|
||
criticalBugCount: 0,
|
||
failedTestCount: 0,
|
||
blockedCount: 0,
|
||
silentRiskCount: 0,
|
||
},
|
||
currentSnapshot: {
|
||
versionId: 'ver-1',
|
||
date: '2026-06-30',
|
||
riskScore: 68,
|
||
riskLevel: 'attention',
|
||
openBugCount: 0,
|
||
criticalBugCount: 0,
|
||
failedTestCount: 0,
|
||
blockedCount: 0,
|
||
silentRiskCount: 0,
|
||
confidence: 80,
|
||
createdAt: '2026-06-30T10:00:00.000Z',
|
||
},
|
||
trend: { direction: 'flat', delta: 0, summary: 'Risk is stable.', pattern: 'stable' },
|
||
...patch,
|
||
} as XiaobaoVersionRisk;
|
||
}
|
||
|
||
test('filterXiaobaoWarningVersions lets managers see every unfinished version', () => {
|
||
const result = filterXiaobaoWarningVersions(
|
||
[
|
||
version({ id: 'ver-1', status: 'developing', members: [{ name: 'Alice', role: 'frontend' }] }),
|
||
version({ id: 'ver-2', status: 'planned', members: [{ name: 'Bob', role: 'testing' }] }),
|
||
version({ id: 'ver-3', status: 'released', members: [{ name: 'Alice', role: 'frontend' }] }),
|
||
],
|
||
{ canManage: true, userName: 'Alice' },
|
||
);
|
||
|
||
assert.deepEqual(result.map((item) => item.id), ['ver-1', 'ver-2']);
|
||
});
|
||
|
||
test('filterXiaobaoWarningVersions limits non-managers to versions where they are a member', () => {
|
||
const result = filterXiaobaoWarningVersions(
|
||
[
|
||
version({ id: 'ver-1', status: 'developing', members: [{ name: 'Alice', role: 'frontend' }] }),
|
||
version({ id: 'ver-2', status: 'developing', members: [{ name: 'Bob', role: 'testing' }] }),
|
||
version({ id: 'ver-3', status: 'closed', members: [{ name: 'Alice', role: 'frontend' }] }),
|
||
],
|
||
{ canManage: false, userName: 'Alice' },
|
||
);
|
||
|
||
assert.deepEqual(result.map((item) => item.id), ['ver-1']);
|
||
});
|
||
|
||
test('filterXiaobaoRiskWarnings hides on_track risks and badge count follows visible risks', () => {
|
||
const risks = [
|
||
risk({ versionId: 'ver-1', riskLevel: 'on_track', riskScore: 10 }),
|
||
risk({ versionId: 'ver-2', riskLevel: 'attention', riskScore: 38 }),
|
||
risk({ versionId: 'ver-3', riskLevel: 'blocked', riskScore: 100 }),
|
||
];
|
||
|
||
assert.deepEqual(filterXiaobaoRiskWarnings(risks).map((item) => item.versionId), ['ver-2', 'ver-3']);
|
||
assert.equal(getXiaobaoWarningRiskCount(risks), 2);
|
||
});
|
||
|
||
test('filterXiaobaoRiskWarnings filters by product project and risk tier', () => {
|
||
const risks = [
|
||
risk({ versionId: 'ver-1', productId: 'prod-1', projectId: 'proj-1', riskLevel: 'attention', riskScore: 38 }),
|
||
risk({ versionId: 'ver-2', productId: 'prod-1', projectId: 'proj-2', riskLevel: 'likely_delayed', riskScore: 88 }),
|
||
risk({ versionId: 'ver-3', productId: 'prod-2', projectId: 'proj-3', riskLevel: 'blocked', riskScore: 100 }),
|
||
];
|
||
|
||
assert.deepEqual(
|
||
filterXiaobaoRiskWarnings(risks, { productId: 'prod-1' }).map((item) => item.versionId),
|
||
['ver-1', 'ver-2'],
|
||
);
|
||
assert.deepEqual(
|
||
filterXiaobaoRiskWarnings(risks, { productId: 'prod-1', projectId: 'proj-2' }).map((item) => item.versionId),
|
||
['ver-2'],
|
||
);
|
||
assert.deepEqual(
|
||
filterXiaobaoRiskWarnings(risks, { riskFilter: 'high' }).map((item) => item.versionId),
|
||
['ver-2', 'ver-3'],
|
||
);
|
||
});
|
||
|
||
test('formatRemainingWork keeps hours and adds work-day conversion', () => {
|
||
assert.equal(formatRemainingWork(0), '0h / 0天');
|
||
assert.equal(formatRemainingWork(8), '8h / 1天');
|
||
assert.equal(formatRemainingWork(12), '12h / 1.5天');
|
||
assert.equal(formatRemainingWork(0.5), '0.5h / 0.1天');
|
||
});
|
||
|
||
test('sanitizeRiskInsight filters invalid page refresh suggested actions', () => {
|
||
const result = sanitizeRiskInsight({
|
||
summary: '风险上升',
|
||
why: ['P1 Bug 增加'],
|
||
forecast: '预计延期 1 天',
|
||
suggestedActions: [
|
||
'手动触发页面刷新,等待小宝预警自动更新',
|
||
'优先处理 3 个 P1 Bug,并同步测试负责人复测',
|
||
],
|
||
ownerHints: ['研发负责人协调修复顺序'],
|
||
generatedAt: '2026-06-30T10:00:00.000Z',
|
||
});
|
||
|
||
assert.deepEqual(result.suggestedActions, ['优先处理 3 个 P1 Bug,并同步测试负责人复测']);
|
||
});
|