feat(版本): 完善研发计划与预警已读

关键改动:

- 增加版本表单、发布校验和调研方向进度规则

- 扩展小宝预警已读状态、风险签名和今日证据

- 补充组长权限、加班查看范围、活动记录与相关测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 16:48:58 +08:00
parent 04520bd8c5
commit 3d3d56697a
41 changed files with 1701 additions and 121 deletions

View File

@@ -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);
});