Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
62 lines
3.5 KiB
TypeScript
62 lines
3.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { searchPlaces } from "./place-search-client";
|
|
|
|
const item = { id: "place-1", name: "南桥路", address: "奉贤区南桥路", longitude: 121.441891, latitude: 31.220188, category: "道路名", district: "奉贤区" };
|
|
const results = { items: [item], total: 1, truncated: false, region: "奉贤区" };
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
describe("Fengxian place search client", () => {
|
|
it("uses the new endpoint with trimmed keywords, identity and project headers", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(Response.json(results));
|
|
expect(await searchPlaces(" 南桥路 ", "project-1", async () => "read-token", undefined)).toEqual(results);
|
|
const [url, init] = fetchMock.mock.calls[0];
|
|
expect(url).toBe("http://127.0.0.1:8000/api/v1/place-search-requests");
|
|
expect(init?.method).toBe("POST");
|
|
expect(JSON.parse(init?.body as string)).toEqual({ keyword: "南桥路" });
|
|
const headers = new Headers(init?.headers);
|
|
expect(headers.get("Authorization")).toBe("Bearer read-token");
|
|
expect(headers.get("X-Project-Id")).toBe("project-1");
|
|
expect(headers.get("Content-Type")).toBe("application/json");
|
|
});
|
|
it("accepts empty search results", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(Response.json({ ...results, items: [], total: 0 }));
|
|
expect((await searchPlaces("未知位置", "project-1")).items).toEqual([]);
|
|
});
|
|
it("accepts at most ten Fengxian candidates", async () => {
|
|
const capped = { ...results, items: Array.from({ length: 10 }, (_, index) => ({ ...item, id: `place-${index}` })), total: 10, truncated: true };
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(Response.json(capped));
|
|
expect(await searchPlaces("南桥路", "project-1")).toEqual(capped);
|
|
});
|
|
it.each([
|
|
{ ...results, items: [{ ...item, longitude: 181 }] },
|
|
{ ...results, items: [{ ...item, latitude: 91 }] },
|
|
{ ...results, items: [item, item] },
|
|
{ ...results, region: "北京市" },
|
|
{ ...results, items: [{ ...item, district: "徐汇区" }] },
|
|
{ ...results, total: 0 },
|
|
{ ...results, total: 11 },
|
|
{ ...results, items: Array.from({ length: 11 }, (_, index) => ({ ...item, id: `place-${index}` })) },
|
|
{ status: "0", location: { lon: "121", lat: "31" } }
|
|
])("rejects malformed, mismatched or legacy responses: %j", async (payload) => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(Response.json(payload));
|
|
await expect(searchPlaces("南桥路", "project-1")).rejects.toThrow("未获得有效的地名搜索结果");
|
|
});
|
|
it("does not fetch blank keywords", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch");
|
|
await expect(searchPlaces(" ", "project-1")).rejects.toThrow("请输入");
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
it.each([403, 502, 503, 504])("preserves server failures for HTTP %s", async (status) => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(Response.json({ detail: "查询暂不可用" }, { status }));
|
|
await expect(searchPlaces("南桥路", "project-1")).rejects.toThrow("查询暂不可用");
|
|
});
|
|
it("does not fetch after cancellation during token refresh", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch");
|
|
const controller = new AbortController();
|
|
await expect(searchPlaces("南桥路", "project-1", async () => {
|
|
controller.abort(); return "read-token";
|
|
}, controller.signal)).rejects.toThrow();
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|