tests/e2e/control-open-pr-approval.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import { convertToModelMessages, streamText, tool, type UIMessage } from "ai";
import { MockLanguageModelV3 } from "ai/test";
import { z } from "zod";
import { wrapWithPolicy } from "../../lib/agents/orchestrator/policy";
import { enableScopedE2EAuth, scopedPath } from "./helpers/auth";
import {
fulfillJson,
mockBaseChrome,
mockControlSessionBootstrap,
} from "./helpers/automation-control-plane-fixtures";
for (const decision of ["Approve", "Deny"] as const) {
test(`opening a PR waits for operator approval after reload: ${decision}`, async ({
page,
}) => {
await enableScopedE2EAuth(page);
await page.setViewportSize({
width: decision === "Approve" ? 1440 : 390,
height: 900,
});
await mockBaseChrome(page);
await mockControlSessionBootstrap(page);
await page.route("**/api/control/approvals**", (route) =>
fulfillJson(route, { approvals: [] })
);
let createdPrs = 0;
const approvals: string[] = [];
const resolved: string[] = [];
const openPr = wrapWithPolicy(
"open_pr",
tool({
inputSchema: z.object({ title: z.string() }),
execute: async () => {
createdPrs += 1;
return { url: "https://github.com/acme/widgets/pull/42" };
},
}),
{ userId: "user-1", repoBranch: "feat/work", controlMode: "run" },
{
createApproval: async (input) => {
approvals.push(input.toolCallId);
return { id: "approval-pr" };
},
resolveApprovalByToolCall: async (input) => {
resolved.push(input.toolCallId);
},
}
);
let turns = 0;
let conversationId: string | undefined;
await page.route("**/api/control/chat", async (route) => {
const body = route.request().postDataJSON() as {
messages: UIMessage[];
conversationId?: string;
};
const { messages } = body;
// Match the durable route's required session identity. A messages-only
// mock previously hid the production approval continuation regression.
if (
!body.conversationId ||
(conversationId && body.conversationId !== conversationId)
)
return fulfillJson(
route,
{ error: "Missing or changed conversation" },
400
);
conversationId = body.conversationId;
const firstTurn = turns++ === 0;
const result = streamText({
stopWhen: [],
model: new MockLanguageModelV3({
doStream: async () => ({
stream: new ReadableStream({
start(controller) {
const callsTool = firstTurn;
if (callsTool) {
controller.enqueue({
type: "tool-call",
toolCallId: "open-pr-call",
toolName: "open_pr",
input: JSON.stringify({ title: "Fix regression" }),
});
} else {
controller.enqueue({ type: "text-start", id: "answer" });
controller.enqueue({
type: "text-delta",
id: "answer",
delta: createdPrs
? "Pull request created."
: "Pull request was not created.",
});
controller.enqueue({ type: "text-end", id: "answer" });
}
controller.enqueue({
type: "finish",
finishReason: {
unified: callsTool ? "tool-calls" : "stop",
raw: undefined,
},
usage: {
inputTokens: {
total: 1,
noCache: 1,
cacheRead: 0,
cacheWrite: 0,
},
outputTokens: { total: 1, text: 1, reasoning: 0 },
},
});
controller.close();
},
}),
}),
}),
tools: { open_pr: openPr },
messages: await convertToModelMessages(messages),
});
const response = result.toUIMessageStreamResponse({
originalMessages: messages,
});
await route.fulfill({
contentType: "text/event-stream",
headers: { "x-vercel-ai-ui-message-stream": "v1" },
body: await response.text(),
});
});
await page.goto(scopedPath("control"));
await page
.getByPlaceholder("Ask anything or run a command...")
.fill("Open a PR for this change");
const request = page.waitForResponse("**/api/control/chat");
const saved = page.waitForResponse(
(response) =>
response.url().includes("/api/control/sessions") &&
response.request().method() === "PUT" &&
response.request().postData()?.includes("approval-requested") === true
);
await page.getByRole("button", { name: "Start mission" }).click();
expect((await request).ok()).toBe(true);
await expect(
page.getByRole("button", { name: "Approve", exact: true })
).toBeVisible();
expect(createdPrs).toBe(0);
expect(approvals).toEqual(["open-pr-call"]);
await saved;
await page.reload();
await expect(
page.getByRole("button", { name: "Approve", exact: true })
).toBeVisible();
const continuation = page.waitForResponse("**/api/control/chat");
await page.getByRole("button", { name: decision, exact: true }).click();
expect((await continuation).ok()).toBe(true);
await expect(
page.getByText(
decision === "Approve"
? "Pull request created."
: "Pull request was not created.",
{ exact: true }
)
).toBeVisible();
expect(createdPrs).toBe(decision === "Approve" ? 1 : 0);
expect(resolved).toEqual(decision === "Approve" ? ["open-pr-call"] : []);
});
}