Ctrl K
ComunidadGuías…

tests/e2e/control-new-project.spec.ts

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

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

test("project search offers creation with the typed name and hides it for an exact match", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockBaseChrome(page);
  await page.route("**/api/connections", (route) =>
    fulfillJson(route, { connections: [] })
  );
  await page.route("**/api/control/sessions**", (route) =>
    fulfillJson(route, [])
  );
  const repo: Repo = {
    id: "repo-1",
    user_id: "user-1",
    full_name: "acme/widgets",
    owner: "acme",
    name: "",
    default_branch: "main",
    created_at: "2026-09-01T00:00:00Z",
  };
  await page.route("**/api/repos**", (route) => fulfillJson(route, [repo]));
  await page.route("**/api/github/owners", (route) =>
    fulfillJson(route, [
      {
        login: "alex",
        kind: "personal",
        github_installation_id: null,
        scope_label: "Personal",
        source: "oauth",
      },
    ])
  );
  await page.route("**/api/github/repos/availability**", (route) =>
    fulfillJson(route, { availability: "available" })
  );
  await page.goto(scopedPath("control"));
  const picker = page.getByRole("combobox", { name: "Project", exact: true });
  await picker.click();
  const search = page.getByRole("textbox", { name: "Search projects" });
  await search.fill("  invoices  ");
  await expect(
    page.getByRole("option", { name: 'Create project "invoices"' })
  ).toBeVisible();
  await search.press("Enter");
  await expect(page.getByLabel("New project name")).toHaveValue("invoices");
  await picker.click();
  await search.fill("WIDGETS");
  await expect(
    page.getByRole("option", { name: "acme/widgets" })
  ).toBeVisible();
  await expect(
    page.getByRole("option", { name: /Create project/ })
  ).toHaveCount(0);
  await search.fill("acme/widgets");
  await expect(
    page.getByRole("option", { name: /Create project/ })
  ).toHaveCount(0);
  // With the create row hidden, the first repo is option 0 for the keyboard.
  await search.press("ArrowDown");
  const widgets = page.getByRole("option", { name: "acme/widgets" });
  await expect(widgets).toBeFocused();
  await widgets.press("ArrowUp");
  await expect(search).toBeFocused();
  await search.fill("wid");
  await expect(
    page.getByRole("option", { name: 'Create project "wid"' })
  ).toBeVisible();
  await page.getByRole("option", { name: 'Create project "wid"' }).click();
  await expect(page.getByLabel("New project name")).toHaveValue("wid");
});

test("project picker pins New project above a scrolling repo list", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockBaseChrome(page);
  await page.route("**/api/connections", (route) =>
    fulfillJson(route, { connections: [] })
  );
  await page.route("**/api/control/sessions**", (route) =>
    fulfillJson(route, [])
  );
  const repos: Repo[] = Array.from({ length: 30 }, (_, index) => ({
    id: `repo-${index}`,
    user_id: "user-1",
    full_name: `acme/app-${String(index).padStart(2, "0")}`,
    owner: "acme",
    name: `app-${String(index).padStart(2, "0")}`,
    default_branch: "main",
    created_at: "2026-09-01T00:00:00Z",
  }));
  await page.route("**/api/repos**", (route) => fulfillJson(route, repos));
  await page.goto(scopedPath("control"));
  await page.getByRole("combobox", { name: "Project", exact: true }).click();

  const options = page.getByRole("option");
  const newProject = page.getByRole("option", { name: "New project…" });
  await expect(options.first()).toHaveText("New project…");
  const listbox = page.getByRole("listbox", { name: "Project options" });
  await expect(listbox.getByRole("group", { name: "Create" })).toBeVisible();
  await expect(
    listbox.getByRole("group", { name: "Repositories" })
  ).toBeVisible();
  await page
    .getByRole("option", { name: "acme/app-29" })
    .scrollIntoViewIfNeeded();
  await expect(newProject).toBeInViewport();

  const search = page.getByRole("textbox", { name: "Search projects" });
  await search.press("ArrowDown");
  await expect(newProject).toBeFocused();
  await newProject.press("ArrowDown");
  const firstRepo = page.getByRole("option", { name: "acme/app-00" });
  await expect(firstRepo).toBeFocused();
  await firstRepo.press("ArrowUp");
  await expect(newProject).toBeFocused();
  await newProject.press("ArrowUp");
  await expect(search).toBeFocused();

  await search.fill("no-such-repo");
  await expect(
    listbox.getByRole("group", { name: "Repositories" })
  ).toHaveCount(0);
  await expect(options).toHaveCount(1);
  await search.fill("app-1");
  await expect(options.first()).toHaveText('Create project "app-1"');
  await search.press("Enter");
  await expect(
    page.getByRole("combobox", { name: "Project", exact: true })
  ).toContainText("acme/app-10");
});

