bdc7916196
Squashed integration of the user-facing message audit for this surface set. Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { fetchJSON } from "./api";
|
|
import { API_UNREACHABLE_MESSAGE, ApiError, errorMessage, extractDetail } from "./api-error";
|
|
|
|
vi.mock("./dashboard-auth-reload", () => ({
|
|
attemptDashboardTokenReloadOnce: vi.fn(() => false),
|
|
clearDashboardTokenReloadAttempt: vi.fn(),
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
Object.defineProperty(window, "__HERMES_SESSION_TOKEN__", {
|
|
configurable: true,
|
|
value: "tok",
|
|
writable: true,
|
|
});
|
|
Object.defineProperty(window, "__HERMES_AUTH_REQUIRED__", {
|
|
configurable: true,
|
|
value: false,
|
|
writable: true,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("fetchJSON error contract", () => {
|
|
it("throws an ApiError whose message is the JSON detail, not the status or body", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn<typeof fetch>(
|
|
async () =>
|
|
new Response(JSON.stringify({ detail: "Server 'foo' not found" }), {
|
|
headers: { "Content-Type": "application/json" },
|
|
status: 404,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const err = await fetchJSON("/api/mcp/servers/foo").catch((e: unknown) => e);
|
|
expect(err).toBeInstanceOf(ApiError);
|
|
const apiErr = err as ApiError;
|
|
expect(apiErr.message).toBe("Server 'foo' not found");
|
|
expect(apiErr.message).not.toMatch(/404|detail|\{/);
|
|
expect(apiErr.status).toBe(404);
|
|
expect(apiErr.details).toContain("404");
|
|
// The toast pattern `\`Error: ${e}\`` must no longer double-prefix.
|
|
expect(`${errorMessage(apiErr)}`).not.toMatch(/^Error:/);
|
|
});
|
|
|
|
it("maps a detail-less status to a plain sentence with no HTTP code lead", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn<typeof fetch>(async () => new Response("", { status: 500 })),
|
|
);
|
|
const err = (await fetchJSON("/api/status").catch((e: unknown) => e)) as ApiError;
|
|
expect(err.message).not.toMatch(/^\d{3}/);
|
|
expect(err.message).toMatch(/internal error/i);
|
|
});
|
|
|
|
it("turns a network failure into the 'is hermes dashboard running' sentence", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn<typeof fetch>(async () => {
|
|
throw new TypeError("Failed to fetch");
|
|
}),
|
|
);
|
|
const err = (await fetchJSON("/api/status").catch((e: unknown) => e)) as ApiError;
|
|
expect(err).toBeInstanceOf(ApiError);
|
|
expect(err.message).toBe(API_UNREACHABLE_MESSAGE);
|
|
expect(err.message).not.toContain("Failed to fetch");
|
|
expect(err.status).toBe(0);
|
|
expect(err.body).toContain("Failed to fetch");
|
|
});
|
|
|
|
it("logs status, path and body to the console so bug reports keep what the toast drops", async () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn<typeof fetch>(async () => new Response('{"detail":"nope"}', { status: 503 })),
|
|
);
|
|
await fetchJSON("/api/status").catch(() => null);
|
|
|
|
const logged = warn.mock.calls.map((c) => c.map(String).join(" ")).join("\n");
|
|
expect(logged).toContain("503");
|
|
expect(logged).toContain("/api/status");
|
|
expect(logged).toContain("nope");
|
|
|
|
warn.mockClear();
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn<typeof fetch>(async () => {
|
|
throw new TypeError("Failed to fetch");
|
|
}),
|
|
);
|
|
await fetchJSON("/api/status").catch(() => null);
|
|
expect(warn.mock.calls.map((c) => c.map(String).join(" ")).join("\n")).toContain("Failed to fetch");
|
|
});
|
|
});
|
|
|
|
describe("extractDetail", () => {
|
|
it("joins FastAPI validation errors and ignores HTML bodies", () => {
|
|
expect(
|
|
extractDetail(JSON.stringify({ detail: [{ msg: "field required", loc: ["body", "x"] }] })),
|
|
).toBe("field required");
|
|
expect(extractDetail("<html><body>502 Bad Gateway</body></html>")).toBeNull();
|
|
expect(extractDetail("")).toBeNull();
|
|
});
|
|
});
|