From a3cdcfe240e7a72430df13a2a897a32d680fe250 Mon Sep 17 00:00:00 2001 From: Xiao Liu Date: Sun, 30 Aug 2026 05:31:07 +0800 Subject: [PATCH] test(desktop): story-cover chat transcript failure and edge states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sixth surface under #3944 (one surface per PR): extend the chat transcript (ChatView → materializeTurns → TurnView, via Product/Shell Official AppShell) with the failure and edge states that never show up in normal use. All feed a real message ledger through ComposedShell, like InterruptedToolAfterTurnAbort. - FailedTurnWithToolError — a tool call errors mid-turn and the turn settles `failed`: the errored tool row plus the failed Banner + erroredTool description. - ProviderRateLimited — a settled `failed` turn with errorClass 'rate_limit'. - ProviderRetrying — a live turn whose providerRetry swaps the working phrase for the ModelProviderRetryIndicator banner. - SafeResumeAfterRestart — the last turn failed with errorClass 'app_restarted', offering the warning-severity Banner with the 继续这一轮 safe-resume button. - ManyTurns — 120 turns, past the transcript virtualizer's window (review feedback: 20 did not guarantee the virtualization path). Streaming, aborted/interrupted, empty, and long-message states were already covered, so this adds only the genuinely missing ones. All are safe presentational states driven by the message ledger and live overlay. Refs #3944, #3893 Generated-by: Claude Code --- apps/desktop/stories/app-shell.stories.tsx | 172 +++++++++++++++++++++ 1 file changed, 172 insertions(+) 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