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