From 19d5e8fa3f0ba694ee77fb9d75ac44bd2ab95227 Mon Sep 17 00:00:00 2001 From: Xiao Liu Date: Sun, 30 Aug 2026 05:23:28 +0800 Subject: [PATCH] test(desktop): story-cover daily review empty, many-sessions, failure, and long-report states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tenth surface under #3944 (one surface per PR): extend the Daily Review panel (DailyReviewPage, via Product/Module Hubs) with the states the existing Scheduled Daily Review stories never reach. - ScheduledDailyReviewEmpty — a day with no recorded activity: the panel's own empty overview. - ScheduledDailyReviewManySessions — the list caps at DAILY_REVIEW_LIST_LIMIT (8), so a higher total (12) surfaces as an 8-row list (review feedback: the array can't exceed the cap the coordinator picks and the decoder enforces). - ScheduledDailyReviewLongReport — view analysis on a report with long sections. - ScheduledDailyReviewGenerationFailed — generate returns a `failed` archive (model timeout/error); the report route shows the error Banner and failure message. Reachable via generate/retry, the only path to a failed archive (review feedback: it is reachable, so it is covered rather than omitted). Refs #3944, #3893 Generated-by: Claude Code --- apps/desktop/stories/module-hubs.stories.tsx | 153 +++++++++++++++++++ 1 file changed, 153 insertions(+) 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, '生成失败'); + }, +};