refactor(proxy): remove is_proxy_target in favor of failover_queue

- Remove `is_proxy_target` field from Provider struct (Rust & TypeScript)
- Remove related DAO methods: get_proxy_target_provider, set_proxy_target
- Remove deprecated Tauri commands: get_proxy_targets, set_proxy_target
- Add `is_available()` method to CircuitBreaker for availability checks
  without consuming HalfOpen probe permits (used in select_providers)
- Keep `allow_request()` for actual request gating with permit tracking
- Update stream_check to use failover_queue instead of is_proxy_target
- Clean up commented-out reset circuit breaker button in ProviderActions
- Remove unused useProxyTargets and useSetProxyTarget hooks
This commit is contained in:
Jason
2025-12-16 15:45:15 +08:00
parent d4f33224c6
commit e6654bd7f9
37 changed files with 348 additions and 564 deletions

View File

@@ -19,6 +19,23 @@ vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: tMock }),
}));
vi.mock("@/hooks/useProxyStatus", () => ({
useProxyStatus: () => ({
status: null,
isLoading: false,
isRunning: false,
isTakeoverActive: false,
startWithTakeover: vi.fn(),
stopWithRestore: vi.fn(),
switchProxyProvider: vi.fn(),
checkRunning: vi.fn(),
checkTakeoverActive: vi.fn(),
isStarting: false,
isStopping: false,
isPending: false,
}),
}));
interface SettingsMock {
settings: any;
isLoading: boolean;
@@ -288,6 +305,7 @@ describe("SettingsPage Component", () => {
});
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("数据管理"));
// 有文件时,点击导入按钮执行 importConfig
fireEvent.click(
@@ -363,6 +381,7 @@ describe("SettingsPage Component", () => {
await waitFor(() => {
expect(toastSuccessMock).toHaveBeenCalledWith(
"settings.devModeRestartHint",
expect.objectContaining({ closeButton: true }),
);
});
});
@@ -393,6 +412,7 @@ describe("SettingsPage Component", () => {
render(<SettingsPage open={true} onOpenChange={vi.fn()} />);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
fireEvent.click(screen.getByText("browse-directory"));
expect(settingsMock.browseDirectory).toHaveBeenCalledWith("claude");

View File

@@ -120,6 +120,7 @@ describe("useImportExport Hook (edge cases)", () => {
expect(exportConfigMock).toHaveBeenCalledWith("/exports/config.json");
expect(toastSuccessMock).toHaveBeenCalledWith(
expect.stringContaining("/final/config.json"),
expect.objectContaining({ closeButton: true }),
);
});
});

View File

@@ -180,6 +180,7 @@ describe("useImportExport Hook", () => {
expect(exportConfigMock).toHaveBeenCalledWith("/export.json");
expect(toastSuccessMock).toHaveBeenCalledWith(
expect.stringContaining("/backup/export.json"),
expect.objectContaining({ closeButton: true }),
);
});

View File

@@ -150,6 +150,7 @@ describe("SettingsPage integration", () => {
expect(screen.getByText("language:zh")).toBeInTheDocument(),
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
const appInput = await screen.findByPlaceholderText(
"settings.browsePlaceholderApp",
);
@@ -165,6 +166,7 @@ describe("SettingsPage integration", () => {
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("数据管理"));
fireEvent.click(screen.getByText("settings.selectConfigFile"));
await waitFor(() =>
expect(screen.getByTestId("selected-file").textContent).toContain(
@@ -188,6 +190,7 @@ describe("SettingsPage integration", () => {
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
const appInput = await screen.findByPlaceholderText(
"settings.browsePlaceholderApp",
);
@@ -214,6 +217,7 @@ describe("SettingsPage integration", () => {
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("配置文件目录"));
const browseButtons = screen.getAllByTitle("settings.browseDirectory");
const resetButtons = screen.getAllByTitle("settings.resetDefault");
@@ -253,6 +257,7 @@ describe("SettingsPage integration", () => {
expect(screen.getByText("language:zh")).toBeInTheDocument(),
);
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("数据管理"));
server.use(
http.post("http://tauri.local/save_file_dialog", () =>

View File

@@ -236,4 +236,64 @@ export const handlers = [
http.post(`${TAURI_ENDPOINT}/sync_current_providers_live`, () =>
success({ success: true }),
),
// Proxy status (for SettingsPage / ProxyPanel hooks)
http.post(`${TAURI_ENDPOINT}/get_proxy_status`, () =>
success({
running: false,
address: "127.0.0.1",
port: 0,
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,
active_targets: [],
}),
),
http.post(`${TAURI_ENDPOINT}/is_live_takeover_active`, () => success(false)),
// Failover / circuit breaker defaults
http.post(`${TAURI_ENDPOINT}/get_failover_queue`, () => success([])),
http.post(`${TAURI_ENDPOINT}/get_available_providers_for_failover`, () =>
success([]),
),
http.post(`${TAURI_ENDPOINT}/add_to_failover_queue`, () => success(true)),
http.post(`${TAURI_ENDPOINT}/remove_from_failover_queue`, () => success(true)),
http.post(`${TAURI_ENDPOINT}/reorder_failover_queue`, () => success(true)),
http.post(`${TAURI_ENDPOINT}/set_failover_item_enabled`, () => success(true)),
http.post(`${TAURI_ENDPOINT}/get_circuit_breaker_config`, () =>
success({
failureThreshold: 3,
successThreshold: 2,
timeoutSeconds: 60,
errorRateThreshold: 50,
minRequests: 5,
}),
),
http.post(`${TAURI_ENDPOINT}/update_circuit_breaker_config`, () =>
success(true),
),
http.post(`${TAURI_ENDPOINT}/get_provider_health`, () =>
success({
provider_id: "mock-provider",
app_type: "claude",
is_healthy: true,
consecutive_failures: 0,
last_success_at: null,
last_failure_at: null,
last_error: null,
updated_at: new Date().toISOString(),
}),
),
http.post(`${TAURI_ENDPOINT}/reset_circuit_breaker`, () => success(true)),
http.post(`${TAURI_ENDPOINT}/get_circuit_breaker_stats`, () => success(null)),
];