Ctrl K
ComunidadGuías…

tests/e2e/control-turn-progress.spec.ts

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

Archivo original ↗Preguntar a KiBot
import { expect, test } from "@playwright/test";
import type { ControlWorker } from "@/lib/control/workers";
import { enableScopedE2EAuth, scopedPath } from "./helpers/auth";
import {
  fulfillJson,
  mockBaseChrome,
  mockControlSessionBootstrap,
} from "./helpers/automation-control-plane-fixtures";

function currentWorker(
  id: string,
  status: ControlWorker["status"],
  output: string
): ControlWorker {
  return {
    id,
    worktreeId: id,
    branch: `mogplex/task/current/${id}`,
    status,
    error: status === "failed" ? "Worker stopped before finishing." : null,
    updatedAt: "2026-08-14T00:00:01Z",
    events: [
      {
        id: `${id}-start`,
        type: "tool_started",
        toolName: "Command",
        message: "Started",
        payload: { toolCallId: id, input: { command: `check ${id}` } },
        createdAt: "2026-08-14T00:00:00Z",
      },
      {
        id: `${id}-end`,
        type: "tool_finished",
        toolName: "Command",
        message: "Finished",
        payload: {
          toolCallId: id,
          state: status === "failed" ? "error" : "done",
          output,
        },
        createdAt: "2026-08-14T00:00:01Z",
      },
    ],
  };
}

