mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-10 22:20:27 +00:00
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
114 lines
5.1 KiB
TypeScript
114 lines
5.1 KiB
TypeScript
// 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();
|
|
});
|