diff --git a/apps/desktop/stories/module-hubs.stories.tsx b/apps/desktop/stories/module-hubs.stories.tsx index 82ab26e470..482dae04ce 100644 --- a/apps/desktop/stories/module-hubs.stories.tsx +++ b/apps/desktop/stories/module-hubs.stories.tsx @@ -441,6 +441,60 @@ const DAILY_REVIEW_ARCHIVE: DailyReviewArchive = { type DailyReviewBridge = NonNullable['bridge']>; +// A day with no recorded activity — the panel's own empty state. +const EMPTY_DAILY_REVIEW_SUMMARY: DailyReviewSummary = { + ...DAILY_REVIEW_SUMMARY, + totals: { sessionCount: 0, requestCount: 0, totalTokens: 0, costUsd: 0, errorCount: 0 }, + sessions: [], + topTools: [], + topModels: [], +}; + +// A busy day whose session list runs past its display limit (8). +// A busy day whose true session count exceeds the list cap. Production caps the +// list at DAILY_REVIEW_LIST_LIMIT (8) — the coordinator picks 8 and the +// protocol decoder rejects more — so the fixture lists 8 rows while the higher +// total lives in totals.sessionCount. +const MANY_SESSION_SUMMARY: DailyReviewSummary = { + ...DAILY_REVIEW_SUMMARY, + totals: { ...DAILY_REVIEW_SUMMARY.totals, sessionCount: 12, requestCount: 96 }, + sessions: Array.from({ length: 8 }, (_, index) => ({ + id: `s-many-${index + 1}`, + name: `第 ${index + 1} 个会话:回归与修复`, + lastMessageAt: NOW - (index + 1) * 15 * 60_000, + lastMessagePreview: `第 ${index + 1} 条会话的最后一条消息预览。`, + })), +}; + +// A generation that failed — the host returns a `failed` archive on a model +// timeout/error. Reached by triggering generate/retry (runOnce → getArchive → +// report route), where DailyReviewReport shows the error Banner + errorMessage. +const FAILED_DAILY_REVIEW_ARCHIVE: DailyReviewArchive = { + ...DAILY_REVIEW_ARCHIVE, + id: '2026-07-01-1d-failed', + status: 'failed', + errorMessage: '模型在生成分析时超时,未能产出报告,请稍后重试。', + sections: {}, +}; + +// A generated report whose sections run long, to check the report body stays +// readable instead of clipping. +const LONG_REVIEW_SECTION = Array.from( + { length: 6 }, + (_, index) => + `## 第 ${index + 1} 节\n\n这一节刻意写得很长,用来检验报告正文在多段落、多标题下仍然可读,` + + '不会把布局撑破或截断关键结论:它覆盖了当天的活动重点、发现的问题,以及后续要跟进的改进项,逐条展开说明。', +).join('\n\n'); +const LONG_DAILY_REVIEW_ARCHIVE: DailyReviewArchive = { + ...DAILY_REVIEW_ARCHIVE, + sections: { + summary: LONG_REVIEW_SECTION, + gaps: LONG_REVIEW_SECTION, + usage: LONG_REVIEW_SECTION, + code: LONG_REVIEW_SECTION, + }, +}; + const configuredMcpConfig: McpConfigFile = { version: MCP_CONFIG_VERSION, mcpServers: { @@ -1201,3 +1255,102 @@ export const ScheduledDailyReviewReport: Story = { await waitForStoryText(canvasElement, '返回活动'); }, }; + +// Real path: sidebar → scheduled tasks → 每日回顾 on a day with no recorded +// activity — the panel's own empty state, not a spinner and not an error. +export const ScheduledDailyReviewEmpty: Story = { + render: () => ( + EMPTY_DAILY_REVIEW_SUMMARY, + listArchives: async () => [], + runOnce: async () => ({ archiveId: DAILY_REVIEW_ARCHIVE.id }), + getArchive: async () => DAILY_REVIEW_ARCHIVE, + }} + /> + ), + play: async ({ canvasElement }) => { + await waitForStoryText(canvasElement, '等待记录今天活动'); + }, +}; + +// Real path: sidebar → scheduled tasks → 每日回顾 on a busy day. The list caps +// at DAILY_REVIEW_LIST_LIMIT (8), so a higher total (12) surfaces as 8 rows — +// the production-reachable "many sessions" shape the seeded two-session fixture +// never reaches. +export const ScheduledDailyReviewManySessions: Story = { + render: () => ( + MANY_SESSION_SUMMARY, + listArchives: async () => [], + runOnce: async () => ({ archiveId: DAILY_REVIEW_ARCHIVE.id }), + getArchive: async () => DAILY_REVIEW_ARCHIVE, + }} + /> + ), + play: async ({ canvasElement }) => { + await waitForStorySelector(canvasElement, '.maka-daily-review-content'); + await waitForStoryText(canvasElement, '第 1 个会话:回归与修复'); + }, +}; + +// Real path: sidebar → scheduled tasks → 每日回顾 → view analysis on a long +// report — the report body must stay readable across many long sections. +export const ScheduledDailyReviewLongReport: Story = { + render: () => ( + DAILY_REVIEW_SUMMARY, + listArchives: async () => [ + { + id: LONG_DAILY_REVIEW_ARCHIVE.id, + day: LONG_DAILY_REVIEW_ARCHIVE.day, + range: LONG_DAILY_REVIEW_ARCHIVE.range, + status: LONG_DAILY_REVIEW_ARCHIVE.status, + generatedAt: LONG_DAILY_REVIEW_ARCHIVE.generatedAt, + trigger: LONG_DAILY_REVIEW_ARCHIVE.trigger, + modelKey: LONG_DAILY_REVIEW_ARCHIVE.modelKey, + totals: LONG_DAILY_REVIEW_ARCHIVE.totals, + }, + ], + getArchive: async () => LONG_DAILY_REVIEW_ARCHIVE, + }} + /> + ), + play: async ({ canvasElement }) => { + const view = await waitForStoryButton( + canvasElement, + (candidate) => candidate.textContent?.includes('查看分析') === true, + ); + view.click(); + await waitForStoryText(canvasElement, '返回活动'); + await waitForStoryText(canvasElement, '第 1 节'); + }, +}; + +// Real path: sidebar → scheduled tasks → 每日回顾 → generate, when generation +// fails (model timeout/error). runOnce → getArchive returns a `failed` archive, +// so the report route shows the error Banner and failure message. This is the +// only path to a failed archive — it has no "view analysis" affordance, so +// generate/retry is how it is reached. +export const ScheduledDailyReviewGenerationFailed: Story = { + render: () => ( + DAILY_REVIEW_SUMMARY, + listArchives: async () => [], + runOnce: async () => ({ archiveId: FAILED_DAILY_REVIEW_ARCHIVE.id }), + getArchive: async () => FAILED_DAILY_REVIEW_ARCHIVE, + }} + /> + ), + play: async ({ canvasElement }) => { + const generate = await waitForStoryButton( + canvasElement, + (candidate) => candidate.textContent?.includes('生成分析') === true, + ); + generate.click(); + await waitForStoryText(canvasElement, '生成失败'); + }, +};