Ctrl K
ComunidadGuías…

tests/e2e/mutation-errors.spec.ts

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

Archivo original ↗Preguntar a KiBot
import { expect, test, type Page } from "@playwright/test";
import { enableScopedE2EAuth, scopedPath } from "./helpers/auth";
import { fulfillJson, mockActivationFlow } from "./helpers/activation-fixtures";
import type { Agent, Assignment, ObservabilityJob } from "@/lib/types";
import type { AgentRosterItem } from "@/lib/agents/roster-types";
import type { Session } from "@/hooks/session-types";

test.beforeEach(async ({ page }) => {
  await page.route("**/api/integrations/slack/installations", (route) =>
    fulfillJson(route, { installations: [] })
  );
});

const agent: Agent = {
  id: "agent-1",
  user_id: "user-1",
  name: "Review agent",
  slug: "review-agent",
  model: "test-model",
  system_prompt: "Review changes",
  created_at: "2026-10-01T00:00:00Z",
};
const assignment: Assignment = {
  id: "assignment-1",
  repo_id: "repo-1",
  agent_id: agent.id,
  type: "pr_review",
  cron_schedule: null,
  skill_id: null,
  enabled: true,
  created_at: "2026-10-01T00:00:00Z",
  last_job_run_id: "job-1",
  last_run_status: "running",
  last_run_cancelable: true,
};

type Rule = { id: string; name: string; content: string; type: string };
const rule: Rule = {
  id: "rule-1",
  name: "Review rules",
  content: "Original instructions",
  type: "rules",
};

async function setupRules(page: Page, failedMethod: string) {
  await enableScopedE2EAuth(page);
  await mockActivationFlow(page);
  let saved = { ...rule };
  await page.route("**/api/rules**", async (route) => {
    const method = route.request().method();
    if (method === "GET") return fulfillJson(route, [saved]);
    if (method === failedMethod)
      return fulfillJson(route, { error: "Rule access denied" }, 403);
    if (method === "PUT") {
      const body = route.request().postDataJSON() as { content: string };
      saved = { ...saved, content: body.content };
    }
    return fulfillJson(route, saved);
  });
  await page.goto(scopedPath("agents/rules"));
  await expect(page.getByText(rule.name, { exact: true })).toBeVisible();
}

test("failed rule saves retain the draft and allow a successful retry", async ({
  page,
}) => {
  await setupRules(page, "PUT");
  await page.getByText(rule.name, { exact: true }).click();
  await page.getByRole("button", { name: "Edit", exact: true }).click();
  await page.getByRole("textbox").fill("Unsaved instructions");
  await page.getByRole("button", { name: "Save", exact: true }).click();
  await expect(
    page.getByText("Rule access denied", { exact: true })
  ).toBeVisible();
  await expect(page.getByRole("textbox")).toHaveValue("Unsaved instructions");
  await expect(page.getByText(rule.content, { exact: true })).toBeVisible();
  await page.route("**/api/rules", (route) =>
    fulfillJson(route, { ...rule, content: "Unsaved instructions" })
  );
  await page.getByRole("button", { name: "Save", exact: true }).click();
  await expect(
    page.getByRole("button", { name: "Edit", exact: true })
  ).toBeVisible();
  await expect(page.getByRole("textbox")).toHaveCount(0);
  await expect(
    page.getByText("Unsaved instructions", { exact: true })
  ).toBeVisible();
});

test("failed rule creation keeps the name and form open", async ({ page }) => {
  await setupRules(page, "POST");
  await page.getByRole("button", { name: "New Rule", exact: true }).click();
  await page.getByPlaceholder("rule-name.md").fill("draft.md");
  await page.getByRole("button", { name: "Create", exact: true }).click();
  await expect(
    page.getByText("Rule access denied", { exact: true })
  ).toBeVisible();
  await expect(page.getByPlaceholder("rule-name.md")).toHaveValue("draft.md");
});

test("failed rule deletion retains the selected rule", async ({ page }) => {
  await setupRules(page, "DELETE");
  await page.getByText(rule.name, { exact: true }).click();
  await page.getByRole("button", { name: "Delete", exact: true }).click();
  await expect(
    page.getByText("Rule access denied", { exact: true })
  ).toBeVisible();
  await expect(
    page.getByRole("button", { name: "Edit", exact: true })
  ).toBeVisible();
  await expect(page.getByText(rule.content, { exact: true })).toHaveCount(2);
});

test("failed assignment actions report errors and restore their controls", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockActivationFlow(page);
  await page.route("**/api/agents", (route) => fulfillJson(route, [agent]));
  await page.route("**/api/assignments**", (route) =>
    fulfillJson(
      route,
      route.request().method() === "GET"
        ? [assignment]
        : { error: "Assignment access denied" },
      route.request().method() === "GET" ? 200 : 403
    )
  );
  await page.route("**/api/observability/jobs/job-1/cancel", (route) =>
    fulfillJson(route, { error: "Cannot cancel run" }, 500)
  );
  await page.goto(scopedPath("assignments"));
  const cancel = page.getByRole("button", { name: "Cancel", exact: true });
  await cancel.click();
  await expect(
    page.getByText("Cannot cancel run", { exact: true })
  ).toBeVisible();
  await expect(cancel).toBeEnabled();
  await page.getByRole("button", { name: "Assignment actions" }).click();
  await page.getByRole("menuitem", { name: "Delete", exact: true }).click();
  await expect(
    page.getByText("Assignment access denied", { exact: true })
  ).toBeVisible();
  await expect(
    page.getByText("Last run: running", { exact: false })
  ).toBeVisible();
});

