import { beforeEach, describe, expect, it, vi } from "vitest"; const mockResolveMnoteWebInternalUrl = vi.fn(); const mockBuildForwardHeaders = vi.fn(); const mockFetch = vi.fn(); vi.mock("@/lib/mnote-web/internal-url", () => ({ resolveMnoteWebInternalUrl: (...args: unknown[]) => mockResolveMnoteWebInternalUrl(...args), })); vi.mock("@/lib/server/forward-headers", () => ({ buildForwardHeaders: (...args: unknown[]) => mockBuildForwardHeaders(...args), })); describe("/api/tree/runtime/reduce route", () => { beforeEach(() => { vi.resetModules(); mockResolveMnoteWebInternalUrl.mockReset(); mockBuildForwardHeaders.mockReset(); mockFetch.mockReset(); vi.stubGlobal("fetch", mockFetch); }); it("通过 3000 同源 POST 代理到 mnote-web runtime reduce,并原样转发 JSON body 与来源 header", async () => { mockResolveMnoteWebInternalUrl.mockResolvedValue("http://127.0.0.1:3104"); mockBuildForwardHeaders.mockResolvedValue( new Headers({ cookie: "session=abc", authorization: "Bearer test-token", }), ); mockFetch.mockResolvedValue( new Response(JSON.stringify({ ok: true, revision: 7 }), { status: 200, statusText: "OK", headers: { "content-type": "application/json; charset=utf-8", "set-cookie": "debug=1", connection: "keep-alive", "transfer-encoding": "chunked", "x-upstream": "mnote-web-runtime", }, }), ); const { POST } = await import("./route"); const body = JSON.stringify({ workspaceId: "ws_body", op: "tree.node.rename", payload: { documentId: "doc_1", title: "新标题" }, }); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/runtime/reduce?workspaceId=ws_query", { method: "POST", headers: { "content-type": "application/json", accept: "application/json", cookie: "session=abc", }, body, }), ); expect(mockFetch).toHaveBeenCalledWith( "http://127.0.0.1:3104/api/tree/runtime/reduce?workspaceId=ws_query", expect.objectContaining({ method: "POST", headers: expect.any(Headers), body, cache: "no-store", redirect: "manual", }), ); const fetchHeaders = mockFetch.mock.calls[0]?.[1]?.headers as Headers; expect(fetchHeaders.get("cookie")).toBe("session=abc"); expect(fetchHeaders.get("authorization")).toBe("Bearer test-token"); expect(fetchHeaders.get("content-type")).toBe("application/json"); expect(fetchHeaders.get("accept")).toBe("application/json"); expect(fetchHeaders.get("x-mnote-source-channel")).toBe("next_tree_runtime_reduce_proxy"); expect(fetchHeaders.get("x-mnote-source-client")).toBe("wolai-frontend"); expect(fetchHeaders.get("x-mnote-workspace-id")).toBe("ws_query"); expect(response.status).toBe(200); expect(response.headers.get("content-type")).toContain("application/json"); expect(response.headers.get("cache-control")).toBe("no-store"); expect(response.headers.get("set-cookie")).toBeNull(); expect(response.headers.get("connection")).toBeNull(); expect(response.headers.get("transfer-encoding")).toBeNull(); expect(response.headers.get("x-upstream")).toBe("mnote-web-runtime"); expect(await response.json()).toEqual({ ok: true, revision: 7 }); }); it("没有 workspaceId query 时使用 x-mnote-workspace-id header fallback", async () => { mockResolveMnoteWebInternalUrl.mockResolvedValue("http://127.0.0.1:3104"); mockBuildForwardHeaders.mockResolvedValue( new Headers({ "x-mnote-workspace-id": "ws_header", }), ); mockFetch.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 202, headers: { "content-type": "application/json", }, }), ); const { POST } = await import("./route"); await POST( new Request("http://127.0.0.1:3000/api/tree/runtime/reduce", { method: "POST", headers: { "content-type": "application/json", "x-mnote-workspace-id": "ws_header", }, body: JSON.stringify({ op: "noop" }), }), ); expect(mockFetch).toHaveBeenCalledWith( "http://127.0.0.1:3104/api/tree/runtime/reduce", expect.objectContaining({ method: "POST", }), ); const fetchHeaders = mockFetch.mock.calls[0]?.[1]?.headers as Headers; expect(fetchHeaders.get("x-mnote-workspace-id")).toBe("ws_header"); }); it("上游调用异常时返回 502 JSON", async () => { mockResolveMnoteWebInternalUrl.mockResolvedValue("http://127.0.0.1:3104"); mockBuildForwardHeaders.mockResolvedValue(new Headers()); mockFetch.mockRejectedValue(new Error("runtime reduce unavailable")); const { POST } = await import("./route"); const response = await POST( new Request("http://127.0.0.1:3000/api/tree/runtime/reduce", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ op: "noop" }), }), ); expect(response.status).toBe(502); expect(response.headers.get("cache-control")).toBe("no-store"); expect(await response.json()).toEqual({ error: "runtime reduce unavailable", }); }); });