From 7903dc129c73f4f14cb6bb42617739ed2c1c61d3 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 11:08:51 +0000 Subject: [PATCH 1/2] test(e2e): stabilize activity feed + Clerk helper timeouts --- frontend/cypress.config.ts | 7 ++ frontend/cypress/e2e/activity_feed.cy.ts | 101 ++++++++++++----------- 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index 0a2195d6..18baa6bb 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -14,6 +14,13 @@ export default defineConfig({ baseUrl: "http://localhost:3000", specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", supportFile: "cypress/support/e2e.ts", + // Clerk helpers perform async work inside `cy.then()`. CI can be slow enough + // that Cypress' 4s default command timeout flakes. + defaultCommandTimeout: 20_000, + retries: { + runMode: 2, + openMode: 0, + }, setupNodeEvents(on, config) { return clerkSetup({ config }); }, diff --git a/frontend/cypress/e2e/activity_feed.cy.ts b/frontend/cypress/e2e/activity_feed.cy.ts index 93208651..8d2ac86b 100644 --- a/frontend/cypress/e2e/activity_feed.cy.ts +++ b/frontend/cypress/e2e/activity_feed.cy.ts @@ -4,30 +4,14 @@ describe("/activity feed", () => { const apiBase = "**/api/v1"; const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; - const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); - - 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); - }); - - afterEach(() => { - Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); - }); - - function stubStreamEmpty() { - cy.intercept( - "GET", - `${apiBase}/activity/task-comments/stream*`, - { - statusCode: 200, - headers: { - "content-type": "text/event-stream", - }, - body: "", + function stubSseEmpty(pathGlob: string, alias: string) { + cy.intercept("GET", pathGlob, { + statusCode: 200, + headers: { + "content-type": "text/event-stream", }, - ).as("activityStream"); + body: "", + }).as(alias); } function assertSignedInAndLanded() { @@ -35,35 +19,52 @@ describe("/activity feed", () => { cy.contains(/live feed/i).should("be.visible"); } - it("auth negative: signed-out user cannot access /activity", () => { - // Story: signed-out user tries to visit /activity and is redirected to sign-in. + it("auth negative: signed-out user is prompted to sign in", () => { cy.visit("/activity"); - cy.location("pathname", { timeout: 20_000 }).should("match", /\/sign-in/); + cy.contains(/sign in to view the feed/i).should("be.visible"); + cy.get('[data-testid="activity-signin"]').should("be.visible"); }); - it("happy path: renders task comment cards", () => { - cy.intercept("GET", `${apiBase}/activity/task-comments*`, { + it("happy path: renders feed items from the activity endpoint", () => { + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { + items: [{ id: "b1", name: "Testing", updated_at: "2026-02-07T00:00:00Z" }], + }, + }).as("boardsList"); + + cy.intercept("GET", `${apiBase}/boards/b1/snapshot*`, { + statusCode: 200, + body: { + tasks: [{ id: "t1", title: "CI hardening" }], + agents: [], + approvals: [], + chat_messages: [], + }, + }).as("boardSnapshot"); + + cy.intercept("GET", `${apiBase}/activity*`, { statusCode: 200, body: { items: [ { - id: "c1", - message: "Hello world", - agent_name: "Kunal", - agent_role: "QA 2", - board_id: "b1", - board_name: "Testing", - task_id: "t1", - task_title: "CI hardening", + id: "evt-1", created_at: "2026-02-07T00:00:00Z", + event_type: "task.comment", + message: "Hello world", + agent_id: null, + task_id: "t1", }, ], }, }).as("activityList"); - stubStreamEmpty(); + // 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"); - // Story: user signs in, then visits /activity and sees the live feed. cy.visit("/sign-in"); cy.clerkLoaded(); cy.clerkSignIn({ strategy: "email_code", identifier: email }); @@ -76,14 +77,18 @@ describe("/activity feed", () => { }); it("empty state: shows waiting message when no items", () => { - cy.intercept("GET", `${apiBase}/activity/task-comments*`, { + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { items: [] }, + }).as("boardsList"); + + cy.intercept("GET", `${apiBase}/activity*`, { statusCode: 200, body: { items: [] }, }).as("activityList"); - stubStreamEmpty(); + stubSseEmpty(`${apiBase}/agents/stream*`, "agentsStream"); - // Story: user signs in, then visits /activity and sees an empty-state message. cy.visit("/sign-in"); cy.clerkLoaded(); cy.clerkSignIn({ strategy: "email_code", identifier: email }); @@ -91,18 +96,22 @@ describe("/activity feed", () => { cy.visit("/activity"); assertSignedInAndLanded(); - cy.contains(/waiting for new comments/i).should("be.visible"); + cy.contains(/waiting for new activity/i).should("be.visible"); }); it("error state: shows failure UI when API errors", () => { - cy.intercept("GET", `${apiBase}/activity/task-comments*`, { + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { items: [] }, + }).as("boardsList"); + + cy.intercept("GET", `${apiBase}/activity*`, { statusCode: 500, body: { detail: "boom" }, }).as("activityList"); - stubStreamEmpty(); + stubSseEmpty(`${apiBase}/agents/stream*`, "agentsStream"); - // Story: user signs in, then visits /activity; API fails and user sees an error. cy.visit("/sign-in"); cy.clerkLoaded(); cy.clerkSignIn({ strategy: "email_code", identifier: email }); @@ -110,6 +119,6 @@ describe("/activity feed", () => { cy.visit("/activity"); assertSignedInAndLanded(); - cy.contains(/unable to load feed|boom/i).should("be.visible"); + cy.contains(/unable to load activity feed|boom/i).should("be.visible"); }); }); From 10f12bd4a00ee929cce48d4edd3e5a82fb69989e Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 11:21:53 +0000 Subject: [PATCH 2/2] test(e2e): assert signed-out redirect on /activity --- frontend/cypress/e2e/activity_feed.cy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/cypress/e2e/activity_feed.cy.ts b/frontend/cypress/e2e/activity_feed.cy.ts index 8d2ac86b..3e53c99a 100644 --- a/frontend/cypress/e2e/activity_feed.cy.ts +++ b/frontend/cypress/e2e/activity_feed.cy.ts @@ -19,10 +19,10 @@ describe("/activity feed", () => { cy.contains(/live feed/i).should("be.visible"); } - it("auth negative: signed-out user is prompted to sign in", () => { + it("auth negative: signed-out user is redirected to sign-in", () => { + // SignedOutPanel runs in redirect mode on this page. cy.visit("/activity"); - cy.contains(/sign in to view the feed/i).should("be.visible"); - cy.get('[data-testid="activity-signin"]').should("be.visible"); + cy.location("pathname", { timeout: 20_000 }).should("match", /\/sign-in/); }); it("happy path: renders feed items from the activity endpoint", () => {