test("a follow-up preserves one continuous conversation with live command progress and stop", async ({
  page,
}, testInfo) => {
  const browserErrors: string[] = [];
  page.on("pageerror", (error) => browserErrors.push(error.message));
  await enableScopedE2EAuth(page);
  await mockBaseChrome(page);
  await mockControlSessionBootstrap(page);
  let currentWorkers: ControlWorker[] = [];
  await page.route("**/api/control/workers?*", (route) =>
    fulfillJson(route, {
      workers: [
        ...currentWorkers,
        ...["ci", "docs", "accessibility", "links", "cleanup"].map((task) => ({
          id: `old-worker-${task}`,
          worktreeId: task,
          branch: `mogplex/task/old-request/${task}`,
          status: "failed",
          error:
            "Worker could not authenticate. Check its AI connection before retrying.",
          updatedAt: "2026-08-13T00:00:00Z",
          events: [
            {
              id: `${task}-start`,
              type: "tool_started",
              toolName: "Command",
              message: "Started",
              payload: { toolCallId: task, input: { command: "old command" } },
              createdAt: "2026-08-13T00:00:00Z",
            },
            {
              id: `${task}-end`,
              type: "tool_finished",
              toolName: "Command",
              message: "Failed",
              payload: {
                toolCallId: task,
                state: "error",
                output: "Previous batch output",
              },
              createdAt: "2026-08-13T00:00:01Z",
            },
          ],
        })),
      ],
    })
  );
  await page.route("**/api/connections", (route) =>
    fulfillJson(route, { connections: [] })
  );
  await page.addInitScript(() => {
    class FixtureEventSource extends EventTarget {
      private listener: (event: Event) => void;
      constructor(url: string) {
        super();
        this.listener = (event: Event) => {
          const detail = (event as CustomEvent<{ table: string }>).detail;
          if (decodeURIComponent(url).includes(detail.table))
            this.dispatchEvent(
              new MessageEvent("message", { data: JSON.stringify(detail) })
            );
        };
        window.addEventListener("fixture-table-event", this.listener);
        queueMicrotask(() => this.dispatchEvent(new Event("open")));
      }
      close() {
        window.removeEventListener("fixture-table-event", this.listener);
      }
    }
    Object.defineProperty(window, "EventSource", { value: FixtureEventSource });
    const original = window.fetch.bind(window);
    let count = 0;
    window.fetch = async (input, init) => {
      if (!String(input).endsWith("/api/control/chat"))
        return original(input, init);
      count++;
      const encoder = new TextEncoder();
      return new Response(
        new ReadableStream({
          start(controller) {
            const send = (chunk: object) =>
              controller.enqueue(
                encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)
              );
            send({ type: "start", messageId: `answer-${count}` });
            if (count === 1) {
              send({ type: "text-start", id: "t" });
              send({
                type: "text-delta",
                id: "t",
                delta: "Earlier analysis. ".repeat(100),
              });
              send({ type: "text-end", id: "t" });
              send({
                type: "tool-input-available",
                toolCallId: "earlier-read",
                toolName: "read_file",
                input: { path: "app/page.tsx" },
              });
              send({
                type: "tool-output-available",
                toolCallId: "earlier-read",
                output: { content: "Earlier source content" },
              });
              send({ type: "finish" });
              controller.enqueue(encoder.encode("data: [DONE]\n\n"));
              controller.close();
            } else {
              send({
                type: "tool-input-available",
                toolCallId: "cmd",
                toolName: "run_command",
                input: { command: "pnpm test" },
              });
              init?.signal?.addEventListener(
                "abort",
                () =>
                  controller.error(new DOMException("Aborted", "AbortError")),
                { once: true }
              );
            }
          },
        }),
        {
          headers: {
            "content-type": "text/event-stream",
            "x-vercel-ai-ui-message-stream": "v1",
          },
        }
      );
    };
  });
  await page.goto(scopedPath("control"));
  await page
    .getByPlaceholder("Ask anything or run a command...")
    .fill("Review this repository");
  await page.getByRole("button", { name: "Start mission" }).click();
  const composer = page.getByPlaceholder(
    "Ask for follow-up changes or attach images"
  );
  await expect(
    page.getByRole("button", { name: "Send", exact: true })
  ).toBeVisible();
  await page.getByRole("button", { name: /^Agent commands/ }).click();
  await expect(page.getByTestId("control-terminal-activity")).toContainText(
    "Previous batch output"
  );
  await page.getByRole("button", { name: "Close terminal" }).click();
  const earlierTool = page
    .getByTestId("tool-activity")
    .filter({ hasText: "Reading file" });
  await earlierTool.locator("summary").click();
  await expect(earlierTool.locator("details")).toHaveAttribute("open", "");
  await composer.fill("Fix the failing tests");
  await page.getByRole("button", { name: "Send", exact: true }).click();
  const progress = page.getByRole("status", { name: "Current activity" });
  await expect(progress).toContainText("Running command");
  await expect(progress).toContainText("pnpm test");
  const workers = page.getByRole("region", { name: "Mission workers" });
  await expect(workers).toContainText("5 workers failed");
  await expect(workers.getByRole("link", { name: /View work/ })).toHaveCount(0);
  await expect(page.getByTestId("control-terminal-activity")).not.toContainText(
    "Previous batch output"
  );
  await expect(
    page.getByRole("region", { name: "Latest request" })
  ).toContainText("Fix the failing tests");
  await expect(page.getByText(/Earlier analysis/)).toHaveCount(1);
  await expect(earlierTool.locator("details")).toHaveAttribute("open", "");
  await expect(
    page.getByRole("button", { name: /Earlier conversation/i })
  ).toHaveCount(0);
  await expect(page.getByRole("log", { name: "Conversation" })).toHaveCount(1);
  await expect(
    page.getByRole("button", { name: "Stop", exact: true })
  ).toHaveText("Stop");
  await page.screenshot({ path: testInfo.outputPath("follow-up-desktop.png") });
  await page.setViewportSize({ width: 390, height: 844 });
  const navigation = await page
    .getByRole("button", { name: "Open sessions", exact: true })
    .boundingBox();
  const title = await page
    .getByRole("banner")
    .getByText("Review this repository", { exact: true })
    .boundingBox();
  expect(navigation).not.toBeNull();
  expect(title).not.toBeNull();
  // Navigation shares the breadcrumb row rather than consuming another phone row.
  expect(
    Math.abs(
      navigation!.y + navigation!.height / 2 - title!.y - title!.height / 2
    )
  ).toBeLessThan(2);
  await expect(progress).toBeInViewport();
  await expect(
    page
      .getByRole("region", { name: "Latest request" })
      .getByText("Fix the failing tests")
  ).toBeInViewport();
  await expect(
    page.getByRole("button", { name: "Stop", exact: true })
  ).toBeInViewport();
  await page.screenshot({ path: testInfo.outputPath("follow-up-mobile.png") });
  currentWorkers = [
    currentWorker("tests", "streaming", "Current tests progress"),
    currentWorker("lint", "streaming", "Current lint progress"),
  ];
  const refreshWorkers = () =>
    page.evaluate(() =>
      window.dispatchEvent(
        new CustomEvent("fixture-table-event", {
          detail: { table: "external_agent_runs", op: "UPDATE" },
        })
      )
    );
  await refreshWorkers();
  await page
    .getByRole("button", { name: "Open terminal", exact: true })
    .click();
  await page.getByRole("tab", { name: "Agent commands", exact: true }).click();
  const terminal = page.getByTestId("control-terminal-activity");
  await expect(terminal).toContainText("Current tests progress");
  await expect(terminal).toContainText("Current lint progress");
  currentWorkers = [
    currentWorker("tests", "success", "Current tests passed"),
    currentWorker("lint", "failed", "Current lint failed"),
  ];
  await refreshWorkers();
  await expect(terminal).toContainText("Current tests passed");
  await expect(terminal).toContainText("Current lint failed");
  await expect(terminal).not.toContainText("Previous batch output");
  await page.getByRole("button", { name: "Close terminal" }).click();
  await expect(workers).toContainText("6 workers failed");
  await expect(
    page.getByRole("button", { name: "Stop", exact: true })
  ).toBeVisible();
  await page.getByRole("button", { name: "Stop", exact: true }).click();
  await expect(progress).toHaveCount(0);
  await expect(
    page
      .getByRole("region", { name: "Latest request" })
      .getByTestId("tool-activity")
  ).toContainText("Status unconfirmed");
  // Earlier turns remain part of this transcript on a phone, with no panel to open.
  await page.getByText(/Earlier analysis/).scrollIntoViewIfNeeded();
  await expect(page.getByText(/Earlier analysis/)).toBeVisible();
  await page.screenshot({
    path: testInfo.outputPath("conversation-history-mobile.png"),
  });
  await composer.fill("Also check the build");
  await page.getByRole("button", { name: "Send", exact: true }).click();
  await expect(
    page.getByRole("region", { name: "Latest request" })
  ).toContainText("Also check the build");
  await expect(
    page.getByText("Fix the failing tests", { exact: true })
  ).toHaveCount(1);
  await expect(page.getByText(/Earlier analysis/)).toHaveCount(1);
  await expect(earlierTool.locator("details")).toHaveAttribute("open", "");
  await expect(page.getByRole("log", { name: "Conversation" })).toHaveCount(1);
  await page.getByRole("button", { name: "Stop", exact: true }).click();
  expect(browserErrors).toEqual([]);
});