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,9 @@
|
||||
import { responseFixtureWriter } from "./support/responses-fixture";
|
||||
import { createServer, type Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockScadaApi } from "./support/mock-scada-api";
|
||||
import { expectThemeColor } from "./support/theme-colors";
|
||||
|
||||
const STREAM_CHUNK_SIZE = 18;
|
||||
const STREAM_CHUNK_DELAY_MS = 20;
|
||||
@@ -17,16 +19,14 @@ test.beforeAll(async () => {
|
||||
streamServer = createServer((_request, response) => {
|
||||
response.writeHead(200, {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "text/event-stream",
|
||||
"X-Vercel-AI-UI-Message-Stream": "v1"
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
"Content-Type": "text/event-stream"
|
||||
});
|
||||
|
||||
const content = createLongMarkdownResponse();
|
||||
const chunks = splitIntoCodePointChunks(content, STREAM_CHUNK_SIZE);
|
||||
const send = (chunk: Record<string, unknown>) => {
|
||||
response.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
||||
};
|
||||
const encode = responseFixtureWriter();
|
||||
const send = (chunk: Record<string, unknown>) => { response.write(encode(chunk)); };
|
||||
|
||||
send({ type: "start", messageId: "assistant-stream-stability" });
|
||||
send({
|
||||
@@ -186,7 +186,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
body: `globalThis.__TJWATER_CONFIG__ = ${JSON.stringify({
|
||||
TJWATER_AUTH_MODE: "disabled",
|
||||
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
|
||||
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
|
||||
TJWATER_AGENT_URL: "http://127.0.0.1:8787",
|
||||
TJWATER_MAP_URL: "https://stream-map.invalid/geoserver",
|
||||
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
|
||||
TJWATER_MAPBOX_ACCESS_TOKEN: "",
|
||||
@@ -195,7 +195,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
})
|
||||
);
|
||||
await page.route("https://stream-map.invalid/**", (route) => route.fulfill({ status: 204 }));
|
||||
await page.route("**/api/v1/agent/chat/**", async (route) => {
|
||||
await page.route("http://127.0.0.1:8787/api/v1/**", async (route) => {
|
||||
if (route.request().method() === "OPTIONS") {
|
||||
await route.fulfill({
|
||||
status: 204,
|
||||
@@ -210,7 +210,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
|
||||
if (path.endsWith("/stream")) {
|
||||
if (path.endsWith("/responses")) {
|
||||
await route.continue({ url: streamServerUrl });
|
||||
return;
|
||||
}
|
||||
@@ -244,7 +244,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
page.getByRole("button", { name: "选择 Agent 模型" }).filter({ visible: true }).first()
|
||||
).toContainText("测试模型", { timeout: 10_000 });
|
||||
const prompt = page
|
||||
.getByPlaceholder(/输入调度问题/)
|
||||
.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" })
|
||||
.filter({ visible: true })
|
||||
.first();
|
||||
await expect(prompt).toBeVisible();
|
||||
@@ -311,7 +311,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
await sendButton.click();
|
||||
const userMessage = page.locator(".agent-panel-user-message");
|
||||
const assistantMessage = page.locator(".agent-panel-message").first();
|
||||
await expect(userMessage).toHaveCSS("background-color", "oklch(0.965 0.018 255)");
|
||||
await expectThemeColor(userMessage, "background-color", "--agent-user-message-surface");
|
||||
expect(
|
||||
await userMessage.evaluate((element) => getComputedStyle(element).backgroundColor)
|
||||
).not.toBe(
|
||||
@@ -323,26 +323,10 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
await expect
|
||||
.poll(() => assistantMessage.evaluate((element) => getComputedStyle(element).boxShadow))
|
||||
.toBe(await userMessage.evaluate((element) => getComputedStyle(element).boxShadow));
|
||||
const operationalBrief = page.locator(".agent-panel-control").filter({
|
||||
has: page.getByRole("button", { name: "预览影响", exact: true })
|
||||
});
|
||||
await expect(operationalBrief).toBeVisible();
|
||||
await expect(operationalBrief).toContainText("Agent 分析中");
|
||||
await expect(operationalBrief.getByRole("button", { name: "预览影响" })).toBeDisabled();
|
||||
await expect(operationalBrief.getByRole("button", { name: "检查证据" })).toBeDisabled();
|
||||
await expect(operationalBrief).toHaveCSS("position", "static");
|
||||
await expect(operationalBrief).toHaveCSS("border-radius", "12px");
|
||||
// Desktop uses an inline brief above the conversation, not the legacy floating overlay.
|
||||
await expect(page.locator(".agent-panel-operational-brief")).toHaveCount(0);
|
||||
await expectThemeColor(page.locator("[data-agent-conversation-state=active]"), "background-color", "--agent-dock-reading");
|
||||
await expect(page.locator(".agent-panel-shell")).toHaveClass(/surface-dock/);
|
||||
await expect(page.locator(".agent-panel-operational-float")).toHaveCount(0);
|
||||
await expect
|
||||
.poll(() =>
|
||||
operationalBrief.evaluate((element) => {
|
||||
const conversation = document.querySelector(".agent-panel-conversation")!;
|
||||
return conversation.getBoundingClientRect().top - element.getBoundingClientRect().bottom;
|
||||
})
|
||||
)
|
||||
.toBeGreaterThanOrEqual(0);
|
||||
|
||||
const liveProgress = page.getByRole("list", { name: "最近执行进度" });
|
||||
await expect(liveProgress.locator("li")).toHaveCount(3);
|
||||
@@ -470,9 +454,9 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
const expandedLiveProgress = page.getByRole("list", { name: "全部执行进度" });
|
||||
await expect(expandedLiveProgress.locator("li")).toHaveCount(4);
|
||||
await expect(expandedLiveProgress.locator(".status-badge")).toHaveCount(0);
|
||||
await expect(expandedLiveProgress.locator('[data-progress-status="running"]').first()).toHaveCSS(
|
||||
"color",
|
||||
"oklch(0.546 0.245 262.881)"
|
||||
await expectThemeColor(
|
||||
expandedLiveProgress.locator('[data-progress-status="running"]').first(),
|
||||
"color", "--action-blue-hover"
|
||||
);
|
||||
await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
@@ -494,10 +478,8 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
resumeStream();
|
||||
|
||||
await expect(
|
||||
page.getByText("分析完成", { exact: true }).filter({ visible: true }).first()
|
||||
).toBeVisible({
|
||||
timeout: 10_000
|
||||
});
|
||||
page.getByRole("button", { name: "发送 Agent 指令" }).filter({ visible: true }).first()
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(progressToggle.locator("../..")).toContainText("步骤4/4");
|
||||
await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
|
||||
await expect(
|
||||
@@ -618,7 +600,7 @@ test("stream completion and the plan card remain positionally stable", async ({
|
||||
expect(pressedScrollButtonBox.y).toBe(scrollButtonBox.y + 1);
|
||||
await page.mouse.move(scrollButtonBox.x - 20, scrollButtonBox.y);
|
||||
await page.mouse.up();
|
||||
await expect(scrollToLatestButton).toHaveCSS("background-color", "rgba(255, 255, 255, 0.94)");
|
||||
await expectThemeColor(scrollToLatestButton, "background-color", "--agent-dock-overlay-surface");
|
||||
await expect
|
||||
.poll(() =>
|
||||
scrollToLatestButton.evaluate((element) => getComputedStyle(element).backdropFilter)
|
||||
|
||||
Reference in New Issue
Block a user