diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx index 3341c59da3..66bfd959f4 100644 --- a/apps/desktop/stories/app-shell.stories.tsx +++ b/apps/desktop/stories/app-shell.stories.tsx @@ -18,6 +18,7 @@ */ import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, waitFor } from 'storybook/test'; import { useState, type CSSProperties, type ReactNode } from 'react'; import type { ComponentProps } from 'react'; import type { ProjectRecord } from '@maka/core/project'; @@ -679,6 +680,177 @@ export const InterruptedToolAfterTurnAbort: Story = { ), }; +// Real path: a tool call fails mid-turn and the turn settles as failed. The +// errored tool row renders inside `.maka-turn`, and the turn wears its failed +// Banner (`describeTurnErrorClass('tool_failed')`) with the erroredTool +// execution-state description — the failed-turn chrome no story exercised. +export const FailedTurnWithToolError: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + await waitFor(() => { + const banner = canvasElement.querySelector('.maka-turn-failed-banner'); + expect(banner?.textContent).toContain('工具调用失败'); + expect(banner?.textContent).toContain('这一轮有工具执行出错'); + }); + }, +}; + +// Real path: the provider rate-limits the request and the turn settles failed. +// The failed Banner carries the rate-limit guidance — the settled provider +// error a bare transcript never shows. +export const ProviderRateLimited: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + await waitFor(() => + expect(canvasElement.querySelector('.maka-turn-failed-banner')?.textContent).toContain( + '模型请求太频繁被限流了', + ), + ); + }, +}; + +// Real path: the provider throttles a live request and Runtime schedules a +// retry. The running turn swaps its working phrase for the retry Banner +// (`ModelProviderRetryIndicator`) — the "retrying" state no story reached. +export const ProviderRetrying: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + await waitFor(() => + expect(canvasElement.querySelector('.maka-turn-provider-retry')).not.toBeNull(), + ); + }, +}; + +// Real path: the app restarted mid-turn, so the last turn is failed with +// errorClass 'app_restarted' and offers safe-resume. The warning-severity +// Banner carries the 继续这一轮 button (`safeResumeAction`) — the recovery +// affordance no story reached. +export const SafeResumeAfterRestart: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + await waitFor(() => { + const banner = canvasElement.querySelector('.maka-turn-failed-banner'); + expect(banner?.textContent).toContain('本地应用重启'); + expect(banner?.textContent).toContain('继续这一轮'); + }); + }, +}; + +// Real path: a long session with 120 turns — past the transcript virtualizer's +// window, so it must stay correct and quiet where a handful of seeded turns +// would never trip the virtualization path. +export const ManyTurns: Story = { + render: () => ( + { + const turnId = `turn-m-${index}`; + const minutesAgo = (120 - index) * 3; + return [ + user(`msg-m-u-${index}`, turnId, minutesAgo, `第 ${index + 1} 轮:这个模块的边界条件该怎么覆盖?`), + assistant(`msg-m-a-${index}`, turnId, minutesAgo - 1, `第 ${index + 1} 轮回答:先列输入域,再对空、超长、并发三类分别加断言。`), + ]; + }).flat(), + }} + /> + ), +}; + // Real path: Desktop Computer Use is exposed through the Runtime Host Client // Capability bridge. The settled observation establishes the confirmed target; // the following sequence inherits it while live progress replaces the generic