Ctrl K
ComunidadGuías…

tests/e2e/control-sessions.spec.ts

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

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

const NOW = new Date().toISOString();

const SESSION_1_MESSAGES = [
  {
    id: "u1",
    role: "user",
    parts: [{ type: "text", text: "Investigate the auth bug" }],
  },
  {
    id: "a1",
    role: "assistant",
    parts: [{ type: "text", text: "Found it in session.ts." }],
  },
];

test("control sessions: history list, restore, persist, and new session", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockBaseChrome(page);
  await page.route("**/api/connections", (route) =>
    fulfillJson(route, { connections: [] })
  );
  await page.route("**/api/repos**", (route) =>
    fulfillJson(route, [
      {
        id: "repo-1",
        full_name: "acme/widgets",
        owner: "acme",
        name: "widgets",
        default_branch: "main",
      },
    ])
  );

  const putBodies: Array<{ id?: string; messages?: unknown[] }> = [];
  let posted = false;
  await page.route("**/api/control/sessions**", (route) => {
    const request = route.request();
    const url = new URL(request.url());
    const id = url.searchParams.get("id");

    if (request.method() === "GET" && id === "sess-1") {
      return fulfillJson(route, {
        id: "sess-1",
        title: "Earlier investigation",
        project: "acme/widgets",
        repo_id: "repo-1",
        pinned: false,
        archived: false,
        messages: SESSION_1_MESSAGES,
        created_at: NOW,
        updated_at: NOW,
      });
    }
    if (request.method() === "GET") {
      return fulfillJson(route, [
        {
          id: "sess-1",
          title: "Earlier investigation",
          project: "acme/widgets",
          repo_id: "repo-1",
          pinned: false,
          archived: false,
          created_at: NOW,
          updated_at: NOW,
        },
      ]);
    }
    if (request.method() === "POST") {
      posted = true;
      return fulfillJson(route, {
        id: "sess-2",
        title: "Fix billing",
        project: "acme/widgets",
        repo_id: "repo-1",
        pinned: false,
        archived: false,
        messages: [],
        created_at: NOW,
        updated_at: NOW,
      });
    }
    if (request.method() === "PUT") {
      const body = request.postDataJSON() as (typeof putBodies)[number] & {
        expected_updated_at?: string;
      };
      putBodies.push(body);
      return fulfillJson(route, {
        ok: true,
        session: {
          id: body.id,
          title: "Earlier investigation",
          pinned: false,
          archived: false,
          messages: body.messages ?? [],
          created_at: NOW,
          updated_at: new Date().toISOString(),
        },
      });
    }
    return route.fallback();
  });

  const streamChunks = [
    { type: "start" },
    { type: "text-start", id: "t1" },
    { type: "text-delta", id: "t1", delta: "Glad to help." },
    { type: "text-end", id: "t1" },
    { type: "finish" },
  ];
  const streamBody =
    streamChunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("") +
    "data: [DONE]\n\n";
  await page.route("**/api/control/chat", (route) =>
    route.fulfill({
      status: 200,
      headers: {
        "content-type": "text/event-stream",
        "x-vercel-ai-ui-message-stream": "v1",
      },
      body: streamBody,
    })
  );

  await page.goto(scopedPath("control"));
  await page.waitForLoadState("networkidle");

  // History: the sidebar lists the stored session.
  const sidebar = page.getByRole("complementary", { name: "Sessions" });
  await expect(
    sidebar.getByRole("button", { name: /^Earlier investigation/ })
  ).toBeVisible();

  // Restore: selecting it brings back the full conversation.
  await sidebar.getByRole("button", { name: /^Earlier investigation/ }).click();
  await expect(page).toHaveURL(/\/control\?mission=sess-1$/);
  const conversation = page.getByRole("log", { name: "Conversation" });
  await expect(
    conversation.getByText("Investigate the auth bug")
  ).toBeVisible();
  await expect(conversation.getByText("Found it in session.ts.")).toBeVisible();

  // Continue the restored session: the completed turn persists via PUT.
  await page
    .getByPlaceholder("Ask for follow-up changes or attach images")
    .fill("Thanks, ship it");
  await page.keyboard.press("Enter");
  await expect(conversation.getByText("Glad to help.")).toBeVisible();
  await expect
    .poll(() => putBodies.length, { timeout: 10_000 })
    .toBeGreaterThan(0);
  const persisted = putBodies.at(-1);
  expect(persisted?.id).toBe("sess-1");
  expect(
    JSON.stringify(persisted?.messages ?? []).includes("Thanks, ship it")
  ).toBe(true);

  // Multi-session: starting a new session creates a fresh row and clears the
  // chat; the old session stays in the list for switching back. Submit with
  // Enter — at narrow widths the options row wraps the button out of view.
  await sidebar.getByRole("button", { name: "New session" }).click();
  await page
    .getByPlaceholder("Ask anything or run a command...")
    .fill("Fix billing");
  await page.keyboard.press("Enter");
  await expect.poll(() => posted, { timeout: 10_000 }).toBe(true);
  await expect(page).toHaveURL(/\/control\?mission=sess-2$/);
  await expect(
    sidebar.getByRole("button", { name: /^Fix billing/ })
  ).toBeVisible();
  await expect(
    sidebar.getByRole("button", { name: /^Earlier investigation/ })
  ).toBeVisible();

  // Archiving clears both the selection and its now-dead deep link.
  await page.getByRole("button", { name: "More options" }).click();
  await page.getByRole("menuitem", { name: "Archive" }).click();
  await expect(page).toHaveURL(/\/control$/);
  await expect(
    sidebar.getByRole("button", { name: /^Fix billing/ })
  ).toHaveCount(0);
});