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:
@@ -1,7 +1,54 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createAgentApiClient } from "./client";
|
||||
|
||||
describe("Agent prompt suggestions", () => {
|
||||
it("submits and edits pending input through the scoped Responses and session endpoints", async () => {
|
||||
const input = { id: `msg_${"1".repeat(32)}`, text: "补充压力测点", delivery: "steer", status: "pending" };
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(Response.json(input)).mockResolvedValueOnce(Response.json({ items: [input] }))
|
||||
.mockResolvedValueOnce(Response.json({ ...input, delivery: "queue" }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const client = createAgentApiClient("http://agent.local", { getAccessToken: async () => "token", getProjectId: async () => "project" });
|
||||
await expect(client.submitResponseInput("session-1", input.text, "steer", input.id)).resolves.toEqual(input);
|
||||
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({ input: input.text, conversation: "session-1", stream: false, tjwater: { delivery: "steer", input_id: input.id } });
|
||||
await expect(client.listResponseInputs("session-1")).resolves.toEqual([input]);
|
||||
await expect(client.updateResponseInput("session-1", input.id, "queue")).resolves.toEqual({ ...input, delivery: "queue" });
|
||||
expect(String(fetchMock.mock.calls[0]?.[0])).toBe("http://agent.local/api/v1/responses");
|
||||
expect(String(fetchMock.mock.calls[2]?.[0])).toContain(`/api/v1/session/session-1/inputs/${input.id}`);
|
||||
for (const [, request] of fetchMock.mock.calls) {
|
||||
expect(new Headers(request?.headers).get("Authorization")).toBe("Bearer token");
|
||||
expect(new Headers(request?.headers).get("X-Project-Id")).toBe("project");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the scoped Agent request and rejects invented activity references", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(Response.json({ suggestions: [{ question: "核对城南压力告警的证据", activity_id: "alert:pressure" }] }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const client = createAgentApiClient("http://agent.local", { getAccessToken: async () => "token", getProjectId: async () => "project" });
|
||||
const activities = [{ id: "alert:pressure", kind: "alert" as const, title: "城南压力告警", occurred_at: "2026-10-09T07:00:00Z", origin: "backend" as const }];
|
||||
const controller = new AbortController();
|
||||
expect(await client.generatePromptSuggestions(activities, "test/model", controller.signal)).toEqual([{ question: "核对城南压力告警的证据", activityId: "alert:pressure" }]);
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe("http://agent.local/api/v1/suggestions");
|
||||
expect(new Headers(init.headers).get("Authorization")).toBe("Bearer token");
|
||||
expect(new Headers(init.headers).get("X-Project-Id")).toBe("project");
|
||||
expect(init.signal).toBe(controller.signal);
|
||||
fetchMock.mockResolvedValue(Response.json({ suggestions: [{ question: "引用其他项目", activity_id: "unknown" }] }));
|
||||
await expect(client.generatePromptSuggestions(activities, "test/model")).rejects.toThrow("Invalid Agent suggestion");
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Agent API client sessions", () => {
|
||||
it("forces renewal and sends only the refresh request id with verified context headers", async () => {
|
||||
const getAccessToken = vi.fn(async () => "TEST_NEW_TOKEN");
|
||||
const fetchMock = vi.fn(async () => Response.json({ accepted: true }, { status: 202 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
await createAgentApiClient("http://agent.local", { getAccessToken, getProjectId: async () => "p" }).refreshCredential("s", "request-1");
|
||||
expect(getAccessToken).toHaveBeenCalledWith(true);
|
||||
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
||||
expect(new Headers(init.headers).get("Authorization")).toBe("Bearer TEST_NEW_TOKEN");
|
||||
expect(JSON.parse(String(init.body))).toEqual({ request_id: "request-1" });
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
@@ -85,10 +132,10 @@ describe("Agent API client sessions", () => {
|
||||
});
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"http://not-agent.local/api/v1/agent/chat/ui-registry",
|
||||
"http://not-agent.local/api/v1/ui-registry",
|
||||
undefined
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, "http://agent.local/api/v1/agent/chat/ui-registry", undefined);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, "http://agent.local/api/v1/ui-registry", undefined);
|
||||
});
|
||||
|
||||
it("sends title update payloads using backend field names", async () => {
|
||||
@@ -100,7 +147,7 @@ describe("Agent API client sessions", () => {
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://agent.local/api/v1/agent/chat/session/session-1/title",
|
||||
"http://agent.local/api/v1/session/session-1/title",
|
||||
expect.objectContaining({
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user