fix: unify chat process and reply presentation
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { responseFixtureWriter } from "./support/responses-fixture";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
import type { InteractionRequest } from "../../src/features/agent/api/interaction-requests";
|
||||
|
||||
for (const width of [1440, 320]) {
|
||||
test(`host question restores draft and requires explicit submit at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" });
|
||||
await mockAgentApi(page, { sessions: [{ session_id: "host-question", title: "待补充范围", created_at: Date.now(), updated_at: Date.now() }] });
|
||||
const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, PUT, POST, OPTIONS", "Access-Control-Allow-Headers": "Authorization, Content-Type, X-Project-Id" };
|
||||
let record: InteractionRequest = { version: "agent-interaction-request@1", interaction_id: "i", message_id: "m", turn_id: "t", call_id: "c", questions: [{ question_id: "q", title: "核查范围", body: "选择本次核查范围", options: [{ id: "north", label: "北侧管网" }, { id: "all", label: "全部管网" }], mode: "single", required: true, allow_custom: true }], revision: 0, draft_revision: 0, draft: [], status: "pending", created_at: new Date().toISOString() };
|
||||
const writes: string[] = [];
|
||||
const inputs: Array<{ id: string; text: string; delivery: string; status: string }> = [];
|
||||
await page.route("**/api/v1/responses", (route) => {
|
||||
const body = route.request().postDataJSON();
|
||||
expect(body.stream).toBe(false);
|
||||
const input = { id: body.tjwater.input_id, text: body.input, delivery: body.tjwater.delivery, status: "pending" };
|
||||
inputs.push(input);
|
||||
return route.fulfill({ headers, status: 202, json: input });
|
||||
});
|
||||
await page.route("**/api/v1/session/host-question/inputs", (route) => route.fulfill({ headers, json: { items: inputs } }));
|
||||
await page.route("**/api/v1/session/host-question?**", (route) => route.fulfill({ headers, json: { session_id: "host-question", title: "待补充范围", purpose: "ordinary_chat", status: "active", is_streaming: false, messages: [{ id: "m", role: "assistant", parts: [{ type: "text", text: "请补充核查范围。" }] }] } }));
|
||||
await page.route("**/api/v1/session/host-question/interactions**", async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers });
|
||||
const action = new URL(request.url()).pathname.split("/").at(-1)!;
|
||||
if (request.method() === "GET") return route.fulfill({ headers, json: action === "interactions" ? { items: [record] } : record });
|
||||
writes.push(action);
|
||||
const body = request.postDataJSON();
|
||||
if (action === "draft") record = { ...record, draft: body.answers, draft_revision: record.draft_revision + 1 };
|
||||
if (action === "cancel") record = { ...record, status: "cancelled", revision: 1 };
|
||||
return route.fulfill({ headers, json: record });
|
||||
});
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
|
||||
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
||||
await panel.getByRole("button", { name: /历史/ }).click();
|
||||
await page.locator("button.agent-history-session-trigger").filter({ hasText: "待补充范围" }).click();
|
||||
const card = panel.getByRole("article", { name: "需要补充的信息" });
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card.getByRole("button", { name: "提交回答" })).toBeDisabled();
|
||||
await expect(card.getByRole("button", { name: "跳过", exact: true })).toHaveCount(0);
|
||||
await card.getByRole("radio", { name: "北侧管网" }).check();
|
||||
await card.getByRole("textbox", { name: "核查范围:补充说明" }).fill("优先检查压力下降区域");
|
||||
await expect.poll(() => record.draft[0]?.custom_text).toBe("优先检查压力下降区域");
|
||||
expect(writes.length).toBeGreaterThan(0);
|
||||
expect(writes.every((action) => action === "draft")).toBe(true);
|
||||
await expect(card.getByRole("button", { name: "提交回答" })).toBeEnabled();
|
||||
await card.screenshot({ path: `/tmp/host-question-${width}.png` });
|
||||
const bounds = await card.boundingBox();
|
||||
expect(bounds!.x).toBeGreaterThanOrEqual(0);
|
||||
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width);
|
||||
await panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("先核对压力测点");
|
||||
await panel.getByRole("button", { name: "引导当前 Agent 任务", exact: true }).click();
|
||||
await expect.poll(() => inputs.map((input) => input.delivery)).toEqual(["steer"]);
|
||||
await expect(card.getByText("等待回答", { exact: true })).toBeVisible();
|
||||
expect(writes.every((action) => action === "draft")).toBe(true);
|
||||
await card.getByRole("button", { name: "取消提问" }).click();
|
||||
await expect(card.getByText("已取消", { exact: true })).toBeVisible();
|
||||
await expect(card.getByRole("button", { name: "提交回答" })).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
for (const responseMode of ["completed_without_event", "pending_conflict"] as const) {
|
||||
test(`collaboration restores a persisted question after ${responseMode} and submits its answer`, async ({ page }, testInfo) => {
|
||||
const width = responseMode === "pending_conflict" ? 320 : 1440;
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" });
|
||||
const sessionId = "collaboration-question";
|
||||
await mockAgentApi(page, { sessions: [{ session_id: sessionId, title: "公司位置协作", purpose: "task_collaboration", created_at: Date.now(), updated_at: Date.now() }] });
|
||||
const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, PUT, POST, OPTIONS", "Access-Control-Allow-Headers": "Authorization, Content-Type, X-Project-Id" };
|
||||
let pending = false;
|
||||
let listReads = 0;
|
||||
let responses = 0;
|
||||
let record: InteractionRequest = { version: "agent-interaction-request@1", interaction_id: "locate-target", message_id: "question-message", turn_id: "question-turn", call_id: "ask-call", questions: [{ question_id: "target", title: "选择定位对象", body: "选择公司上报地址或模拟管段。", options: [{ id: "company", label: "公司上报地址" }, { id: "pipes", label: "模拟管段" }], mode: "single", required: true, allow_custom: true }], revision: 0, draft_revision: 0, draft: [], status: "pending", created_at: new Date().toISOString() };
|
||||
await page.route(`**/api/v1/session/${sessionId}?**`, (route) => route.fulfill({ headers, json: { session_id: sessionId, title: "公司位置协作", purpose: "task_collaboration", status: "active", is_streaming: false, messages: [{ id: "context-message", role: "assistant", parts: [{ type: "text", text: "调查报告已就绪。" }] },
|
||||
...(record.delivery ? [{ id: record.delivery.native_message_id, role: "user", parts: [{ type: "text", text: "用户提交的提问回复:\n选择定位对象:公司上报地址;使用报告中的公司位置" }] }] : [])] } }));
|
||||
await page.route(`**/api/v1/session/${sessionId}/interactions**`, async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers });
|
||||
const action = new URL(request.url()).pathname.split("/").at(-1)!;
|
||||
if (request.method() === "GET") {
|
||||
if (action === "interactions") listReads++;
|
||||
return route.fulfill({ headers, json: action === "interactions" ? { items: pending ? [record] : [] } : record });
|
||||
}
|
||||
const body = request.postDataJSON();
|
||||
if (action === "draft") record = { ...record, draft: body.answers, draft_revision: record.draft_revision + 1 };
|
||||
if (action === "reply") record = { ...record, status: "answered", revision: 1,
|
||||
answers: body.answers.map((answer: { question_id: string; selected_option_ids: string[]; custom_text: string }) => ({ ...answer, selected_options: record.questions[0].options.filter((option) => answer.selected_option_ids.includes(option.id)) })),
|
||||
delivery: { native_message_id: `msg_${"a".repeat(32)}`, status: "queued", revision: 0 } };
|
||||
if (action === "resume") record = { ...record, delivery: { ...record.delivery!, status: "queued", revision: record.delivery!.revision + 1 } };
|
||||
return route.fulfill({ headers, json: record });
|
||||
});
|
||||
await page.route("**/api/v1/responses", (route) => {
|
||||
responses++;
|
||||
const body = route.request().postDataJSON();
|
||||
if (responses === 1) {
|
||||
pending = true;
|
||||
if (responseMode === "pending_conflict") return route.fulfill({ headers, status: 409, json: { code: "INTERACTION_PENDING", message: "请先回答或取消当前提问。" } });
|
||||
} else {
|
||||
expect(body.tjwater.interaction_id).toBe(record.interaction_id);
|
||||
expect(record.answers?.[0]).toMatchObject({ selected_option_ids: responseMode === "pending_conflict" ? ["company"] : [], custom_text: "使用报告中的公司位置" });
|
||||
if (responseMode === "pending_conflict" && responses === 2) {
|
||||
record = { ...record, delivery: { ...record.delivery!, status: "blocked", revision: 1 } };
|
||||
return route.fulfill({ headers, status: 503, body: "暂时无法继续,请重试。" });
|
||||
}
|
||||
record = { ...record, delivery: { ...record.delivery!, status: "delivered", revision: record.delivery!.revision + 1 } };
|
||||
}
|
||||
const write = responseFixtureWriter(sessionId);
|
||||
return route.fulfill({ headers: { ...headers, "Content-Type": "text/event-stream" }, body: [
|
||||
{ type: "start", messageId: responses === 1 ? "question-message" : "located-message" },
|
||||
{ type: "text-start", id: "text" },
|
||||
{ type: "text-delta", id: "text", delta: responses === 1 ? "请选择要定位的对象。" : "已定位公司上报地址。" },
|
||||
{ type: "text-end", id: "text" },
|
||||
{ type: "finish", finishReason: "stop" }
|
||||
].map(write).join("") });
|
||||
});
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
|
||||
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
||||
await panel.getByRole("button", { name: /历史/ }).click();
|
||||
await page.locator("button.agent-history-session-trigger").filter({ hasText: "公司位置协作" }).click();
|
||||
await expect.poll(() => listReads).toBeGreaterThan(0);
|
||||
await expect(panel.getByRole("article", { name: "需要补充的信息" })).toHaveCount(0);
|
||||
const readsBeforeSend = listReads;
|
||||
const input = panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" });
|
||||
await input.fill("定位到公司上报地址");
|
||||
await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click();
|
||||
const card = panel.getByRole("article", { name: "需要补充的信息" });
|
||||
await expect(card).toBeVisible();
|
||||
expect(listReads).toBeGreaterThan(readsBeforeSend);
|
||||
await expect(panel.getByText("离线", { exact: true })).toHaveCount(0);
|
||||
await expect(panel.getByRole("button", { name: "引导当前 Agent 任务", exact: true })).toBeDisabled();
|
||||
if (responseMode === "pending_conflict") await card.getByRole("radio", { name: "公司上报地址", exact: true }).check();
|
||||
await card.getByRole("textbox", { name: "选择定位对象:补充说明" }).fill("使用报告中的公司位置");
|
||||
await card.screenshot({ path: testInfo.outputPath(`recovered-${responseMode}.png`) });
|
||||
await card.getByRole("button", { name: "提交回答", exact: true }).click();
|
||||
const reply = panel.getByRole("group", { name: "已提交的回答" });
|
||||
await expect(reply).toHaveCount(1);
|
||||
await expect(reply.getByText("已回答", { exact: true })).toBeVisible();
|
||||
await expect(reply.getByText("使用报告中的公司位置", { exact: false })).toBeVisible();
|
||||
if (responseMode === "pending_conflict") {
|
||||
await expect(reply.getByText("继续失败,可以重试", { exact: true })).toBeVisible();
|
||||
await reply.getByRole("button", { name: "重试继续", exact: true }).click();
|
||||
}
|
||||
await expect(reply.getByText("已继续处理", { exact: true })).toBeVisible();
|
||||
await expect(card).toHaveCount(0);
|
||||
await expect(panel.getByText("用户提交的提问回复", { exact: false })).toHaveCount(0);
|
||||
expect(await reply.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
|
||||
await reply.screenshot({ path: testInfo.outputPath(`submitted-${responseMode}.png`) });
|
||||
await reply.getByText("使用报告中的公司位置", { exact: true }).click({ clickCount: 3 });
|
||||
await page.keyboard.press("Control+c");
|
||||
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe("使用报告中的公司位置");
|
||||
await expect(panel.getByText("已定位公司上报地址。", { exact: true })).toBeVisible();
|
||||
expect(responses).toBe(responseMode === "pending_conflict" ? 3 : 2);
|
||||
await expect(reply).toHaveCount(1);
|
||||
await input.fill("继续核查");
|
||||
await expect(panel.getByRole("button", { name: "发送 Agent 指令", exact: true })).toBeEnabled();
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
|
||||
await panel.getByRole("button", { name: /历史/ }).click();
|
||||
await page.locator("button.agent-history-session-trigger").filter({ hasText: "公司位置协作" }).click();
|
||||
await expect(reply).toHaveCount(1);
|
||||
await expect(reply.getByText("使用报告中的公司位置", { exact: false })).toBeVisible();
|
||||
await expect(panel.getByText("用户提交的提问回复", { exact: false })).toHaveCount(0);
|
||||
await expect(card).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user