diff --git a/frontend/cypress/e2e/activity_feed.cy.ts b/frontend/cypress/e2e/activity_feed.cy.ts index 3e53c99a..4f8d3d3f 100644 --- a/frontend/cypress/e2e/activity_feed.cy.ts +++ b/frontend/cypress/e2e/activity_feed.cy.ts @@ -4,28 +4,47 @@ describe("/activity feed", () => { const apiBase = "**/api/v1"; const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; - function stubSseEmpty(pathGlob: string, alias: string) { - cy.intercept("GET", pathGlob, { - statusCode: 200, - headers: { - "content-type": "text/event-stream", - }, - body: "", - }).as(alias); - } + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); - function assertSignedInAndLanded() { - cy.waitForAppLoaded(); - cy.contains(/live feed/i).should("be.visible"); - } - - it("auth negative: signed-out user is redirected to sign-in", () => { - // SignedOutPanel runs in redirect mode on this page. - cy.visit("/activity"); - cy.location("pathname", { timeout: 20_000 }).should("match", /\/sign-in/); + beforeEach(() => { + // Clerk's Cypress helpers perform async work inside `cy.then()`. + // CI can be slow enough that the default 4s command timeout flakes. + Cypress.config("defaultCommandTimeout", 20_000); }); - it("happy path: renders feed items from the activity endpoint", () => { + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); + + function stubStreamsEmpty() { + // The activity page connects multiple SSE streams (tasks/approvals/agents/board memory). + // In E2E we keep them empty to avoid flake and keep assertions deterministic. + const emptySse = { + statusCode: 200, + headers: { "content-type": "text/event-stream" }, + body: "", + }; + + cy.intercept("GET", `${apiBase}/boards/*/tasks/stream*`, emptySse).as( + "tasksStream", + ); + cy.intercept("GET", `${apiBase}/boards/*/approvals/stream*`, emptySse).as( + "approvalsStream", + ); + cy.intercept("GET", `${apiBase}/boards/*/memory/stream*`, emptySse).as( + "memoryStream", + ); + cy.intercept("GET", `${apiBase}/agents/stream*`, emptySse).as("agentsStream"); + } + + function stubBoardBootstrap() { + // Some app bootstraps happen before we get to the /activity call. + // Keep these stable so the page always reaches the activity request. + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { organization_id: "org1", role: "owner" }, + }).as("orgMeMember"); + cy.intercept("GET", `${apiBase}/boards*`, { statusCode: 200, body: { @@ -42,28 +61,42 @@ describe("/activity feed", () => { chat_messages: [], }, }).as("boardSnapshot"); + } + + function assertSignedInAndLanded() { + cy.waitForAppLoaded(); + cy.contains(/live feed/i).should("be.visible"); + } + + it("auth negative: signed-out user is redirected to sign-in", () => { + // SignedOutPanel runs in redirect mode on this page. + cy.visit("/activity"); + cy.location("pathname", { timeout: 20_000 }).should("match", /\/sign-in/); + }); + + it("happy path: renders task comment cards", () => { + stubBoardBootstrap(); cy.intercept("GET", `${apiBase}/activity*`, { statusCode: 200, body: { items: [ { - id: "evt-1", - created_at: "2026-02-07T00:00:00Z", + id: "e1", event_type: "task.comment", message: "Hello world", agent_id: null, + agent_name: "Kunal", + created_at: "2026-02-07T00:00:00Z", task_id: "t1", + task_title: "CI hardening", + agent_role: "QA 2", }, ], }, }).as("activityList"); - // Prevent SSE connections from hanging the test. - stubSseEmpty(`${apiBase}/boards/b1/tasks/stream*`, "tasksStream"); - stubSseEmpty(`${apiBase}/boards/b1/approvals/stream*`, "approvalsStream"); - stubSseEmpty(`${apiBase}/boards/b1/memory/stream*`, "memoryStream"); - stubSseEmpty(`${apiBase}/agents/stream*`, "agentsStream"); + stubStreamsEmpty(); cy.visit("/sign-in"); cy.clerkLoaded(); @@ -72,22 +105,19 @@ describe("/activity feed", () => { cy.visit("/activity"); assertSignedInAndLanded(); - cy.contains("CI hardening").should("be.visible"); - cy.contains("Hello world").should("be.visible"); + cy.contains(/ci hardening/i).should("be.visible"); + cy.contains(/hello world/i).should("be.visible"); }); it("empty state: shows waiting message when no items", () => { - cy.intercept("GET", `${apiBase}/boards*`, { - statusCode: 200, - body: { items: [] }, - }).as("boardsList"); + stubBoardBootstrap(); cy.intercept("GET", `${apiBase}/activity*`, { statusCode: 200, body: { items: [] }, }).as("activityList"); - stubSseEmpty(`${apiBase}/agents/stream*`, "agentsStream"); + stubStreamsEmpty(); cy.visit("/sign-in"); cy.clerkLoaded(); @@ -100,17 +130,14 @@ describe("/activity feed", () => { }); it("error state: shows failure UI when API errors", () => { - cy.intercept("GET", `${apiBase}/boards*`, { - statusCode: 200, - body: { items: [] }, - }).as("boardsList"); + stubBoardBootstrap(); cy.intercept("GET", `${apiBase}/activity*`, { statusCode: 500, body: { detail: "boom" }, }).as("activityList"); - stubSseEmpty(`${apiBase}/agents/stream*`, "agentsStream"); + stubStreamsEmpty(); cy.visit("/sign-in"); cy.clerkLoaded(); @@ -119,6 +146,6 @@ describe("/activity feed", () => { cy.visit("/activity"); assertSignedInAndLanded(); - cy.contains(/unable to load activity feed|boom/i).should("be.visible"); + cy.contains(/unable to load activity feed/i).should("be.visible"); }); });