Files
hermes-agent/web/src/lib/api.test.ts
teknium1 38137d2b6e test: purge low-value tests, lane js06 (375 removed)
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
2026-09-23 03:15:26 -07:00

290 lines
9.3 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
api,
authedFetch,
fetchJSON,
getManagementProfile,
setManagementProfile,
} from "./api";
const reloadMocks = vi.hoisted(() => ({
attemptDashboardTokenReloadOnce: vi.fn(() => false),
clearDashboardTokenReloadAttempt: vi.fn(),
}));
vi.mock("./dashboard-auth-reload", () => ({
attemptDashboardTokenReloadOnce: reloadMocks.attemptDashboardTokenReloadOnce,
clearDashboardTokenReloadAttempt: reloadMocks.clearDashboardTokenReloadAttempt,
}));
const SESSION_HEADER = "X-Hermes-Session-Token";
beforeEach(() => {
reloadMocks.attemptDashboardTokenReloadOnce.mockReset();
reloadMocks.attemptDashboardTokenReloadOnce.mockReturnValue(false);
reloadMocks.clearDashboardTokenReloadAttempt.mockReset();
Object.defineProperty(window, "__HERMES_SESSION_TOKEN__", {
configurable: true,
value: "stale-token",
writable: true,
});
Object.defineProperty(window, "__HERMES_AUTH_REQUIRED__", {
configurable: true,
value: false,
writable: true,
});
});
afterEach(() => {
setManagementProfile("");
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
function jsonFetchMock(body: unknown = { ok: true }) {
return vi.fn<typeof fetch>(
async () =>
new Response(JSON.stringify(body), {
headers: { "Content-Type": "application/json" },
status: 200,
}),
);
}
describe("fetchJSON", () => {
it("tries the one-shot reload path for loopback 401s", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
clone: () => ({
json: async () => ({}),
}),
ok: false,
status: 401,
statusText: "Unauthorized",
text: async () => "Unauthorized",
})),
);
reloadMocks.attemptDashboardTokenReloadOnce.mockReturnValue(true);
const pending = fetchJSON("/api/status");
await expect(Promise.race([pending, Promise.resolve("pending")])).resolves.toBe(
"pending",
);
expect(reloadMocks.attemptDashboardTokenReloadOnce).toHaveBeenCalledTimes(1);
expect(reloadMocks.clearDashboardTokenReloadAttempt).not.toHaveBeenCalled();
});
it("clears the reload latch after a successful response", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
json: async () => ({ ok: true }),
ok: true,
status: 200,
})),
);
await expect(fetchJSON("/api/status")).resolves.toEqual({ ok: true });
expect(reloadMocks.clearDashboardTokenReloadAttempt).toHaveBeenCalledTimes(1);
});
});
describe("api.getModelOptions", () => {
it("keeps explicit profile scoping when refreshing", async () => {
vi.stubGlobal("window", {});
const fetchMock = jsonFetchMock({ providers: [] });
vi.stubGlobal("fetch", fetchMock);
await api.getModelOptions({ profile: "default", refresh: true });
expect(fetchMock).toHaveBeenCalledWith(
"/api/model/options?profile=default&refresh=1&include_unconfigured=1",
expect.objectContaining({ credentials: "include" }),
);
});
});
describe("management profile scope", () => {
// Every family whose routes write into a named profile's home must carry the
// scope; an unprofiled request 400s on a host that merely HAS a second profile.
it.each([
"/api/credentials/pool/anthropic/0",
"/api/dashboard/plugin-providers",
"/api/model/recommended-default",
"/api/local-models",
"/api/ops/restart",
])("scopes %s to the selected management profile", async (path) => {
vi.stubGlobal("window", {});
const fetchMock = jsonFetchMock();
vi.stubGlobal("fetch", fetchMock);
setManagementProfile("worker");
await fetchJSON(path, { method: "POST" });
expect(fetchMock.mock.calls[0][0]).toBe(`${path}?profile=worker`);
});
it("leaves endpoints outside the scoped families alone", async () => {
vi.stubGlobal("window", {});
const fetchMock = jsonFetchMock();
vi.stubGlobal("fetch", fetchMock);
setManagementProfile("worker");
await fetchJSON("/api/sessions");
expect(fetchMock.mock.calls[0][0]).toBe("/api/sessions");
});
it("falls back to the profile this backend serves when nothing is selected", async () => {
vi.stubGlobal("window", { __HERMES_DASHBOARD_PROFILE__: "served" });
const fetchMock = jsonFetchMock();
vi.stubGlobal("fetch", fetchMock);
setManagementProfile("");
expect(getManagementProfile()).toBe("served");
await fetchJSON("/api/credentials/pool/anthropic/0", { method: "DELETE" });
expect(fetchMock.mock.calls[0][0]).toBe(
"/api/credentials/pool/anthropic/0?profile=served",
);
});
it("keeps the selected profile ahead of the serving profile", async () => {
vi.stubGlobal("window", { __HERMES_DASHBOARD_PROFILE__: "served" });
const fetchMock = jsonFetchMock();
vi.stubGlobal("fetch", fetchMock);
setManagementProfile("worker");
expect(getManagementProfile()).toBe("worker");
await fetchJSON("/api/credentials/pool/anthropic/0", { method: "DELETE" });
expect(fetchMock.mock.calls[0][0]).toBe(
"/api/credentials/pool/anthropic/0?profile=worker",
);
});
it("names no profile at all when neither a selection nor a serving profile exists", async () => {
vi.stubGlobal("window", {});
const fetchMock = jsonFetchMock();
vi.stubGlobal("fetch", fetchMock);
setManagementProfile("");
expect(getManagementProfile()).toBe("");
await fetchJSON("/api/credentials/pool/anthropic/0", { method: "DELETE" });
expect(fetchMock.mock.calls[0][0]).toBe("/api/credentials/pool/anthropic/0");
});
it.each([
["/api/ops/backup/download", "/api/ops/backup/download?profile=worker"],
["/api/ops/backup/download?full=1", "/api/ops/backup/download?full=1&profile=worker"],
["/api/ops/backup/download?profile=other", "/api/ops/backup/download?profile=other"],
["/api/sessions/abc/export", "/api/sessions/abc/export"],
])(
"authedFetch resolves %s through the same management scope",
async (path, expected) => {
vi.stubGlobal("window", {});
const fetchMock = vi.fn<typeof fetch>(async () => new Response("binary"));
vi.stubGlobal("fetch", fetchMock);
setManagementProfile("worker");
await authedFetch(path);
expect(fetchMock.mock.calls[0][0]).toBe(expected);
},
);
});
describe("api OAuth helpers", () => {
it("starts OAuth login in gated mode without requiring an injected session token", async () => {
vi.stubGlobal("window", { __HERMES_AUTH_REQUIRED__: true });
const fetchMock = jsonFetchMock({
flow: "device_code",
session_id: "oauth-session",
});
vi.stubGlobal("fetch", fetchMock);
await api.startOAuthLogin("openai-codex");
expect(fetchMock).toHaveBeenCalledWith(
"/api/providers/oauth/openai-codex/start",
expect.objectContaining({
body: "{}",
credentials: "include",
method: "POST",
}),
);
const headers = fetchMock.mock.calls[0][1]?.headers as Headers;
expect(headers.get("Content-Type")).toBe("application/json");
expect(headers.has(SESSION_HEADER)).toBe(false);
});
it("still sends the injected session token for OAuth login in loopback mode", async () => {
vi.stubGlobal("window", { __HERMES_SESSION_TOKEN__: "loopback-token" });
const fetchMock = jsonFetchMock({
flow: "device_code",
session_id: "oauth-session",
});
vi.stubGlobal("fetch", fetchMock);
await api.startOAuthLogin("openai-codex");
const headers = fetchMock.mock.calls[0][1]?.headers as Headers;
expect(headers.get(SESSION_HEADER)).toBe("loopback-token");
});
it("runs provider auth mutations in gated mode via cookie auth", async () => {
vi.stubGlobal("window", { __HERMES_AUTH_REQUIRED__: true });
const fetchMock = jsonFetchMock({ ok: true });
vi.stubGlobal("fetch", fetchMock);
await api.disconnectOAuthProvider("anthropic");
await api.submitOAuthCode("anthropic", "oauth-session", "code-123");
await api.cancelOAuthSession("oauth-session");
await api.revealEnvVar("OPENAI_API_KEY");
for (const call of fetchMock.mock.calls) {
const init = call[1] as RequestInit;
expect(init.credentials).toBe("include");
expect((init.headers as Headers).has(SESSION_HEADER)).toBe(false);
}
});
it("keeps every OAuth operation on the selected management profile", async () => {
vi.stubGlobal("window", {});
const fetchMock = jsonFetchMock({
flow: "device_code",
session_id: "oauth-session",
});
vi.stubGlobal("fetch", fetchMock);
setManagementProfile("worker");
await api.getOAuthProviders();
await api.disconnectOAuthProvider("anthropic");
await api.startOAuthLogin("openai-codex");
await api.submitOAuthCode("anthropic", "oauth-session", "code-123");
await api.pollOAuthSession("anthropic", "oauth-session");
await api.cancelOAuthSession("oauth-session");
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
"/api/providers/oauth?profile=worker",
"/api/providers/oauth/anthropic?profile=worker",
"/api/providers/oauth/openai-codex/start?profile=worker",
"/api/providers/oauth/anthropic/submit?profile=worker",
"/api/providers/oauth/anthropic/poll/oauth-session?profile=worker",
"/api/providers/oauth/sessions/oauth-session?profile=worker",
]);
});
});