feat(版本): 完善研发计划与预警已读
关键改动: - 增加版本表单、发布校验和调研方向进度规则 - 扩展小宝预警已读状态、风险签名和今日证据 - 补充组长权限、加班查看范围、活动记录与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -8,6 +8,10 @@ import {
|
||||
formatRemainingWork,
|
||||
getRiskScoreTone,
|
||||
getXiaobaoWarningRiskCount,
|
||||
getXiaobaoWarningUnreadUpdateCount,
|
||||
getXiaobaoWarningUpdateSignature,
|
||||
isXiaobaoWarningUpdated,
|
||||
markXiaobaoWarningRead,
|
||||
sanitizeRiskInsight,
|
||||
} from './xiaobao-warning-view';
|
||||
|
||||
@@ -157,3 +161,105 @@ test('sanitizeRiskInsight filters invalid page refresh suggested actions', () =>
|
||||
|
||||
assert.deepEqual(result.suggestedActions, ['优先处理 3 个 P1 Bug,并同步测试负责人复测']);
|
||||
});
|
||||
|
||||
test('xiaobao warning update state is unread until the current user reads the same risk signature', () => {
|
||||
const baseRisk = risk();
|
||||
|
||||
assert.equal(isXiaobaoWarningUpdated(baseRisk, [], 'user-1'), true);
|
||||
|
||||
const readStates = markXiaobaoWarningRead([], 'user-1', baseRisk, '2026-06-30T11:00:00.000Z');
|
||||
|
||||
assert.equal(isXiaobaoWarningUpdated(baseRisk, readStates, 'user-1'), false);
|
||||
assert.equal(isXiaobaoWarningUpdated(baseRisk, readStates, 'user-2'), true);
|
||||
});
|
||||
|
||||
test('xiaobao warning update state becomes unread again when risk facts change', () => {
|
||||
const baseRisk = risk();
|
||||
const readStates = markXiaobaoWarningRead([], 'user-1', baseRisk, '2026-06-30T11:00:00.000Z');
|
||||
const changedRisk = risk({
|
||||
riskScore: 69,
|
||||
signals: {
|
||||
...baseRisk.signals,
|
||||
failedTestCount: 1,
|
||||
},
|
||||
});
|
||||
|
||||
assert.notEqual(getXiaobaoWarningUpdateSignature(changedRisk), getXiaobaoWarningUpdateSignature(baseRisk));
|
||||
assert.equal(isXiaobaoWarningUpdated(changedRisk, readStates, 'user-1'), true);
|
||||
assert.equal(getXiaobaoWarningUnreadUpdateCount([baseRisk, changedRisk], readStates, 'user-1'), 1);
|
||||
});
|
||||
|
||||
test('xiaobao warning update state waits until AI suggestion update is complete', () => {
|
||||
const baseRisk = risk();
|
||||
const readStates = markXiaobaoWarningRead([], 'user-1', baseRisk, '2026-06-30T11:00:00.000Z');
|
||||
const changedRisk = risk({
|
||||
riskScore: 72,
|
||||
aiInsightUpdating: true,
|
||||
signals: {
|
||||
...baseRisk.signals,
|
||||
failedTestCount: 1,
|
||||
},
|
||||
});
|
||||
const completedRisk = { ...changedRisk, aiInsightUpdating: false };
|
||||
|
||||
assert.equal(isXiaobaoWarningUpdated(changedRisk, readStates, 'user-1'), false);
|
||||
assert.equal(getXiaobaoWarningUnreadUpdateCount([changedRisk], readStates, 'user-1'), 0);
|
||||
assert.deepEqual(markXiaobaoWarningRead(readStates, 'user-1', changedRisk, '2026-06-30T12:00:00.000Z'), readStates);
|
||||
assert.equal(isXiaobaoWarningUpdated(completedRisk, readStates, 'user-1'), true);
|
||||
});
|
||||
|
||||
test('xiaobao warning update state ignores refresh-volatile timing and hour drift', () => {
|
||||
const baseRisk = risk({
|
||||
forecastReleaseDate: '2026-07-05T10:00:00.000Z',
|
||||
dailyEvidence: {
|
||||
todayDeliveries: [],
|
||||
todayProgress: [
|
||||
{ id: 'ev-1', title: 'Progress', summary: '开始开发:核心流程', occurredAt: '2026-06-30T10:00:00.000Z' },
|
||||
],
|
||||
todayCreations: [],
|
||||
todayRisks: [],
|
||||
progressNotes: [],
|
||||
needsProgressItems: [],
|
||||
recentActivityCount: 2,
|
||||
totalActivityCount: 1,
|
||||
todayActualHours: 1,
|
||||
lastActivityAt: '2026-06-30T10:00:00.000Z',
|
||||
},
|
||||
});
|
||||
const readStates = markXiaobaoWarningRead([], 'user-1', baseRisk, '2026-06-30T11:00:00.000Z');
|
||||
const refreshDriftRisk = risk({
|
||||
forecastReleaseDate: '2026-07-05T11:30:00.000Z',
|
||||
signals: {
|
||||
...baseRisk.signals,
|
||||
daysToExpectedRelease: 4.2,
|
||||
},
|
||||
dailyEvidence: {
|
||||
...baseRisk.dailyEvidence!,
|
||||
recentActivityCount: 3,
|
||||
todayActualHours: 2.5,
|
||||
lastActivityAt: '2026-06-30T11:30:00.000Z',
|
||||
todayProgress: [
|
||||
{ id: 'ev-1', title: 'Progress', summary: '开始开发:核心流程', occurredAt: '2026-06-30T11:30:00.000Z' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(isXiaobaoWarningUpdated(refreshDriftRisk, readStates, 'user-1'), false);
|
||||
});
|
||||
|
||||
test('markXiaobaoWarningRead replaces only the matching user and version read marker', () => {
|
||||
const baseRisk = risk();
|
||||
const otherRisk = risk({ versionId: 'ver-2', versionName: 'V2.0' });
|
||||
const initial = [
|
||||
...markXiaobaoWarningRead([], 'user-1', baseRisk, '2026-06-30T10:00:00.000Z'),
|
||||
...markXiaobaoWarningRead([], 'user-2', baseRisk, '2026-06-30T10:05:00.000Z'),
|
||||
...markXiaobaoWarningRead([], 'user-1', otherRisk, '2026-06-30T10:10:00.000Z'),
|
||||
];
|
||||
|
||||
const updated = markXiaobaoWarningRead(initial, 'user-1', baseRisk, '2026-06-30T11:00:00.000Z');
|
||||
|
||||
assert.equal(updated.filter((item) => item.userId === 'user-1' && item.versionId === 'ver-1').length, 1);
|
||||
assert.equal(updated.find((item) => item.userId === 'user-1' && item.versionId === 'ver-1')?.readAt, '2026-06-30T11:00:00.000Z');
|
||||
assert.equal(updated.some((item) => item.userId === 'user-2' && item.versionId === 'ver-1'), true);
|
||||
assert.equal(updated.some((item) => item.userId === 'user-1' && item.versionId === 'ver-2'), true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user