- decision endpoint requires pendingKey; mismatch with the stored pending is a 409 - refuse to continue chats over 1.5M chars; compact pending.completed on save - SSE ": ping" heartbeat every 15s, cleared on finish/cancel - in-flight set lives on globalThis (survives HMR, shared across bundles) - resolveOrigin ignores non-http(s) env values; log sanitised cause on tool-load 503 - approval card shows the confirm reason; trust caption updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { NextRequest } from "next/server";
|
|
import { errorResponse, resolveOrigin } from "./giellie-http";
|
|
|
|
const err = (status: unknown, message = "m", extra: Record<string, unknown> = {}) =>
|
|
Object.assign(new Error(message), { status }, extra);
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("errorResponse", () => {
|
|
it("keeps valid 4xx/5xx statuses", async () => {
|
|
expect(errorResponse(err(404, "nope")).status).toBe(404);
|
|
expect(errorResponse(err(401, "Not authenticated.")).status).toBe(401);
|
|
const res = errorResponse(err(409, "busy"));
|
|
expect(await res.json()).toEqual({ error: "busy" });
|
|
});
|
|
|
|
it.each([0, NaN, undefined, 99, 302, 600, 404.5, "500"])("falls back to 500 for status %s without throwing", (s) => {
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
expect(errorResponse(err(s)).status).toBe(500);
|
|
});
|
|
|
|
it("turns upstream PocketBase auth/network failures into 502", () => {
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
for (const s of [0, 401, 403]) {
|
|
const pb = err(s, "pb down", { name: "ClientResponseError" });
|
|
expect(errorResponse(pb).status).toBe(502);
|
|
}
|
|
const viaResponse = err(401, "x", { response: { message: "bad token" } });
|
|
expect(errorResponse(viaResponse).status).toBe(502);
|
|
});
|
|
|
|
it("does not remap PocketBase errors with other statuses", () => {
|
|
expect(errorResponse(err(404, "x", { name: "ClientResponseError" })).status).toBe(404);
|
|
});
|
|
|
|
it("logs >=500 as a plain string and never leaks env secrets", () => {
|
|
vi.stubEnv("AGENT_API_KEY", "sekret-agent-key-123");
|
|
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
errorResponse(err(500, "failed with sekret-agent-key-123"));
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
expect(spy.mock.calls[0][0]).toBe("[giellie]");
|
|
expect(typeof spy.mock.calls[0][1]).toBe("string");
|
|
expect(JSON.stringify(spy.mock.calls)).not.toContain("sekret-agent-key-123");
|
|
});
|
|
|
|
it("does not log client errors", () => {
|
|
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
errorResponse(err(400, "bad"));
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("resolveOrigin", () => {
|
|
const req = () => new NextRequest("https://req.test/api/v1/giellie/x");
|
|
|
|
it("prefers AGENT_API_BASE_URL", () => {
|
|
vi.stubEnv("AGENT_API_BASE_URL", "https://agent.test/some/path");
|
|
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "https://public.test");
|
|
expect(resolveOrigin(req())).toBe("https://agent.test");
|
|
});
|
|
|
|
it("uses NEXT_PUBLIC_BASE_URL when the agent one is unset", () => {
|
|
vi.stubEnv("AGENT_API_BASE_URL", "");
|
|
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "https://public.test/");
|
|
expect(resolveOrigin(req())).toBe("https://public.test");
|
|
});
|
|
|
|
it("falls back to the request origin when unset", () => {
|
|
vi.stubEnv("AGENT_API_BASE_URL", "");
|
|
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "");
|
|
expect(resolveOrigin(req())).toBe("https://req.test");
|
|
});
|
|
|
|
it("ignores a scheme-less env value like localhost:4001 and uses the request origin", () => {
|
|
vi.stubEnv("AGENT_API_BASE_URL", "localhost:4001");
|
|
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "");
|
|
expect(resolveOrigin(req())).toBe("https://req.test");
|
|
});
|
|
|
|
it("skips a non-http scheme and tries the next candidate", () => {
|
|
vi.stubEnv("AGENT_API_BASE_URL", "ftp://agent.test");
|
|
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "http://public.test:3000/x");
|
|
expect(resolveOrigin(req())).toBe("http://public.test:3000");
|
|
});
|
|
|
|
it("falls back when the env value is malformed", () => {
|
|
vi.stubEnv("AGENT_API_BASE_URL", "not a url");
|
|
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "");
|
|
expect(resolveOrigin(req())).toBe("https://req.test");
|
|
});
|
|
});
|