test("failed agent deletion keeps the agent and displays the server error", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockActivationFlow(page);
  await page.route("**/api/agents**", async (route) => {
    const url = new URL(route.request().url());
    if (url.pathname !== "/api/agents") return route.fallback();
    if (route.request().method() === "DELETE")
      return fulfillJson(route, { error: "Cannot delete agent" }, 403);
    return fulfillJson(route, [agent]);
  });
  await page.route("**/api/agent-categories**", (route) =>
    fulfillJson(route, [])
  );
  await page.goto(scopedPath("agents/roster"));
  await page.getByRole("button", { name: "Agent actions" }).click();
  await page.getByRole("menuitem", { name: "Delete", exact: true }).click();
  await expect(
    page.getByText("Cannot delete agent", { exact: true })
  ).toBeVisible();
  await expect(page.getByText(agent.name, { exact: true })).toBeVisible();
});

test("failed output run actions show an error and keep the run available", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockActivationFlow(page);
  const roster: AgentRosterItem[] = [
    {
      id: agent.id,
      name: agent.name,
      description: "Review changes",
      status: "running",
      cycleCount: 1,
      lastActivity: "2026-10-01T00:00:00Z",
    },
  ];
  const job: ObservabilityJob = {
    id: "job-1",
    assignment_id: assignment.id,
    trigger_id: null,
    status: "running",
    created_at: "2026-10-01T00:00:00Z",
    started_at: "2026-10-01T00:00:00Z",
    completed_at: null,
    input_tokens: null,
    output_tokens: null,
    cost_usd: null,
    duration_ms: null,
    error: null,
    start_attempts: 1,
    metadata: null,
    source_kind: "assignment",
    source_type: "pr_review",
    repo: { id: "repo-1", full_name: "acme/widgets" },
    agent: { id: agent.id, name: agent.name, slug: agent.slug },
    latest_ai_call: null,
    latest_dispatch_event: null,
    repairable: false,
    requeueable: false,
    cancelable: true,
  };
  const sessions: Session[] = [
    {
      id: "output-workspace",
      index: 0,
      name: "Output workspace",
      color: "green",
      activeId: "output",
      paneTree: {
        id: "root",
        dir: "horizontal",
        sizes: [40, 60],
        children: [
          {
            id: "roster",
            type: "roster",
            name: "Agents",
            lines: [],
            status: "idle",
          },
          {
            id: "output",
            type: "output",
            name: "Output",
            lines: [],
            status: "idle",
          },
        ],
      },
    },
  ];
  await page.addInitScript(
    (state) =>
      localStorage.setItem(
        "mogplex-sessions",
        JSON.stringify({ state, version: 3 })
      ),
    { sessions, activeSessionId: "output-workspace" }
  );
  await page.route("**/api/agents/roster", (route) =>
    fulfillJson(route, roster)
  );
  await page.route("**/api/observability/jobs?**", (route) =>
    fulfillJson(route, { jobs: [job], total: 1, page: 1, limit: 30 })
  );
  await page.route("**/api/observability/jobs/job-1/cancel", (route) =>
    fulfillJson(route, { error: "Cannot cancel output run" }, 500)
  );
  await page.goto(scopedPath("projects/workspace"));
  await page
    .getByTestId("pane-roster")
    .getByRole("button", { name: /Review agent Review changes/ })
    .click();
  const output = page.getByTestId("pane-output");
  await output.getByRole("button", { name: /widgets/ }).click();
  const cancel = output.getByRole("button", { name: "Cancel", exact: true });
  await cancel.click();
  await expect(
    page.getByText("Cannot cancel output run", { exact: true })
  ).toBeVisible();
  await expect(cancel).toBeEnabled();
});

test("failed cron toggles preserve the enabled item and report the server error", async ({
  page,
}) => {
  await enableScopedE2EAuth(page);
  await mockActivationFlow(page);
  const sessions: Session[] = [
    {
      id: "legacy-workspace",
      index: 0,
      name: "Legacy workspace",
      color: "green",
      activeId: "legacy",
      paneTree: {
        id: "legacy",
        type: "cron",
        name: "Cron",
        lines: [],
        status: "idle",
      },
    },
  ];
  await page.addInitScript(
    (state) =>
      localStorage.setItem(
        "mogplex-sessions",
        JSON.stringify({ state, version: 3 })
      ),
    { sessions, activeSessionId: "legacy-workspace" }
  );
  const cron: Assignment = {
    ...assignment,
    type: "cron",
    cron_schedule: "0 9 * * *",
  };
  await page.route("**/api/assignments**", (route) =>
    fulfillJson(
      route,
      route.request().method() === "GET"
        ? [cron]
        : { error: "Cannot change enabled state" },
      route.request().method() === "GET" ? 200 : 403
    )
  );
  await page.goto(scopedPath("projects/workspace"));
  const pane = page.getByTestId("pane-legacy");
  const toggle = pane.getByRole("button", { name: "", exact: true });
  await toggle.click();
  await expect(
    page.getByText("Cannot change enabled state", { exact: true })
  ).toBeVisible();
  await expect(toggle).toBeEnabled();
  await expect(toggle).toHaveClass(/bg-accent-green/);
});