Ctrl K
ComunidadGuías…

tests/e2e/control-slack-run.spec.ts

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

Archivo original ↗Preguntar a KiBot
import { expect, test } from "@playwright/test";
import { setupControlSidebar } from "./helpers/control-sidebar-fixture";
import type {
  RunWorkspaceContext,
  RunWorkspaceEvent,
} from "@/lib/run-workspace/types";

for (const withGuidance of [false, true]) {
  test(`Slack conversation ${withGuidance ? "with recorded guidance" : "without guidance"} opens its recorded run in Control and survives reload without launching work`, async ({
    page,
  }, testInfo) => {
    await page.setViewportSize({ width: 1600, height: 1000 });
    const { sessions, sidebar } = await setupControlSidebar(page);
    const runId = "00000000-0000-4000-8000-000000000901";
    sessions[0].external_run_id = runId;
    sessions[0].title = "Mobile hero from Slack";
    sessions[0].updated_at = "2026-09-17T16:00:00.000Z";
    const context: RunWorkspaceContext = {
      runId,
      aiCallId: "call-1",
      prompt: "Fix the mobile hero",
      status: "failed",
      sandboxRecordId: "sandbox-1",
      workingBranch: "fix/hero",
      canGuide: false,
      guidance: withGuidance
        ? [
            { id: "g1", body: "Keep the desktop layout", status: "delivered" },
            { id: "g2", body: "Check the mobile CTA", status: "not_applied" },
          ]
        : [],
      repo: {
        id: "repo-1",
        user_id: "user-1",
        full_name: "acme/widgets",
        created_at: "2026-09-05T00:00:00Z",
        default_branch: "main",
        root_directory: null,
      },
    };
    await page.route("**/api/runs/*/workspace", (route) =>
      route.fulfill({ json: context })
    );
    const events: RunWorkspaceEvent[] = [
      {
        id: "e1",
        type: "log",
        message: "The mobile hero needs a smaller gutter.",
        payload: { kind: "assistant_delta" },
        toolName: null,
        createdAt: "2026-09-05T00:00:00Z",
      },
      {
        id: "e2",
        type: "tool_started",
        message: null,
        payload: { toolCallId: "read-1", input: { path: "app/page.tsx" } },
        toolName: "read_file",
        createdAt: "2026-09-05T00:00:01Z",
      },
      {
        id: "e3",
        type: "tool_finished",
        message: null,
        payload: {
          toolCallId: "read-1",
          state: "done",
          output: "Hero source loaded",
        },
        toolName: "read_file",
        createdAt: "2026-09-05T00:00:02Z",
      },
    ];
    await page.route("**/api/runs/*/stream?*", (route) =>
      route.fulfill({
        contentType: "text/event-stream",
        body: `event: run\ndata: ${JSON.stringify(context)}\n\n${events.map((event) => `event: log\ndata: ${JSON.stringify(event)}\n\n`).join("")}event: replay_complete\ndata: {}\n\n`,
      })
    );
    const mutations: string[] = [];
    page.on("request", (request) => {
      if (
        request.method() === "POST" &&
        /\/api\/(?:control\/chat|sandbox(?:\/|$)|v1\/mogplex\/runs)/.test(
          new URL(request.url()).pathname
        )
      )
        mutations.push(request.url());
    });
    await page.reload();
    const conversation = page.getByTestId("control-external-run");
    await expect(
      conversation.getByRole("heading", { name: "Mobile hero from Slack" })
    ).toBeVisible();
    await expect(conversation).toContainText(
      "The mobile hero needs a smaller gutter."
    );
    const transcript = conversation.getByRole("log", { name: "Conversation" });
    await expect(transcript).toBeVisible();
    await expect(transcript).toContainText("Fix the mobile hero");
    await expect(
      conversation.getByRole("button", { name: /Earlier conversation/ })
    ).toHaveCount(0);
    if (withGuidance) {
      await expect(transcript).toContainText("Keep the desktop layout");
      await expect(transcript).toContainText("Delivered to agent");
      await expect(transcript).toContainText("Check the mobile CTA");
      await expect(transcript).toContainText(
        "Not applied before the run stopped"
      );
    }
    await expect(
      transcript.getByText("YOU", { exact: true }).first()
    ).toBeVisible();
    await expect(
      transcript.getByText("MOGPLEX", { exact: true })
    ).toBeVisible();
    const activity = transcript.getByTestId("tool-activity");
    await expect(activity).toContainText("Reading file");
    await expect(activity).toContainText("Complete");
    await activity.locator("summary").click();
    await expect(activity.locator("pre")).toHaveText("app/page.tsx");
    const response = transcript.getByText(
      "The mobile hero needs a smaller gutter.",
      { exact: true }
    );
    await expect(response).toHaveCSS("font-size", "14px");
    const transcriptContent = transcript.locator(":scope > div").first();
    expect((await transcriptContent.boundingBox())?.width).toBeLessThanOrEqual(
      1024
    );
    await conversation.screenshot({
      path: testInfo.outputPath("control-external-chat.png"),
    });
    await expect(
      conversation.getByText("Run failed", { exact: true })
    ).toBeVisible();
    await expect(
      conversation.getByRole("button", { name: "Continue in this chat" })
    ).toBeEnabled();
    await sidebar.getByRole("button", { name: /^Fix 6 / }).click();
    await expect(conversation).toHaveCount(0);
    await sidebar
      .getByRole("button", { name: /^Mobile hero from Slack / })
      .click();
    await expect(conversation).toContainText(
      "The mobile hero needs a smaller gutter."
    );
    await page.reload();
    await expect(conversation).toContainText(
      "The mobile hero needs a smaller gutter."
    );
    await expect(transcript).toContainText("Fix the mobile hero");
    await expect(transcript.getByTestId("tool-activity")).toContainText(
      "Reading file"
    );
    if (withGuidance)
      await expect(transcript).toContainText("Check the mobile CTA");
    expect(mutations).toEqual([]);
  });
}

