Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions apps/desktop/stories/module-hubs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,60 @@ const DAILY_REVIEW_ARCHIVE: DailyReviewArchive = {

type DailyReviewBridge = NonNullable<ComponentProps<typeof DailyReviewPage>['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: {
Expand Down Expand Up @@ -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: () => (
<ScheduledDailyReviewSurface
bridge={{
fetchDay: async () => 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: () => (
<ScheduledDailyReviewSurface
bridge={{
fetchDay: async () => MANY_SESSION_SUMMARY,
listArchives: async () => [],
runOnce: async () => ({ archiveId: DAILY_REVIEW_ARCHIVE.id }),
getArchive: async () => DAILY_REVIEW_ARCHIVE,
}}
/>
),
play: async ({ canvasElement }) => {
await waitForStorySelector<HTMLElement>(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: () => (
<ScheduledDailyReviewSurface
bridge={{
fetchDay: async () => 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: () => (
<ScheduledDailyReviewSurface
bridge={{
fetchDay: async () => 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, '生成失败');
},
};