tests/e2e/mutation-errors.spec.ts
Rama main · réplica de lectura del repositorio original.
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/);
});