test("new mission validates and creates an org-scoped project before starting", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockBaseChrome(page);
  await page.route("**/api/connections", (route) =>
    fulfillJson(route, { connections: [] })
  );

  let createdRepo: {
    id: string;
    full_name: string;
    owner: string;
    name: string;
    default_branch: string;
  } | null = null;
  await page.route("**/api/repos**", (route) =>
    fulfillJson(route, createdRepo ? [createdRepo] : [])
  );
  await page.route("**/api/github/owners", (route) =>
    fulfillJson(route, [
      {
        login: "alex",
        kind: "personal",
        github_installation_id: null,
        scope_label: "Personal",
        source: "oauth",
      },
      {
        login: "acme",
        kind: "org",
        github_installation_id: 42,
        scope_label: "Org",
        source: "oauth+installation",
      },
    ])
  );
  await page.route("**/api/github/repos/availability?**", (route) => {
    const url = new URL(route.request().url());
    const name = url.searchParams.get("name") || "";
    return fulfillJson(route, {
      availability: name === "taken" ? "taken" : "available",
      owner: url.searchParams.get("owner"),
      name,
    });
  });

  const repoCreates: Array<{ owner_login?: string; name?: string }> = [];
  await page.route("**/api/github/repos", (route) => {
    if (route.request().method() !== "POST") return route.continue();
    const body = route.request().postDataJSON() as {
      owner_login?: string;
      name?: string;
    };
    repoCreates.push(body);
    createdRepo = {
      id: "repo-created",
      full_name: `${body.owner_login}/${body.name}`,
      owner: body.owner_login || "",
      name: body.name || "",
      default_branch: "main",
    };
    return fulfillJson(route, createdRepo);
  });

  const sessionCreates: Array<{
    title?: string;
    project?: string | null;
    repo_id?: string | null;
  }> = [];
  let sessionAttempts = 0;
  await page.route("**/api/control/sessions**", (route) => {
    const request = route.request();
    if (request.method() === "GET") return fulfillJson(route, []);
    if (request.method() !== "POST") return route.continue();
    sessionAttempts += 1;
    if (sessionAttempts === 1) {
      return fulfillJson(route, { error: "Temporary session failure" }, 500);
    }
    const body = request.postDataJSON() as (typeof sessionCreates)[number];
    sessionCreates.push(body);
    return fulfillJson(route, {
      id: "sess-created",
      title: body.title ?? "Session",
      project: body.project ?? null,
      repo_id: body.repo_id ?? null,
      pinned: false,
      archived: false,
      created_at: "2026-08-14T00:00:00.000Z",
      updated_at: "2026-08-14T00:00:00.000Z",
      messages: [],
    });
  });

  const chatRequests: Array<Record<string, unknown>> = [];
  await page.route("**/api/control/chat", (route) => {
    chatRequests.push(
      route.request().postDataJSON() as Record<string, unknown>
    );
    return route.fulfill({
      status: 200,
      headers: {
        "content-type": "text/event-stream",
        "x-vercel-ai-ui-message-stream": "v1",
      },
      body: 'data: {"type":"start"}\n\ndata: [DONE]\n\n',
    });
  });

  await page.goto(scopedPath("control"));
  await page.waitForLoadState("networkidle");
  await page
    .getByPlaceholder("Ask anything or run a command...")
    .fill("Rebuild the analytics dashboard");

  const ownerPicker = page.getByLabel("GitHub owner");
  await expect(ownerPicker).toBeVisible();
  await ownerPicker.click();
  await page.getByRole("option", { name: /acme.*Org/ }).click();

  const nameInput = page.getByLabel("New project name");
  await nameInput.fill("taken");
  await expect(page.getByText("acme/taken already exists.")).toBeVisible();
  await expect(
    page.getByRole("button", { name: "Start mission" })
  ).toBeDisabled();

  await nameInput.fill("Analytics / redesign 🚀");
  await expect(
    page.getByText("Will create as Analytics-redesign.")
  ).toBeVisible();
  await expect(
    page.getByText("acme/Analytics-redesign is available.")
  ).toBeVisible();
  await page.getByRole("button", { name: "Start mission" }).click();

  await expect.poll(() => repoCreates.length).toBe(1);
  await expect(
    page.getByText(
      "Repository created, but the mission could not start. Try again."
    )
  ).toBeVisible();
  await expect(
    page.getByText("Could not create the mission session. Please try again.")
  ).toHaveCount(0);
  await expect(page.getByText("acme/Analytics-redesign")).toBeVisible();
  await page.getByRole("button", { name: "Start mission" }).click();

  await expect.poll(() => sessionAttempts).toBe(2);
  expect(repoCreates).toHaveLength(1);
  await expect.poll(() => sessionCreates.length).toBe(1);
  await expect.poll(() => chatRequests.length).toBe(1);
  expect(repoCreates[0]).toEqual({
    owner_login: "acme",
    name: "Analytics-redesign",
  });
  expect(sessionCreates[0]).toMatchObject({
    project: "acme/Analytics-redesign",
    repo_id: "repo-created",
  });
  expect(chatRequests[0]).toMatchObject({
    conversationId: "sess-created",
    missionId: "sess-created",
    repoId: "repo-created",
    repoFullName: "acme/Analytics-redesign",
    repoOwner: "acme",
    repoName: "Analytics-redesign",
    repoBranch: "main",
    repoBaseBranch: "main",
  });
  expect(
    await page.evaluate(() =>
      window.localStorage.getItem("mogplex:last-github-repo-owner")
    )
  ).toBe("acme");
});

