дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
#2118 made agent replies plain text on the panel and gave the prompt the 6px tab radius; assert that style instead of the earlier card.
151 рядки
5.9 KiB
TypeScript
151 рядки
5.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import type { AgentEntry, AgentTask } from "../src/bindings";
|
|
|
|
const task: AgentTask = {
|
|
id: "approval",
|
|
title: "Command approval",
|
|
status: "needsInput",
|
|
isolation: "local",
|
|
provider: "codex",
|
|
model: null,
|
|
cwd: "/dev/acme-api",
|
|
branch: null,
|
|
base: null,
|
|
baseCommit: null,
|
|
error: null,
|
|
createdMs: 0,
|
|
updatedMs: 0,
|
|
config: [],
|
|
commands: [],
|
|
usage: null,
|
|
queue: [],
|
|
};
|
|
const command = `python3 -c 'import pathlib\np = pathlib.Path("note.txt")\np.write_text("${"long content ".repeat(80)}")'`;
|
|
const entries: AgentEntry[] = [
|
|
{ type: "user", text: "Write a note with Python.", attachments: [], steered: false },
|
|
{ type: "thought", text: "I need approval to write." },
|
|
{
|
|
type: "tool",
|
|
id: "inspect",
|
|
title: "Read README.md",
|
|
kind: "read",
|
|
status: "completed",
|
|
locations: [],
|
|
output: null,
|
|
diffs: [],
|
|
},
|
|
{ type: "agent", text: "I will request approval before writing the note." },
|
|
{
|
|
type: "tool",
|
|
id: "read",
|
|
title: "List .",
|
|
kind: "search",
|
|
status: "running",
|
|
locations: [],
|
|
output: null,
|
|
diffs: [],
|
|
},
|
|
{
|
|
type: "permission",
|
|
id: 1,
|
|
title: `Run ${command}`,
|
|
options: [
|
|
{ id: "once", kind: "allow_once", name: "Allow once" },
|
|
{ id: "always", kind: "allow_always", name: `Always allow \`${command}\`` },
|
|
{ id: "decline", kind: "reject_once", name: "Decline" },
|
|
{ id: "cancel", kind: "reject_once", name: "Decline and stop" },
|
|
],
|
|
chosen: null,
|
|
},
|
|
];
|
|
|
|
for (const theme of ["dark", "light"] as const) {
|
|
for (const width of [760, 1400]) {
|
|
test(`approval stays inside the agent column at ${width}px in ${theme}`, async ({ page }) => {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await page.goto("/");
|
|
await page.getByRole("tree", { name: "Project" }).waitFor();
|
|
await page.evaluate(
|
|
async ({ task, entries, theme }) => {
|
|
const path = "/src/features/agents/store.ts";
|
|
const store = await import(path);
|
|
const themePath = "/src/workbench/theme.ts";
|
|
(await import(themePath)).previewTheme(theme);
|
|
store.useAgents.setState({
|
|
loaded: true,
|
|
tasks: new Map([[task.id, task]]),
|
|
entries: new Map([[task.id, entries]]),
|
|
});
|
|
store.openTaskInEditorLocation(task.id);
|
|
},
|
|
{ task, entries, theme },
|
|
);
|
|
const card = page.getByRole("region", { name: "codex wants to run a command" });
|
|
await expect(card).toHaveCount(1);
|
|
await expect(card.locator("pre")).toHaveText(command);
|
|
await expect(card.getByRole("button")).toHaveText(["Allow Once", "Always Allow", "Decline", "Decline and Stop"]);
|
|
const geometry = await card.evaluate((el) => {
|
|
const transcript = el.parentElement!.parentElement!;
|
|
const prompt = transcript.querySelector('[class*="_user_"]')!;
|
|
const reply = transcript.querySelector('[class*="_agentCard_"]')!;
|
|
const rect = el.getBoundingClientRect();
|
|
const bounds = transcript.getBoundingClientRect();
|
|
return {
|
|
scroll: transcript.scrollWidth,
|
|
client: transcript.clientWidth,
|
|
contained: rect.left >= bounds.left && rect.right <= bounds.right,
|
|
promptRadius: parseFloat(getComputedStyle(prompt).borderRadius),
|
|
replyBackground: getComputedStyle(reply).backgroundColor,
|
|
replyBorder: getComputedStyle(reply).borderTopWidth,
|
|
userOnRight: prompt.getBoundingClientRect().right > reply.getBoundingClientRect().right,
|
|
approvalAligned: Math.abs(rect.left - reply.getBoundingClientRect().left) < 1,
|
|
authorBeforeWork:
|
|
transcript.querySelector('[class*="_agentAuthor_"]')?.nextElementSibling?.matches("details") ?? false,
|
|
footerAligned:
|
|
Math.abs(prompt.getBoundingClientRect().right - prompt.nextElementSibling!.getBoundingClientRect().right) <
|
|
1,
|
|
cardWidth: rect.width,
|
|
available: bounds.width,
|
|
actionsVisible: getComputedStyle(reply.nextElementSibling!.querySelector('[class*="_messageActions_"]')!)
|
|
.opacity,
|
|
};
|
|
});
|
|
expect(geometry.scroll).toBe(geometry.client);
|
|
expect(geometry.contained).toBe(true);
|
|
expect(geometry.promptRadius).toBeLessThanOrEqual(6);
|
|
// Replies read as plain text on the panel.
|
|
expect(geometry.replyBackground).toBe("rgba(0, 0, 0, 0)");
|
|
expect(geometry.replyBorder).toBe("0px");
|
|
expect(geometry.userOnRight).toBe(true);
|
|
expect(geometry.approvalAligned).toBe(true);
|
|
expect(geometry.authorBeforeWork).toBe(true);
|
|
expect(geometry.footerAligned).toBe(true);
|
|
// The last message stays actionable even while an approval follows it.
|
|
expect(geometry.actionsVisible).toBe("1");
|
|
if (width === 760) expect(geometry.cardWidth).toBeGreaterThan(geometry.available * 0.8);
|
|
await card.locator("pre").focus();
|
|
for (const label of ["Allow Once", "Always Allow", "Decline", "Decline and Stop"]) {
|
|
await page.keyboard.press("Tab");
|
|
await expect(card.getByRole("button", { name: label, exact: true })).toBeFocused();
|
|
}
|
|
await page.evaluate(async () => {
|
|
const path = "/src/platform/ipc.ts";
|
|
const { commands } = await import(path);
|
|
commands.agentRespond = async (task: string, permission: number, option: string) => {
|
|
(window as unknown as { approvalAnswer: unknown }).approvalAnswer = { task, permission, option };
|
|
return { status: "ok", data: null };
|
|
};
|
|
});
|
|
await card.getByRole("button", { name: "Allow Once", exact: true }).focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect
|
|
.poll(() => page.evaluate(() => (window as unknown as { approvalAnswer: unknown }).approvalAnswer))
|
|
.toEqual({
|
|
task: task.id,
|
|
permission: 1,
|
|
option: "once",
|
|
});
|
|
});
|
|
}
|
|
}
|