5fe5d51b1b
AI Proxy (was AI Router): - server/lib/ai-proxies.js, proxy-apply.js, routes/ai-proxies.js mounted at /api/ai-proxies and the old /api/ai-routers; `router` still accepted on the start routes; profiles move to <CLAUDE_HOME>/agenthub-proxies/ - dashboard_runs.router renamed in place to proxy: an older build on the same DB fails on that column, so dev and prod sharing one DB update together - a proxy's baseUrl stored as its root (a typed /v1 made /v1/v1/messages); Test probes Claude /v1/messages and Codex /v1/responses - Settings → AI Proxy (/settings/ai-router redirects), ProxyPicker Updates: - every tab reloads onto the new UI build when its WebSocket reconnects to a server of another version (lib/newBuildReload.ts, once per version) - Update now stops waiting on a plugin chain that failed or pulled nothing (the old server never goes down), and reloads as soon as a new version answers Also: - a pane whose command exits at once says so (EPANEEXITED) instead of "Command failed: tmux set-option … mouse on" - the pane drag preview draws where the pane actually lands
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
/**
|
|
* @file ProxyPicker.test.tsx
|
|
* @description The per-session AI Proxy picker: Native plus the saved
|
|
* profiles, Native alone and disabled on a tool that cannot use a proxy,
|
|
* remembering the pick, and falling back to Native when the remembered
|
|
* profile is gone; plus `proxyArg`, which never sends native.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { act, render, renderHook, screen, waitFor } from "@testing-library/react";
|
|
import type { AiProxy } from "../../../lib/api";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { ProxyPicker, proxyArg, useAiProxies, useSessionProxy } from "../ProxyPicker";
|
|
import { api } from "../../../lib/api";
|
|
|
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return {
|
|
...actual,
|
|
api: {
|
|
aiTools: {
|
|
list: vi.fn().mockResolvedValue({
|
|
default: "claude",
|
|
tools: [
|
|
{ id: "claude", label: "Claude Code", proxyCapable: true },
|
|
{ id: "gemini", label: "Gemini", proxyCapable: false },
|
|
].map((x) => ({ ...x, installed: true, enabled: true })),
|
|
}),
|
|
},
|
|
aiProxies: { list: vi.fn() },
|
|
},
|
|
};
|
|
});
|
|
|
|
const native = { name: "native", label: "Native", builtin: true } as AiProxy;
|
|
const nine = { name: "9router", label: "9Router", builtin: false } as AiProxy;
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
vi.mocked(api.aiProxies.list)
|
|
.mockReset()
|
|
.mockResolvedValue({ items: [native, nine] } as never);
|
|
});
|
|
|
|
describe("ProxyPicker", () => {
|
|
it("lists the saved profiles for a tool that can use one", async () => {
|
|
render(
|
|
<ProxyPicker tool="claude" proxies={[native, nine]} value="9router" onChange={() => {}} />
|
|
);
|
|
expect(await screen.findByText("9Router")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows Native, disabled, on a tool with no endpoint to point at a proxy", () => {
|
|
render(
|
|
<ProxyPicker tool="gemini" proxies={[native, nine]} value="9router" onChange={() => {}} />
|
|
);
|
|
const picker = screen.getByTestId("proxy-picker");
|
|
expect(picker.querySelector("button")).toBeDisabled();
|
|
expect(picker).not.toHaveTextContent("9Router");
|
|
});
|
|
|
|
it("falls back to Native when the remembered profile is gone", async () => {
|
|
const onChange = vi.fn();
|
|
render(<ProxyPicker tool="claude" proxies={[native]} value="deleted" onChange={onChange} />);
|
|
await waitFor(() => expect(onChange).toHaveBeenCalledWith("native"));
|
|
});
|
|
|
|
it("useAiProxies reads the saved profiles once", async () => {
|
|
const { result } = renderHook(() => useAiProxies());
|
|
await waitFor(() => expect(result.current).toEqual([native, nine]));
|
|
expect(api.aiProxies.list).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("useSessionProxy / proxyArg", () => {
|
|
it("remembers the last pick for the next session", () => {
|
|
const first = renderHook(() => useSessionProxy());
|
|
expect(first.result.current[0]).toBe("native");
|
|
act(() => first.result.current[1]("9router"));
|
|
expect(renderHook(() => useSessionProxy()).result.current[0]).toBe("9router");
|
|
});
|
|
|
|
it("still reads the pick remembered before the rename", () => {
|
|
localStorage.setItem("agenthub.session.router", "9router");
|
|
expect(renderHook(() => useSessionProxy()).result.current[0]).toBe("9router");
|
|
});
|
|
|
|
it("sends a proxy only for a capable tool and never native", () => {
|
|
expect(proxyArg(true, "9router")).toEqual({ proxy: "9router" });
|
|
expect(proxyArg(true, "native")).toEqual({});
|
|
expect(proxyArg(false, "9router")).toEqual({});
|
|
});
|
|
});
|