From 0ce9606c89480f54648e446200d5e623ea8daa8b Mon Sep 17 00:00:00 2001 From: mesalogo Date: Fri, 2 Oct 2026 19:00:10 +0800 Subject: [PATCH] test: cover five-second conversation ordering and interaction pauses Add App regression coverage and document five-second sidebar sorting, live row updates, and deferred reordering during interaction. The ordering hook and App wiring were already included in ee4eb73. Focused validation: 23 tests passed. --- .../application-tool-navigation/progress.md | 19 +++++++ .../application-tool-navigation/ui-design.md | 7 ++- .../execution-history-storage.md | 2 + src/renderer/src/App.test.tsx | 57 ++++++++++++++++++- 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/docs/features/application-tool-navigation/progress.md b/docs/features/application-tool-navigation/progress.md index b0ad8348..fecf8161 100644 --- a/docs/features/application-tool-navigation/progress.md +++ b/docs/features/application-tool-navigation/progress.md @@ -60,6 +60,25 @@ npm run typecheck 的未跟踪 `sidebar-final-probe.ts` 和 `sidebar-final-run.mjs`。未跑全仓测试、打包程序或 macOS/Linux 窗口验证。中文新增规则经 deai-writing 扫描无阻断项,并已人工核对。 +## 2026-10-02:补齐五秒排序检查 + +会话列表已接入 `useConversationListOrder`,保留实时行数据,定时调整展示顺序。 +交互暂停与即时操作规则见[会话置顶与操作菜单](./ui-design.md#会话置顶与操作菜单)。 +本地和消息通道共用此 Renderer 路径,未修改 Agent 执行、同步或持久化。 + +验证命令: + +```text +npx vitest run src/renderer/src/use-conversation-list-order.test.tsx src/renderer/src/chat-conversation.test.ts src/renderer/src/App.test.tsx -t "useConversationListOrder|conversation display order|parallel streaming conversations|summary and detail order|sorts pinned conversations|merges persisted pin metadata|dedicated pin update" +``` + +3 个文件、23 项通过,覆盖五秒边界、连续增量、交互暂停与恢复、立即增删与置顶、 +项目/搜索范围切换、计时器清理,以及 App 中本地/通道摘要更新的实际列表接线。 +新增 hook 与相关测试的定向 ESLint、`git diff --check` 通过。未运行 Electron 窗口验证。 +全仓检查未通过:typecheck 报并行修改中的 `App.tsx:5436` 缺少 `startTransition`; +ESLint 报打包文件及现有会话视图缓存的 refs 错误;`npm test` 在 120 秒后超时。 +额外运行的草稿置顶失败测试未通过,实际显示持久化错误而非预期的置顶错误。 + ## 2026-09-28:并行会话输出时保持列表顺序 修复多个会话交替输出时,左侧列表随每个增量反复换位的问题。展示顺序统一使用消息创建 diff --git a/docs/features/application-tool-navigation/ui-design.md b/docs/features/application-tool-navigation/ui-design.md index 828b8d85..cfc16a0b 100644 --- a/docs/features/application-tool-navigation/ui-design.md +++ b/docs/features/application-tool-navigation/ui-design.md @@ -249,12 +249,17 @@ CPU 4% · 内存 620 MB · 桌面进程 7 会话行的“更多会话操作”提供“置顶会话/取消置顶”,本地与消息通道会话均可使用。 置顶会话显示带“已置顶”可访问名称的图标;当前项目和搜索过滤仍生效,过滤后的列表先显示 置顶会话,两组内各按最新消息的创建时间倒序;时间相同按会话 ID 固定顺序,无消息时使用 -会话更新时间。新增消息可以前移会话,同一消息的文字、推理、工具进度和完成状态更新不 +会话更新时间。新增消息在每 5 秒的排序检查时可以前移会话,仅顺序变化时调整位置;同一消息的文字、推理、工具进度和完成状态更新不 改变排序。有消息的会话重命名或修改设置也不前移。摘要刷新、打开详情和重新启动沿用同一 规则,行右侧仍显示实际更新时间。取消置顶恢复消息时间排序,不改变会话更新时间。 置顶状态重启后保留;保存成功后更新图标和顺序,失败保留原状态并显示应用错误通知。 存储尚未就绪或置顶请求进行中时禁用置顶操作。 +消息内容、标题与运行状态实时更新。鼠标停留在列表内、焦点在列表内、滚动尚未停止 +200ms 或会话操作菜单打开时,跳过自动排序;操作结束后在下一次 5 秒检查时调整。 +新建、删除、置顶和取消置顶立即反映到列表中,不提前应用其他会话待处理的位置变化。 +切换项目或搜索范围立即显示对应列表;排序计时器不因消息增量而重新计时。 + 操作菜单通过 body Portal 浮在会话列表上方,不撑开会话行。菜单锚定更多按钮,默认位于 按钮下方,随滚动、窗口及菜单尺寸变化重新定位,距窗口边缘至少 `8px`;空间不足时限制 宽高并允许菜单内部滚动。使用共享表面色、边框、圆角与浮层阴影。 diff --git a/docs/features/assistant-workbar/execution-history-storage.md b/docs/features/assistant-workbar/execution-history-storage.md index 925f3450..93d85b3f 100644 --- a/docs/features/assistant-workbar/execution-history-storage.md +++ b/docs/features/assistant-workbar/execution-history-storage.md @@ -76,6 +76,8 @@ WAL 增长与 SQL 全历史扫描次数的定向验证见 [BUILD.md](../../../BU `createdAt` 最大值;摘要的 `latestMessageAt` 在已有聚合查询中取 `MAX(created_at)`, 与摘要上附带的实时消息取最大值。该字段仅用于桌面只读投影,不增加数据库列或持久化副本。 `updatedAt`/`updated_at` 继续随流式事件更新,原有快照合并、终态优先与逐消息保存规则不变。 + `useConversationListOrder` 在内存中保留展示 ID 顺序,以单个 5 秒计时器应用自动排序, + 并按上述交互规则暂缓重排;行数据始终使用最新会话对象,不延迟保存或同步。 - 本机和托管 SSH 的 Renderer/Main 读取、持久化与恢复投影共用以上实现。Agent 的 transcript、原生 Session、权限及模型桥协议未改变;完整列表、摘要和详情都附带 Main 当前恢复请求的问题投影。 diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 52760f5c..e0c1c8e5 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -3857,6 +3857,15 @@ describe("App", () => { }); it("keeps parallel streaming conversations in message order while persisting fresh deltas", async () => { + const realSetInterval = window.setInterval.bind(window); + const orderingTicks: Array<() => void> = []; + vi.spyOn(window, "setInterval").mockImplementation(((handler: TimerHandler, timeout?: number, ...args: unknown[]) => { + if (timeout === 5_000 && typeof handler === "function") { + orderingTicks.push(handler as () => void); + return realSetInterval(() => {}, timeout); + } + return realSetInterval(handler, timeout, ...args); + }) as typeof window.setInterval); const snapshots: ConversationSnapshot[] = ["First", "Second"].map((title, index) => ({ id: `parallel-${index}`, projectId, title, updatedAt: 100 - index, messages: [], })); @@ -3870,6 +3879,10 @@ describe("App", () => { fireEvent.click(screen.getByLabelText("发送")); await waitFor(() => expect(run).toHaveBeenCalledTimes(index + 1)); } + expect(titles()).toEqual(["First", "Second"]); + act(() => screen.getByLabelText("向 GoodBuddy 提问").focus()); + expect(orderingTicks.length).toBeGreaterThan(0); + await act(async () => orderingTicks.forEach(tick => tick())); expect(titles()).toEqual(["Second", "First"]); const first = run.mock.calls[0]![0]; const second = run.mock.calls[1]![0]; @@ -3893,7 +3906,19 @@ describe("App", () => { expect(titles()).toEqual(["Second", "First"]); }); - it.each([false, true])("keeps summary and detail order stable across refreshes and moves a newly messaged conversation forward (channel: %s)", async (channel) => { + it.each([false, true].flatMap(channel => + (["none", "mouse", "focus", "menu"] as const).map(pause => ({ channel, pause })), + ))("batches summary and detail ordering every 5 seconds with live rows (channel: $channel, pause: $pause)", async ({ channel, pause }) => { + const realSetInterval = window.setInterval.bind(window); + const orderingTicks: Array<() => void> = []; + vi.spyOn(window, "setInterval").mockImplementation(((handler: TimerHandler, timeout?: number, ...args: unknown[]) => { + if (timeout === 5_000 && typeof handler === "function") { + orderingTicks.push(handler as () => void); + // Keep async Testing Library waits real, but advance five-second ticks explicitly. + return realSetInterval(() => {}, timeout); + } + return realSetInterval(handler, timeout, ...args); + }) as typeof window.setInterval); const snapshots: ConversationSnapshot[] = ["Earlier", "Later"].map((title, index) => ({ id: `stable-${index}`, projectId, title, updatedAt: 900 - index, ...(channel ? { remote: { channel: "weixin" as const, accountDisplay: "Account", conversationType: "direct" as const } } : {}), @@ -3913,10 +3938,36 @@ describe("App", () => { fireEvent.click(container.querySelectorAll(".conversation-item")[1]!); await screen.findByText("Body Earlier"); expect(titles()).toEqual(["Later", "Refreshed Earlier"]); + const list = container.querySelector(".conversation-list")!; + const composer = screen.getByLabelText("向 GoodBuddy 提问"); + act(() => composer.focus()); + if (pause === "mouse") fireEvent.mouseEnter(list); + if (pause === "focus") act(() => screen.getByLabelText("更多会话操作 Refreshed Earlier").focus()); + if (pause === "menu") { + fireEvent.click(screen.getByLabelText("更多会话操作 Refreshed Earlier")); + await waitFor(() => expect(screen.getByRole("menu")).toContainElement(document.activeElement as HTMLElement)); + expect(list).not.toContainElement(screen.getByRole("menu")); + } snapshots[0] = { ...snapshots[0]!, updatedAt: 1100, messages: [...snapshots[0]!.messages, - { id: "new-message", role: "user", state: "complete", content: "New turn", createdAt: 200 }] }; + { id: "new-message", role: "user", state: "complete", content: "New turn", createdAt: 200 }], title: "Live Earlier" }; fireEvent(window, new Event("focus")); - await waitFor(() => expect(titles()).toEqual(["Refreshed Earlier", "Later"])); + await waitFor(() => expect(titles()).toEqual(["Later", "Live Earlier"])); + expect(await screen.findByText("New turn")).toBeInTheDocument(); + expect(orderingTicks.length).toBeGreaterThan(0); + await act(async () => orderingTicks.forEach(tick => tick())); + if (pause !== "none") { + expect(titles()).toEqual(["Later", "Live Earlier"]); + if (pause === "mouse") fireEvent.mouseLeave(list); + if (pause === "menu") { + fireEvent.keyDown(screen.getByRole("menu"), { key: "Escape" }); + await waitFor(() => expect(screen.queryByRole("menu")).not.toBeInTheDocument()); + await waitFor(() => expect(screen.getByLabelText("更多会话操作 Live Earlier")).toHaveFocus()); + } + act(() => composer.focus()); + expect(titles()).toEqual(["Later", "Live Earlier"]); + await act(async () => orderingTicks.forEach(tick => tick())); + } + expect(titles()).toEqual(["Live Earlier", "Later"]); }); it("uses the dedicated pin update for channel conversations", async () => {