fix(pi): refresh desired takeover state after failures

This commit is contained in:
SaladDay
2026-08-03 02:08:52 +00:00
parent 5360061bb6
commit 1e9e59051a
4 changed files with 100 additions and 19 deletions
+2 -2
View File
@@ -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 实现零文件。
+14 -14
View File
@@ -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 (
<div
+14 -3
View File
@@ -129,9 +129,6 @@ export function useProxyStatus() {
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
queryClient.invalidateQueries({ queryKey: proxyKeys.takeoverStatus });
},
onError: (error: Error) => {
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 {
+70
View File
@@ -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));
});
});