feat: add project-aware agent workspace
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createServer, type ServerResponse } from "node:http";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test, type Route } from "@playwright/test";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
import { responseFixtureWriter } from "./support/responses-fixture";
|
||||
@@ -11,6 +11,8 @@ for (const width of [1440, 375, 320]) {
|
||||
const admitted: Array<{ id: string; text: string; delivery: string }> = [];
|
||||
let response: ServerResponse | undefined;
|
||||
let encode = responseFixtureWriter();
|
||||
let heldWithdrawal: Route | undefined;
|
||||
let holdWithdrawal = false;
|
||||
const send = (chunk: Record<string, unknown>) => response!.write(encode(chunk));
|
||||
const server = createServer((_request, output) => {
|
||||
response = output;
|
||||
@@ -49,6 +51,15 @@ for (const width of [1440, 375, 320]) {
|
||||
return;
|
||||
}
|
||||
if (path.endsWith("/inputs")) { await route.fulfill({ headers, json: { items: [...pending.values()] } }); return; }
|
||||
if (path.includes("/inputs/") && request.method() === "DELETE") {
|
||||
if (holdWithdrawal) { heldWithdrawal = route; return; }
|
||||
const id = path.split("/").at(-1)!;
|
||||
const item = pending.get(id);
|
||||
if (!item) { await route.fulfill({ status: 409, headers, json: { message: "消息已开始处理,无法退回编辑。" } }); return; }
|
||||
pending.delete(id);
|
||||
await route.fulfill({ headers, json: { ...item, status: "cancelled" } });
|
||||
return;
|
||||
}
|
||||
if (path.includes("/inputs/") && request.method() === "PATCH") {
|
||||
const item = pending.get(path.split("/").at(-1)!);
|
||||
if (!item) { await route.fulfill({ status: 409, headers, json: { message: "Consumed" } }); return; }
|
||||
@@ -69,7 +80,7 @@ for (const width of [1440, 375, 320]) {
|
||||
await composer.fill("核对压力变化");
|
||||
await page.getByRole("button", { name: "发送 Agent 指令" }).click();
|
||||
const deliveryMode = page.getByRole("button", { name: "运行中消息发送方式" });
|
||||
await expect(deliveryMode).toHaveText("引导当前任务");
|
||||
await expect(deliveryMode).toHaveText("引导");
|
||||
await deliveryMode.focus();
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await expect(page.getByRole("menuitemradio", { name: "引导当前任务" })).toHaveAttribute("aria-checked", "true");
|
||||
@@ -80,31 +91,89 @@ for (const width of [1440, 375, 320]) {
|
||||
await page.getByRole("menu").screenshot({ path: testInfo.outputPath("delivery-menu.png"), animations: "disabled" });
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(deliveryMode).toBeFocused();
|
||||
await expect(deliveryMode).toHaveText("引导当前任务");
|
||||
await expect(deliveryMode).toHaveText("引导");
|
||||
await expect(page.getByRole("button", { name: "停止 Agent 指令" })).toBeVisible();
|
||||
const actionSlot = await page.getByRole("button", { name: "停止 Agent 指令" }).boundingBox();
|
||||
await composer.fill("补充当前任务");
|
||||
await expect(page.getByRole("button", { name: "停止 Agent 指令" })).toHaveCount(0);
|
||||
const sendSlot = await page.getByRole("button", { name: "引导当前 Agent 任务" }).boundingBox();
|
||||
expect(sendSlot).toEqual(actionSlot);
|
||||
await page.getByRole("button", { name: "引导当前 Agent 任务" }).click();
|
||||
const pendingMode = page.getByRole("button", { name: "消息发送方式:补充当前任务" });
|
||||
await expect(pendingMode).toHaveText("引导当前任务");
|
||||
const queue = page.getByRole("list", { name: "待处理消息" });
|
||||
const pendingRow = queue.getByRole("listitem").filter({ hasText: "补充当前任务" });
|
||||
const pendingMode = pendingRow.getByRole("button", { name: "消息操作:补充当前任务" });
|
||||
await expect(pendingRow).toContainText("引导");
|
||||
await expect(pendingRow.getByRole("button")).toHaveCount(1);
|
||||
await pendingMode.click();
|
||||
await page.getByRole("menuitemradio", { name: "排队处理" }).click();
|
||||
await expect(pendingMode).toHaveText("排队处理");
|
||||
await expect(pendingRow).toContainText("排队");
|
||||
await pendingMode.click();
|
||||
await expect(page.getByRole("menuitemradio", { name: "排队处理" })).toHaveAttribute("aria-checked", "true");
|
||||
const steerOption = page.getByRole("menuitemradio", { name: "引导当前任务" });
|
||||
await steerOption.hover();
|
||||
await expectThemeColor(steerOption, "background-color", "--action-selection-soft-hover");
|
||||
await page.screenshot({ path: testInfo.outputPath("pending-delivery-menu.png"), animations: "disabled" });
|
||||
await page.keyboard.press("Home");
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(pendingMode).toHaveText("引导当前任务");
|
||||
await steerOption.click();
|
||||
await expect(pendingRow).toContainText("引导");
|
||||
await expect(pendingMode).toBeFocused();
|
||||
await deliveryMode.click();
|
||||
await page.getByRole("menuitemradio", { name: "排队处理" }).click();
|
||||
await composer.fill("下一轮核对流量");
|
||||
await page.getByRole("button", { name: "将消息加入 Agent 队列" }).click();
|
||||
await expect(page.getByRole("button", { name: "消息发送方式:下一轮核对流量" })).toHaveText("排队处理");
|
||||
await expect(queue.getByRole("listitem").filter({ hasText: "下一轮核对流量" })).toContainText("排队");
|
||||
expect(admitted.map((item) => item.delivery)).toEqual(["steer", "queue"]);
|
||||
const longInput = "下一轮请检查所有压力测点,并说明每个异常的证据及时间范围。\n请逐一列出测点名称、压力变化、读数时刻和关联管段,保留缺失数据的说明,避免把未核实的推测写成结论。\n保留这段完整的补充内容,末尾核对流量。";
|
||||
await composer.fill(longInput);
|
||||
await page.getByRole("button", { name: "将消息加入 Agent 队列" }).click();
|
||||
const longRow = queue.getByRole("listitem").filter({ hasText: longInput });
|
||||
await expect(longRow).toBeVisible();
|
||||
await expect(longRow.locator("p")).toHaveText(longInput);
|
||||
const expandMessage = longRow.getByRole("button", { name: "展开全文" });
|
||||
await expect(expandMessage).toBeVisible();
|
||||
await expect(expandMessage).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(page.locator("form").getByRole("list", { name: "待处理消息" })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath("pending-messages-compact.png"), animations: "disabled" });
|
||||
await expandMessage.click();
|
||||
await expect(longRow.getByRole("button", { name: "收起全文" })).toHaveAttribute("aria-expanded", "true");
|
||||
expect(await longRow.locator("p").evaluate((node) => node.scrollWidth <= node.clientWidth)).toBe(true);
|
||||
expect(await longRow.locator("p").evaluate((node) => node.scrollHeight <= node.clientHeight)).toBe(true);
|
||||
await page.screenshot({ path: testInfo.outputPath("pending-message-content.png"), animations: "disabled" });
|
||||
await longRow.getByRole("button", { name: /^消息操作:/ }).focus();
|
||||
await page.keyboard.press("ArrowDown");
|
||||
holdWithdrawal = true;
|
||||
await page.getByRole("menuitem", { name: "编辑消息" }).click();
|
||||
await expect.poll(() => Boolean(heldWithdrawal)).toBe(true);
|
||||
await composer.fill("正在输入的新草稿");
|
||||
const withdrawnId = new URL(heldWithdrawal!.request().url()).pathname.split("/").at(-1)!;
|
||||
const withdrawn = pending.get(withdrawnId)!;
|
||||
pending.delete(withdrawnId);
|
||||
await heldWithdrawal!.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { ...withdrawn, status: "cancelled" } });
|
||||
heldWithdrawal = undefined; holdWithdrawal = false;
|
||||
await expect(composer).toHaveValue(`正在输入的新草稿\n\n${longInput}`);
|
||||
await expect(composer).toBeFocused();
|
||||
await expect(queue.getByRole("listitem")).toHaveCount(2);
|
||||
expect(admitted).toHaveLength(3);
|
||||
await composer.fill("编辑后重新发送的消息");
|
||||
await page.getByRole("button", { name: "将消息加入 Agent 队列" }).click();
|
||||
const editedRow = queue.getByRole("listitem").filter({ hasText: "编辑后重新发送的消息" });
|
||||
await editedRow.getByRole("button", { name: /^消息操作:/ }).click();
|
||||
await page.getByRole("menuitem", { name: "关闭排队" }).click();
|
||||
await expect(editedRow).toHaveCount(0);
|
||||
await expect(composer).toHaveValue("");
|
||||
// Consumption can win the race while the edit menu is open.
|
||||
await composer.fill("保留当前草稿");
|
||||
const consumedRow = queue.getByRole("listitem").filter({ hasText: "补充当前任务" });
|
||||
await consumedRow.getByRole("button", { name: /^消息操作:/ }).click();
|
||||
holdWithdrawal = true;
|
||||
await page.getByRole("menuitem", { name: "编辑消息" }).click();
|
||||
await expect.poll(() => Boolean(heldWithdrawal)).toBe(true);
|
||||
const consumed = pending.get(admitted[0].id)!;
|
||||
pending.delete(consumed.id);
|
||||
send({ type: "data-user_input", data: { ...consumed, session_id: "input-session", status: "delivered" } });
|
||||
await heldWithdrawal!.fulfill({ status: 409, headers: { "Access-Control-Allow-Origin": "*" }, json: { message: "消息已开始处理,无法退回编辑。" } });
|
||||
heldWithdrawal = undefined; holdWithdrawal = false;
|
||||
await expect(page.getByText("无法退回编辑", { exact: true })).toBeVisible();
|
||||
await expect(composer).toHaveValue("保留当前草稿");
|
||||
expect(primaryRequests).toBe(1);
|
||||
expect(abortRequests).toBe(0);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
@@ -124,7 +193,14 @@ for (const width of [1440, 375, 320]) {
|
||||
await expect(page.getByRole("button", { name: "停止 Agent 指令" })).toBeVisible();
|
||||
await composer.fill("等待处理的后续问题");
|
||||
await page.getByRole("button", { name: "将消息加入 Agent 队列" }).click();
|
||||
await expect(page.getByRole("button", { name: "消息发送方式:等待处理的后续问题" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "消息操作:等待处理的后续问题" })).toBeVisible();
|
||||
await page.setViewportSize({ width, height: 640 });
|
||||
await expect.poll(async () => {
|
||||
const stopBox = await page.getByRole("button", { name: "停止 Agent 指令" }).boundingBox();
|
||||
return stopBox!.y + stopBox!.height;
|
||||
}).toBeLessThanOrEqual(640);
|
||||
await page.screenshot({ path: testInfo.outputPath("pending-message-short-window.png"), animations: "disabled" });
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
await page.getByRole("button", { name: "停止 Agent 指令" }).click();
|
||||
await expect(page.getByRole("list", { name: "待处理消息" })).toHaveCount(0);
|
||||
expect(abortRequests).toBe(1);
|
||||
|
||||
Reference in New Issue
Block a user