mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 11:33:27 +00:00
* test(open-swe): add Playwright E2E for the Slack → PR → web handoff Local, secrets-free end-to-end suite that drives the full happy path through mock Slack/GitHub control panels and the real dashboard UI. Only the LLM and external SaaS HTTP boundaries (GitHub/Slack APIs, OAuth token mint) are faked — the real process_slack_mention, get_agent, deepagents loop, tools, middleware, and dashboard authorization all run under `langgraph dev` with a scripted fake chat model and a local temp-dir sandbox. - full_flow: a Slack mention runs the agent, which implements a change in the sandbox, opens a PR against a fake GitHub remote, and replies with the PR link in the same thread. - dashboard: clicking the bot's real "Open in Web" link loads the built ui/ app (served same-origin); the thread owner can continue the conversation, while a different user sees the same thread read-only (no composer). Wired into Agent CI as a `Playwright E2E` job that runs on pull requests. * fix(open-swe): serve E2E UI assets via explicit route; pin Playwright The dashboard E2E served the built ui/ SPA's /assets via app.mount(StaticFiles), but LangGraph's custom-app loader serves APIRoutes and drops sub-app Mounts, so /assets 404'd under `langgraph dev` in CI — the React app never booted and the composer/transcript never rendered. Serve assets via an explicit route instead. Also pin @playwright/test to the latest (1.61.0) for reproducible runs, and make the owner composer assertion tolerant of either hydration state. * test(open-swe): record Playwright trace + video on every E2E run Capture a replayable trace (DOM snapshots, network, console, source) and a screen recording for every test, not just retries, plus a screenshot on failure. The CI job already uploads playwright-report/ and test-results/, so each run now has a downloadable replay; documented how to open it.
70 lines
3.2 KiB
TypeScript
70 lines
3.2 KiB
TypeScript
import { test, expect, type Page } from "@playwright/test";
|
|
|
|
// Drives the REAL built ui/ app (served same-origin from the harness) for the
|
|
// Slack → web handoff. Only the LLM/GitHub/Slack/token boundaries are faked.
|
|
const SAME_USER = { login: "dev-user", email: "dev@example.com" };
|
|
const OTHER_USER = { login: "someone-else", email: "someone@example.com" };
|
|
|
|
async function loginAs(page: Page, user: { login: string; email: string }) {
|
|
const res = await page.request.post("/control/login", { data: user });
|
|
expect(res.ok()).toBeTruthy();
|
|
}
|
|
|
|
// Run the Slack flow so a thread + PR exist, then click the bot's real
|
|
// "Open in Web" link, landing on the actual dashboard app.
|
|
async function openThreadViaSlackLink(page: Page) {
|
|
await page.goto("/mock/slack");
|
|
await page.locator("#reset").click();
|
|
await expect(page.locator("#thread")).toContainText("No messages yet");
|
|
await page.locator("#text").fill("<@U0BOT> please add a greet() helper and open a PR");
|
|
await page.locator("#send").click();
|
|
await expect(page.locator(".msg.bot").filter({ hasText: "Add greet() helper" })).toBeVisible();
|
|
|
|
const webLink = page.locator('.msg.bot a[href*="/agents/"]').first();
|
|
await expect(webLink).toBeVisible();
|
|
await webLink.click();
|
|
await expect(page).toHaveURL(/\/agents\//);
|
|
}
|
|
|
|
// The SDK hydrates an idle thread's transcript from getState on load, which can
|
|
// briefly lag; a reload re-fetches it. Retry until the PR link renders.
|
|
async function expectTranscriptVisible(page: Page) {
|
|
await expect(async () => {
|
|
await page.reload();
|
|
await expect(
|
|
page.getByRole("link", { name: "Add greet() helper" }).first(),
|
|
).toBeVisible({ timeout: 8000 });
|
|
}).toPass({ timeout: 60000 });
|
|
}
|
|
|
|
test.describe("Slack → web handoff (real dashboard UI)", () => {
|
|
test("the SAME user continues the conversation in the web app", async ({ page }) => {
|
|
await loginAs(page, SAME_USER);
|
|
await openThreadViaSlackLink(page);
|
|
|
|
// The owner sees the composer (either the follow-up bar once the transcript
|
|
// hydrates, or the empty-state bar before it — both mean they can type).
|
|
const composer = page.getByPlaceholder(/Add a follow up|Send the first message/);
|
|
await expect(composer).toBeVisible();
|
|
|
|
// Continue from the web — a new agent reply streams into the same thread.
|
|
await composer.fill("Looks good — can you also add a docstring?");
|
|
await composer.press("Enter");
|
|
await expect(page.getByText(/anything else you'd like changed/)).toBeVisible();
|
|
|
|
// The transcript that started in Slack is here too (incl. the PR link).
|
|
await expect(page.getByRole("link", { name: "Add greet() helper" }).first()).toBeVisible();
|
|
});
|
|
|
|
test("a DIFFERENT user sees the thread read-only (no composer)", async ({ page }) => {
|
|
await loginAs(page, OTHER_USER);
|
|
await openThreadViaSlackLink(page);
|
|
|
|
// The same thread + transcript is visible…
|
|
await expectTranscriptVisible(page);
|
|
// …but a non-owner gets no composer.
|
|
await expect(page.getByPlaceholder("Add a follow up")).toHaveCount(0);
|
|
await expect(page.getByPlaceholder("Send the first message")).toHaveCount(0);
|
|
await expect(page.getByLabel("Send message")).toHaveCount(0);
|
|
});
|
|
});
|