From 1e9e59051a31bd5d270afb2eabd52c702a1d609c Mon Sep 17 00:00:00 2001 From: SaladDay Date: Mon, 3 Aug 2026 02:08:52 +0000 Subject: [PATCH] fix(pi): refresh desired takeover state after failures --- docs/pi-main-project-zh.md | 4 +- src/components/proxy/ProxyToggle.tsx | 28 +++++------ src/hooks/useProxyStatus.ts | 17 +++++-- tests/hooks/useProxyStatus.test.tsx | 70 ++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/docs/pi-main-project-zh.md b/docs/pi-main-project-zh.md index 25d27e050..378e4cac0 100644 --- a/docs/pi-main-project-zh.md +++ b/docs/pi-main-project-zh.md @@ -122,7 +122,7 @@ ## Scope audit(主工程预审检查点) 以主工程基线 `26a95aeb059235612c665d6b6bcd92fde9c3572e` 计,终态候选为 -140 个文件、约 +15.8k 净行,触发历史 90 文件/12k 净行审计线。审计结论为 +141 个文件、约 +16.9k 净行,触发历史 90 文件/12k 净行审计线。审计结论为 **范围大但未越界**: - Rust 70 文件:Pi catalog/typed endpoints、共享文件与 ownership、gateway/runtime/ @@ -132,7 +132,7 @@ - 前端与构建配置 55 文件:3 个 Pi 专用 panel/form、typed IPC/query、既有导航/ 设置/代理/使用统计接线、四语 i18n 与 Windows 进程树能力;没有复制第二套 app shell。 -- 前端测试 12 文件:专用表单/prompt/usage 测试及既有跨 app matrix 增量。 +- 前端测试 13 文件:专用表单/prompt/usage 测试及既有跨 app matrix 增量。 - 证据与文档 3 文件:transport capture、本文账本、extensions 设计附录。 - `schema.rs`、`migration.rs`、`backup.rs`、三份认证套件及七个 pinned fixtures 均零改动;restore、themes 与 extensions 实现零文件。 diff --git a/src/components/proxy/ProxyToggle.tsx b/src/components/proxy/ProxyToggle.tsx index 905b06561..2b246bb8d 100644 --- a/src/components/proxy/ProxyToggle.tsx +++ b/src/components/proxy/ProxyToggle.tsx @@ -45,21 +45,21 @@ export function ProxyToggle({ className, activeApp }: ProxyToggleProps) { defaultValue: `${appLabel} routing is desired but currently degraded; CC Switch will retry it`, }) : takeoverEnabled - ? isRunning - ? t("proxy.takeover.tooltip.active", { + ? isRunning + ? t("proxy.takeover.tooltip.active", { + appLabel, + address: status?.address, + port: status?.port, + defaultValue: `${appLabel} 已接管 - ${status?.address}:${status?.port}\n切换该应用供应商为热切换`, + }) + : t("proxy.takeover.tooltip.broken", { + appLabel, + defaultValue: `${appLabel} 已接管,但代理服务未运行`, + }) + : t("proxy.takeover.tooltip.inactive", { appLabel, - address: status?.address, - port: status?.port, - defaultValue: `${appLabel} 已接管 - ${status?.address}:${status?.port}\n切换该应用供应商为热切换`, - }) - : t("proxy.takeover.tooltip.broken", { - appLabel, - defaultValue: `${appLabel} 已接管,但代理服务未运行`, - }) - : t("proxy.takeover.tooltip.inactive", { - appLabel, - defaultValue: `接管 ${appLabel} 的 Live 配置,让该应用请求走本地代理`, - }); + defaultValue: `接管 ${appLabel} 的 Live 配置,让该应用请求走本地代理`, + }); return (
{ const detail = @@ -144,6 +141,20 @@ export function useProxyStatus() { }), ); }, + // Pi persists desired takeover before attempting listener/runtime work. + // A rejected IPC can therefore still have changed authoritative state. + onSettled: async () => { + await Promise.all([ + queryClient.refetchQueries({ + queryKey: proxyKeys.status, + type: "active", + }), + queryClient.refetchQueries({ + queryKey: proxyKeys.takeoverStatus, + type: "active", + }), + ]); + }, }); return { diff --git a/tests/hooks/useProxyStatus.test.tsx b/tests/hooks/useProxyStatus.test.tsx index 675f5fb46..18192f098 100644 --- a/tests/hooks/useProxyStatus.test.tsx +++ b/tests/hooks/useProxyStatus.test.tsx @@ -131,4 +131,74 @@ describe("useProxyStatus", () => { expect(invokeMock).toHaveBeenCalledWith("start_proxy_server"); expect(invokeMock).toHaveBeenCalledWith("stop_proxy_server"); }); + + it("refreshes desired takeover state even when activation rejects", async () => { + let desiredPi = false; + invokeMock.mockImplementation((command: string) => { + if (command === "get_proxy_status") { + return Promise.resolve({ + running: false, + address: "127.0.0.1", + port: 15721, + active_connections: 0, + total_requests: 0, + success_requests: 0, + failed_requests: 0, + success_rate: 0, + uptime_seconds: 0, + current_provider: null, + current_provider_id: null, + last_request_at: null, + last_error: null, + failover_count: 0, + }); + } + if (command === "get_proxy_takeover_status") { + return Promise.resolve({ + claude: false, + codex: false, + gemini: false, + grokbuild: false, + opencode: false, + openclaw: false, + pi: desiredPi, + piOperationalState: desiredPi ? "degraded" : "disabled", + }); + } + if (command === "set_proxy_takeover_for_app") { + desiredPi = true; + return Promise.reject(new Error("listener unavailable")); + } + return Promise.resolve(null); + }); + + const { wrapper, queryClient } = createWrapper(); + const { result, rerender } = renderHook(() => useProxyStatus(), { + wrapper, + }); + await waitFor(() => expect(result.current.takeoverStatus).toBeDefined()); + + await expect( + act(async () => { + await result.current.setTakeoverForApp({ + appType: "pi", + enabled: true, + }); + }), + ).rejects.toThrow("listener unavailable"); + + await waitFor(() => + expect( + invokeMock.mock.calls.filter( + ([command]) => command === "get_proxy_takeover_status", + ).length, + ).toBeGreaterThan(1), + ); + expect(queryClient.getQueryData(proxyKeys.takeoverStatus)).toMatchObject({ + pi: true, + piOperationalState: "degraded", + }); + rerender(); + await waitFor(() => expect(result.current.takeoverStatus?.pi).toBe(true)); + }); });