test("new project explains GitHub connection and org-scope requirements", 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, []));
  await page.route("**/api/control/sessions**", (route) =>
    fulfillJson(route, [])
  );
  await page.route("**/api/github/owners", (route) => fulfillJson(route, []));

  await page.goto(scopedPath("control"));
  await expect(
    page.getByText("GitHub must be connected to create a project.")
  ).toBeVisible();
  await expect(
    page.getByRole("link", { name: "Connect GitHub" })
    // The signed-in connect route. /api/auth/login/github is the signup path
    // and bounces an existing account to /login/beta?error=waitlist_required.
  ).toHaveAttribute("href", /\/api\/auth\/github\?next=/);
  await expect(
    page.getByRole("button", { name: "Start mission" })
  ).toBeDisabled();

  await page.unroute("**/api/github/owners");
  await page.route("**/api/github/owners", (route) =>
    route.fulfill({
      status: 200,
      contentType: "application/json",
      headers: { "x-mogplex-github-reauthorize": "read:org" },
      body: JSON.stringify([
        {
          login: "alex",
          kind: "personal",
          github_installation_id: null,
          scope_label: "Personal",
          source: "oauth",
        },
      ]),
    })
  );
  await page.reload();
  await expect(
    page.getByText("Reconnect GitHub to use organization accounts.")
  ).toBeVisible();
  await expect(
    page.getByRole("link", { name: "Reconnect GitHub" })
    // reauthorize=1 forces the OAuth grant, the only path that can add the
    // missing read:org scope where the GitHub App is configured.
  ).toHaveAttribute("href", /\/api\/auth\/github\?next=[^"]*reauthorize=1/);
});

test("new project remains actionable when availability auth expires", 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, []));
  await page.route("**/api/control/sessions**", (route) =>
    fulfillJson(route, [])
  );
  await page.route("**/api/github/owners", (route) =>
    fulfillJson(route, [
      {
        login: "alex",
        kind: "personal",
        github_installation_id: null,
        scope_label: "Personal",
        source: "oauth",
      },
    ])
  );
  await page.route("**/api/github/repos/availability**", (route) =>
    fulfillJson(
      route,
      { availability: "invalid", error: "Connect GitHub account first" },
      400
    )
  );

  await page.goto(scopedPath("control"));
  await page
    .getByPlaceholder("Ask anything or run a command...")
    .fill("Ship account settings");
  await expect(
    page.getByText("Availability could not be verified. You can still try.")
  ).toBeVisible();
  await expect(
    page.getByRole("button", { name: "Start mission" })
  ).toBeEnabled();
});

test("new project preserves the selected owner while switching projects", 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",
      },
    ])
  );
  await page.route("**/api/control/sessions**", (route) =>
    fulfillJson(route, [])
  );
  await page.route("**/api/github/owners", (route) =>
    fulfillJson(route, [
      {
        login: "alex",
        kind: "personal",
        github_installation_id: null,
        scope_label: "Personal",
        source: "oauth",
      },
      {
        login: "acme",
        kind: "org",
        github_installation_id: 42,
        scope_label: "Org",
        source: "oauth+installation",
      },
    ])
  );

  await page.goto(scopedPath("control"));
  const projectPicker = page.getByLabel("Project", { exact: true });
  await projectPicker.click();
  await page.getByRole("option", { name: "New project" }).click();
  const ownerPicker = page.getByLabel("GitHub owner");
  await ownerPicker.click();
  await page.getByRole("option", { name: /acme.*Org/ }).click();
  await projectPicker.click();
  await page.getByRole("option", { name: "acme/widgets" }).click();
  await projectPicker.click();
  await page.getByRole("option", { name: "New project" }).click();

  await expect(ownerPicker).toContainText("acme");
});