test("an accepted Slack session appears in an already open Control sidebar", async ({
  page,
}) => {
  const { sessions, sidebar } = await setupControlSidebar(page);
  let publish!: () => void;
  // Deliver the insert only after the first list has rendered. A reload or
  // selected-conversation refresh cannot satisfy this sidebar assertion.
  const accepted = new Promise<void>((resolve) => {
    publish = resolve;
  });
  await page.route("**/api/realtime/events?*", async (route) => {
    if (
      new URL(route.request().url()).searchParams.get("tables") !==
      "control_sessions"
    )
      return route.fulfill({ status: 204 });
    await accepted;
    await route.fulfill({
      contentType: "text/event-stream",
      body: 'data: {"table":"control_sessions","op":"INSERT"}\n\n',
    });
  });
  try {
    await page.reload();
    await expect(
      sidebar.getByRole("button", { name: /^Fix 6 / })
    ).toBeVisible();
    sessions.push({
      ...sessions[0],
      id: "slack-new",
      title: "New Slack request",
      external_run_id: "00000000-0000-4000-8000-000000000902",
      updated_at: "2026-09-17T16:00:00.000Z",
    });
    publish();
    await expect(
      sidebar.getByRole("button", { name: /^New Slack request / })
    ).toBeVisible();
  } finally {
    publish();
  }
});

test("sending live guidance keeps the prompt and agent response visible", async ({
  page,
}) => {
  const { sessions } = await setupControlSidebar(page);
  const runId = "00000000-0000-4000-8000-000000000903";
  sessions[0].external_run_id = runId;
  const context: RunWorkspaceContext = {
    runId,
    aiCallId: "call-live",
    prompt: "Fix the mobile hero",
    status: "streaming",
    sandboxRecordId: "sandbox-1",
    workingBranch: "fix/hero",
    canGuide: true,
    guidance: [],
    repo: {
      id: "repo-1",
      user_id: "user-1",
      full_name: "acme/widgets",
      created_at: "2026-09-05T00:00:00Z",
    },
  };
  await page.route("**/api/runs/*/workspace", (route) =>
    route.fulfill({ json: context })
  );
  const event: RunWorkspaceEvent = {
    id: "live-1",
    type: "log",
    message: "I am checking the mobile hero.",
    payload: { kind: "assistant_delta" },
    toolName: null,
    createdAt: "2026-09-05T00:00:00Z",
  };
  await page.route("**/api/runs/*/stream?*", (route) =>
    route.fulfill({
      contentType: "text/event-stream",
      body: `event: run\ndata: ${JSON.stringify(context)}\n\nevent: log\ndata: ${JSON.stringify(event)}\n\nevent: replay_complete\ndata: {}\n\n`,
    })
  );
  await page.route("**/api/runs/*/guidance", async (route) => {
    const submission = route.request().postDataJSON() as {
      id: string;
      text: string;
    };
    context.guidance = [
      { id: submission.id, body: submission.text, status: "received" },
    ];
    await route.fulfill({ json: { status: "received" } });
  });
  await page.reload();
  const conversation = page.getByTestId("control-external-run");
  const transcript = conversation.getByRole("log", { name: "Conversation" });
  await expect(transcript).toContainText("I am checking the mobile hero.");
  await conversation
    .getByRole("textbox", { name: "Guide this run" })
    .fill("Keep the CTA visible");
  await conversation
    .getByRole("button", { name: "Send guidance", exact: true })
    .click();
  await expect(transcript).toContainText("Keep the CTA visible");
  await expect(transcript).toContainText("Saved for the next agent step");
  await expect(transcript).toContainText("Fix the mobile hero");
  await expect(transcript).toContainText("I am checking the mobile hero.");
  await expect(
    conversation.getByRole("button", { name: /Earlier conversation/ })
  ).toHaveCount(0);
});