Share project JSON transport, interaction replies and report selectors. Settle tool progress consistently for direct streams, reconnects and history, preserving failure reasons and stopped state.
61 lines
3.2 KiB
TypeScript
61 lines
3.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { findOwnedWorkbenchProject } from "./project-client";
|
|
import { requestWorkbenchJson } from "./request-json";
|
|
|
|
vi.mock("./project-client", async (original) => ({
|
|
...await original<typeof import("./project-client")>(), findOwnedWorkbenchProject: vi.fn()
|
|
}));
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); vi.clearAllMocks(); });
|
|
|
|
describe("project JSON transport", () => {
|
|
it("reuses resolved credentials and preserves request headers without choosing another project", async () => {
|
|
const fetcher = vi.fn().mockResolvedValue(Response.json({ ok: true }));
|
|
vi.stubGlobal("fetch", fetcher);
|
|
await expect(requestWorkbenchJson("/api/v1/probe", "查询", {
|
|
credentials: { projectId: "project-a", accessToken: "token-a" },
|
|
init: { headers: { "X-Request-Id": "request-a" } }
|
|
})).resolves.toEqual({ ok: true });
|
|
const headers = new Headers(fetcher.mock.calls[0][1].headers);
|
|
expect(headers.get("Authorization")).toBe("Bearer token-a");
|
|
expect(headers.get("X-Project-Id")).toBe("project-a");
|
|
expect(headers.get("X-Request-Id")).toBe("request-a");
|
|
expect(findOwnedWorkbenchProject).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects an explicitly empty project instead of falling back to a default", async () => {
|
|
const fetcher = vi.fn(); vi.stubGlobal("fetch", fetcher);
|
|
await expect(requestWorkbenchJson("/api/v1/probe", "查询", { projectId: "" })).rejects.toThrow("所属项目不能为空");
|
|
expect(findOwnedWorkbenchProject).not.toHaveBeenCalled();
|
|
expect(fetcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("preserves caller cancellation before fetching and while reading the body", async () => {
|
|
const controller = new AbortController();
|
|
const reason = new DOMException("changed project", "AbortError");
|
|
const fetcher = vi.fn().mockImplementation(async () => ({ ok: true, json: async () => {
|
|
controller.abort(reason); throw reason;
|
|
} }));
|
|
vi.stubGlobal("fetch", fetcher);
|
|
const options = { credentials: { projectId: "project-a", accessToken: null }, init: { signal: controller.signal } };
|
|
await expect(requestWorkbenchJson("/api/v1/probe", "查询", options)).rejects.toBe(reason);
|
|
await expect(requestWorkbenchJson("/api/v1/probe", "查询", options)).rejects.toBe(reason);
|
|
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("keeps deadlines opt-in and never retries a failed write", async () => {
|
|
const deadline = vi.spyOn(AbortSignal, "timeout");
|
|
const fetcher = vi.fn().mockResolvedValue(Response.json({ detail: "服务繁忙" }, { status: 503 }));
|
|
vi.stubGlobal("fetch", fetcher);
|
|
await expect(requestWorkbenchJson("/api/v1/probe", "提交", {
|
|
credentials: { projectId: "project-a", accessToken: null }, init: { method: "POST", body: "{}" }
|
|
})).rejects.toThrow("服务繁忙");
|
|
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
expect(deadline).not.toHaveBeenCalled();
|
|
fetcher.mockResolvedValue(Response.json({}));
|
|
await requestWorkbenchJson("/api/v1/probe", "地名搜索", {
|
|
credentials: { projectId: "project-a", accessToken: null }, timeoutMs: 20_000
|
|
});
|
|
expect(deadline).toHaveBeenCalledWith(20_000);
|
|
});
|
|
});
|