feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理 - 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类) - 技能: md-to-office (pandoc + wkhtmltopdf) - 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/ - 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
113
surfaces/gui/e2e/unattended.spec.ts
Normal file
113
surfaces/gui/e2e/unattended.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// Unattended mode (item 8) — the "Send approvals to Inbox" toggle and its effect on approvals.
|
||||
// Since §22 the toggle lives at the BOTTOM of the composer's Mode menu (who approves, and when —
|
||||
// one mental model; the standalone InboxControl left the row). When a session is unattended, an
|
||||
// approval PARKS to the Inbox instead of surfacing an inline card (the app suppresses the live
|
||||
// card; the Inbox list itself is covered by inbox.spec.ts). The mocked /v1/sessions/:id/unattended
|
||||
// is stateful so the toggle persists across a reload.
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./fixtures";
|
||||
|
||||
// The toggle sits inside the composer's Mode menu (§22).
|
||||
async function openModeMenu(page) {
|
||||
await page.getByRole("button", { name: "Mode", exact: true }).click();
|
||||
await expect(page.getByTestId("mode-menu")).toBeVisible();
|
||||
}
|
||||
|
||||
test("attended (default): a tool request surfaces the inline approval card", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const box = page.getByPlaceholder(/Ask the coworker/);
|
||||
await box.fill("please run a tool");
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
await expect(page.getByText("The coworker wants to run a command.").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("Send-to-Inbox toggle (in the Mode menu) flips and persists across a reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await openModeMenu(page);
|
||||
const sw = page.getByRole("switch", { name: "Send approvals to the Inbox" });
|
||||
await expect(sw).toHaveAttribute("aria-checked", "false");
|
||||
await sw.click();
|
||||
await expect(sw).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
// Reload: the stateful endpoint returns the saved flag, so the toggle reads back on.
|
||||
await page.reload();
|
||||
await openModeMenu(page);
|
||||
await expect(page.getByRole("switch", { name: "Send approvals to the Inbox" })).toHaveAttribute(
|
||||
"aria-checked",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
test("unattended: a tool request parks (no inline approval card)", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openModeMenu(page);
|
||||
await page.getByRole("switch", { name: "Send approvals to the Inbox" }).click();
|
||||
// The menu's full-screen overlay closes it on any outside click.
|
||||
await page.mouse.click(5, 5);
|
||||
|
||||
const box = page.getByPlaceholder(/Ask the coworker/);
|
||||
await box.fill("please run a tool");
|
||||
await page.getByRole("button", { name: "Send", exact: true }).click();
|
||||
|
||||
// The turn still starts, but the live approval card is suppressed — the prompt is parked to the
|
||||
// Inbox instead. Give the (suppressed) card a beat to NOT appear.
|
||||
await expect(page.getByText("Echo:").first()).toBeVisible().catch(() => {});
|
||||
await expect(page.getByText("The coworker wants to run a command.")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("answering the live approval never re-flashes its parked Inbox mirror", async ({ page }) => {
|
||||
// Every live approval is ALSO parked as a per-session Inbox item (reconnect/remote resolution).
|
||||
// Tester catch 2026-07-12: after "Allow once", the polled sessionInbox copy was still pending
|
||||
// for up to a poll cycle, so the docked answer-in-context card flashed the SAME request again.
|
||||
// Simulate the mirror: any per-session inbox fetch for the live session returns one pending
|
||||
// approval until the decision lands (the fixtures' fixed items belong to other sessions).
|
||||
// The real server resolves the mirror synchronously with the decision — only the CLIENT's
|
||||
// polled copy is stale, which is exactly what this test pins.
|
||||
let mirrorResolved = false;
|
||||
await page.route(/\/v1\/inbox\?/, async (route) => {
|
||||
const q = new URL(route.request().url()).searchParams;
|
||||
const sid = q.get("session_id");
|
||||
if (!sid || sid === "wp-3" || sid === "ops-1") return route.fallback();
|
||||
return route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: mirrorResolved
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "mirror-1",
|
||||
session_id: sid,
|
||||
kind: "approval",
|
||||
title: "Run `run_shell`?",
|
||||
body: "requires approval",
|
||||
state: "pending",
|
||||
resolution: null,
|
||||
inbox: "default",
|
||||
created_at: "2026-07-12 10:00:00",
|
||||
resolved_at: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
const box = page.getByPlaceholder(/Ask the coworker/);
|
||||
await box.fill("please run a tool");
|
||||
await page.getByRole("button", { name: "Send", exact: true }).click();
|
||||
await expect(page.getByText("The coworker wants to run a command.").first()).toBeVisible();
|
||||
|
||||
mirrorResolved = true; // server side resolves with the decision; the stale client copy is the bug
|
||||
await page.getByRole("button", { name: "Allow once" }).last().click();
|
||||
// "Never appears" semantics: pre-fix the stale mirror rendered within a frame of the click and
|
||||
// self-cleared a poll later — so a plain toHaveCount(0) would blink green. Watch the window.
|
||||
const flashed = await page
|
||||
.getByText("Run `run_shell`?")
|
||||
.waitFor({ state: "visible", timeout: 700 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(flashed).toBe(false);
|
||||
await expect(page.getByText("The command ran; 1 file found.")).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user