feat(agent): support Responses steering and project-aware tools
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.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { act, cleanup, renderHook } from "@testing-library/react";
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { searchPlaces, type PlaceSearchItem, type PlaceSearchResults } from "../api/place-search-client";
|
||||
import { locateGeocodingResult, markGeocodingLocation } from "../map/geocoding-location";
|
||||
import { useWorkbenchGeocoding } from "./use-workbench-geocoding";
|
||||
|
||||
vi.mock("../api/place-search-client", () => ({ searchPlaces: vi.fn() }));
|
||||
vi.mock("../map/geocoding-location", () => ({ locateGeocodingResult: vi.fn(), markGeocodingLocation: vi.fn() }));
|
||||
const location: PlaceSearchItem = { id: "place-1", name: "南桥路", address: "奉贤区南桥路", longitude: 121.44, latitude: 31.22, category: "道路名", district: "奉贤区" };
|
||||
const other = { ...location, id: "place-2", address: "奉贤区南桥路", longitude: 121.35, latitude: 30.77 };
|
||||
const results: PlaceSearchResults = { items: [location, other], total: 2, truncated: false, region: "奉贤区" };
|
||||
const remove = vi.fn();
|
||||
const options = {
|
||||
active: true, projectId: "project-1", mapRef: { current: {} as MapLibreMap }, mapReady: true,
|
||||
getPadding: () => ({ top: 48, right: 400, bottom: 48, left: 48 }), reducedMotion: true, onLocate: vi.fn()
|
||||
};
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(markGeocodingLocation).mockReturnValue({ remove } as unknown as ReturnType<typeof markGeocodingLocation>);
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("place search selection lifecycle", () => {
|
||||
it("does not move until selection and replaces the previous marker", async () => {
|
||||
vi.mocked(searchPlaces).mockResolvedValue(results);
|
||||
const { result, rerender } = renderHook(useWorkbenchGeocoding, { initialProps: options });
|
||||
act(() => result.current.changeAddress("南桥路"));
|
||||
await act(() => result.current.search());
|
||||
expect(result.current.results).toEqual(results);
|
||||
expect(result.current.result).toBeNull();
|
||||
expect(result.current.choosing).toBe(true);
|
||||
expect(markGeocodingLocation).not.toHaveBeenCalled();
|
||||
act(() => result.current.select(location.id));
|
||||
expect(result.current.choosing).toBe(false);
|
||||
expect(locateGeocodingResult).toHaveBeenCalledWith(options.mapRef.current, location, options.getPadding(), true);
|
||||
act(() => result.current.select(other.id));
|
||||
expect(result.current.result).toEqual(other);
|
||||
expect(remove).toHaveBeenCalledOnce();
|
||||
expect(markGeocodingLocation).toHaveBeenLastCalledWith(options.mapRef.current, other);
|
||||
act(() => result.current.changeSelection());
|
||||
expect(result.current.choosing).toBe(true);
|
||||
expect(result.current.result).toEqual(other);
|
||||
const moves = vi.mocked(locateGeocodingResult).mock.calls.length;
|
||||
act(() => result.current.select(other.id));
|
||||
expect(result.current.choosing).toBe(false);
|
||||
expect(locateGeocodingResult).toHaveBeenCalledTimes(moves + 1);
|
||||
rerender({ ...options, active: false });
|
||||
expect(result.current.result).toEqual(other);
|
||||
act(() => result.current.clear());
|
||||
expect(remove).toHaveBeenCalledTimes(2);
|
||||
expect(result.current.results).toBeNull();
|
||||
});
|
||||
it.each(["edit", "close", "project"])("ignores a late search after %s", async (action) => {
|
||||
let resolve!: (value: PlaceSearchResults) => void;
|
||||
vi.mocked(searchPlaces).mockReturnValue(new Promise((done) => { resolve = done; }));
|
||||
const { result, rerender } = renderHook(useWorkbenchGeocoding, { initialProps: options });
|
||||
act(() => result.current.changeAddress("旧地址"));
|
||||
let pending!: Promise<void>;
|
||||
act(() => { pending = result.current.search(); });
|
||||
const signal = vi.mocked(searchPlaces).mock.calls[0][3];
|
||||
if (action === "edit") act(() => result.current.changeAddress("新地址"));
|
||||
if (action === "close") rerender({ ...options, active: false });
|
||||
if (action === "project") rerender({ ...options, projectId: "project-2" });
|
||||
expect(signal?.aborted).toBe(true);
|
||||
await act(async () => { resolve(results); await pending; });
|
||||
expect(result.current.results).toBeNull();
|
||||
expect(result.current.result).toBeNull();
|
||||
expect(result.current.pending).toBe(false);
|
||||
});
|
||||
it("recovers from errors and clears the map when editing the query", async () => {
|
||||
vi.mocked(searchPlaces).mockRejectedValueOnce(new Error("查询暂不可用")).mockResolvedValueOnce(results);
|
||||
const { result, unmount } = renderHook(() => useWorkbenchGeocoding(options));
|
||||
act(() => result.current.changeAddress("南桥路"));
|
||||
await act(() => result.current.search());
|
||||
expect(result.current.error).toBe("查询暂不可用");
|
||||
await act(() => result.current.search());
|
||||
expect(result.current.error).toBeNull();
|
||||
act(() => result.current.select(location.id));
|
||||
act(() => result.current.changeAddress("新地址"));
|
||||
expect(remove).toHaveBeenCalledOnce();
|
||||
expect(result.current.result).toBeNull();
|
||||
expect(result.current.results).toBeNull();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user