Ctrl K
ComunidadGuías…

tests/e2e/control-open-pr-approval.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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"] : []);
  });
}