From ea7a5df1ae93b625118aa65944a989f1e2ff8e94 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 11 Feb 2026 20:47:06 +0000 Subject: [PATCH 01/13] test(e2e): add boards list + board task flows (stubbed) --- frontend/cypress/e2e/board_tasks.cy.ts | 140 +++++++++++++++++++++++++ frontend/cypress/e2e/boards_list.cy.ts | 68 ++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 frontend/cypress/e2e/board_tasks.cy.ts create mode 100644 frontend/cypress/e2e/boards_list.cy.ts diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts new file mode 100644 index 00000000..937682f8 --- /dev/null +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -0,0 +1,140 @@ +/// + +describe("/boards/:id task board", () => { + const apiBase = "**/api/v1"; + const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); + + beforeEach(() => { + Cypress.config("defaultCommandTimeout", 20_000); + }); + + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); + + function stubEmptySse() { + // Any SSE endpoint should not hang the UI in tests. + cy.intercept("GET", `${apiBase}/**/stream*`, { + statusCode: 200, + headers: { "content-type": "text/event-stream" }, + body: "", + }); + } + + it("auth negative: signed-out user is redirected to sign-in", () => { + cy.visit("/boards/b1"); + cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + }); + + it("happy path: renders tasks from snapshot and can create a task (stubbed)", () => { + stubEmptySse(); + + cy.intercept("GET", `${apiBase}/boards/b1/snapshot*`, { + statusCode: 200, + body: { + board: { + id: "b1", + name: "Demo Board", + slug: "demo-board", + description: "Demo", + gateway_id: "g1", + board_group_id: null, + board_type: "general", + objective: null, + success_metrics: null, + target_date: null, + goal_confirmed: true, + goal_source: "test", + organization_id: "o1", + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + }, + tasks: [ + { + id: "t1", + board_id: "b1", + title: "Inbox task", + description: "", + status: "inbox", + priority: "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + ], + agents: [], + approvals: [], + chat_messages: [], + pending_approvals_count: 0, + }, + }).as("snapshot"); + + cy.intercept("GET", `${apiBase}/boards/b1/group-snapshot*`, { + statusCode: 200, + body: { group: null, boards: [] }, + }); + + cy.intercept("POST", `${apiBase}/boards/b1/tasks`, (req) => { + // Minimal assertion the UI sends expected fields. + expect(req.body).to.have.property("title"); + req.reply({ + statusCode: 200, + body: { + id: "t2", + board_id: "b1", + title: req.body.title, + description: req.body.description ?? "", + status: "inbox", + priority: req.body.priority ?? "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + }); + }).as("createTask"); + + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); + + cy.visit("/boards/b1"); + cy.waitForAppLoaded(); + + cy.wait(["@snapshot"]); + + // Existing task visible. + cy.contains("Inbox task").should("be.visible"); + + // Open create task flow. + cy.contains("button", /create task|new task|add task|\+/i) + .first() + .click({ force: true }); + + cy.get("input").filter('[placeholder*="Title"], [name*="title"], [id*="title"], input[type="text"]').first().type("New task"); + + cy.contains("button", /create|save|add/i).click({ force: true }); + cy.wait(["@createTask"]); + + cy.contains("New task").should("be.visible"); + }); +}); diff --git a/frontend/cypress/e2e/boards_list.cy.ts b/frontend/cypress/e2e/boards_list.cy.ts new file mode 100644 index 00000000..e8a01387 --- /dev/null +++ b/frontend/cypress/e2e/boards_list.cy.ts @@ -0,0 +1,68 @@ +/// + +describe("/boards", () => { + const apiBase = "**/api/v1"; + const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); + + beforeEach(() => { + Cypress.config("defaultCommandTimeout", 20_000); + }); + + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); + + it("auth negative: signed-out user is redirected to sign-in", () => { + cy.visit("/boards"); + cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + }); + + it("happy path: signed-in user sees boards list", () => { + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { + items: [ + { + id: "b1", + name: "Demo Board", + slug: "demo-board", + description: "Demo", + gateway_id: "g1", + board_group_id: null, + board_type: "general", + objective: null, + success_metrics: null, + target_date: null, + goal_confirmed: true, + goal_source: "test", + organization_id: "o1", + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + }, + ], + total: 1, + limit: 200, + offset: 0, + }, + }).as("boards"); + + cy.intercept("GET", `${apiBase}/board-groups*`, { + statusCode: 200, + body: { items: [], total: 0, limit: 200, offset: 0 }, + }).as("boardGroups"); + + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); + + cy.visit("/boards"); + cy.waitForAppLoaded(); + + cy.wait(["@boards", "@boardGroups"]); + + cy.contains(/boards/i).should("be.visible"); + cy.contains("Demo Board").should("be.visible"); + }); +}); From 584dac18552cedef1d9d50b5f5348c24387bb498 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 11 Feb 2026 23:42:26 +0000 Subject: [PATCH 02/13] test(e2e): cover task status update + delete (stubbed) --- frontend/cypress/e2e/board_tasks.cy.ts | 70 ++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index 937682f8..aedc6f10 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -2,7 +2,8 @@ describe("/boards/:id task board", () => { const apiBase = "**/api/v1"; - const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + const email = + Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); @@ -28,7 +29,7 @@ describe("/boards/:id task board", () => { cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); }); - it("happy path: renders tasks from snapshot and can create a task (stubbed)", () => { + it("happy path: renders tasks from snapshot and supports create + status update + delete (stubbed)", () => { stubEmptySse(); cy.intercept("GET", `${apiBase}/boards/b1/snapshot*`, { @@ -113,6 +114,38 @@ describe("/boards/:id task board", () => { }); }).as("createTask"); + cy.intercept("PATCH", `${apiBase}/boards/b1/tasks/t1`, (req) => { + expect(req.body).to.have.property("status"); + req.reply({ + statusCode: 200, + body: { + id: "t1", + board_id: "b1", + title: "Inbox task", + description: "", + status: req.body.status, + priority: "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:01Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + }); + }).as("updateTask"); + + cy.intercept("DELETE", `${apiBase}/boards/b1/tasks/t1`, { + statusCode: 200, + body: { ok: true }, + }).as("deleteTask"); + cy.visit("/sign-in"); cy.clerkLoaded(); cy.clerkSignIn({ strategy: "email_code", identifier: email }); @@ -130,11 +163,42 @@ describe("/boards/:id task board", () => { .first() .click({ force: true }); - cy.get("input").filter('[placeholder*="Title"], [name*="title"], [id*="title"], input[type="text"]').first().type("New task"); + cy.get("input") + .filter( + '[placeholder*="Title"], [name*="title"], [id*="title"], input[type="text"]', + ) + .first() + .type("New task"); cy.contains("button", /create|save|add/i).click({ force: true }); cy.wait(["@createTask"]); cy.contains("New task").should("be.visible"); + + // Open edit task dialog. + cy.contains("Inbox task").click({ force: true }); + cy.contains("Edit task").should("be.visible"); + + // Change status via Status select. + cy.contains("label", "Status") + .parent() + .within(() => { + cy.get("button").first().click({ force: true }); + }); + + cy.contains("In progress").click({ force: true }); + + cy.contains("button", /save changes/i).click({ force: true }); + cy.wait(["@updateTask"]); + + // Delete task via delete dialog. + cy.contains("button", /^Delete task$/).click({ force: true }); + cy.get('[aria-label="Delete task"]').should("be.visible"); + cy.get('[aria-label="Delete task"]').within(() => { + cy.contains("button", /^Delete task$/).click({ force: true }); + }); + cy.wait(["@deleteTask"]); + + cy.contains("Inbox task").should("not.exist"); }); }); From 4a6f77eed36ed81459fc0efda99a9adf3f0e2aeb Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 09:10:42 +0000 Subject: [PATCH 03/13] test(e2e): select New task button by aria-label --- frontend/cypress/e2e/board_tasks.cy.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index aedc6f10..464dce21 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -159,9 +159,8 @@ describe("/boards/:id task board", () => { cy.contains("Inbox task").should("be.visible"); // Open create task flow. - cy.contains("button", /create task|new task|add task|\+/i) - .first() - .click({ force: true }); + // Board page uses an icon-only button with aria-label="New task". + cy.get('button[aria-label="New task"]').click({ force: true }); cy.get("input") .filter( From c6dedc93b3c8374a0cc31e95298d8d43820847ee Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 01:29:04 +0530 Subject: [PATCH 04/13] feat: enhance task editing and loading states in board tasks and boards list tests --- frontend/cypress/e2e/board_tasks.cy.ts | 106 +++++++++++++++++++++---- frontend/cypress/e2e/boards_list.cy.ts | 37 ++++++++- 2 files changed, 126 insertions(+), 17 deletions(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index 464dce21..13e3f7c5 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -24,6 +24,14 @@ describe("/boards/:id task board", () => { }); } + function openEditTaskDialog() { + cy.get('button[title="Edit task"]', { timeout: 20_000 }) + .should("be.visible") + .and("not.be.disabled") + .click({ force: true }); + cy.get('[aria-label="Edit task"]', { timeout: 20_000 }).should("be.visible"); + } + it("auth negative: signed-out user is redirected to sign-in", () => { cy.visit("/boards/b1"); cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); @@ -32,6 +40,51 @@ describe("/boards/:id task board", () => { it("happy path: renders tasks from snapshot and supports create + status update + delete (stubbed)", () => { stubEmptySse(); + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { + id: "m1", + organization_id: "o1", + user_id: "u1", + role: "owner", + all_boards_read: true, + all_boards_write: true, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + board_access: [{ board_id: "b1", can_read: true, can_write: true }], + }, + }).as("membership"); + + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: "u1", + clerk_user_id: "clerk_u1", + email, + name: "Jane Test", + preferred_name: "Jane", + timezone: "America/New_York", + is_super_admin: false, + }, + }).as("me"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [ + { id: "o1", name: "Personal", role: "owner", is_active: true }, + ], + }).as("organizations"); + + cy.intercept("GET", `${apiBase}/tags*`, { + statusCode: 200, + body: { items: [], total: 0, limit: 200, offset: 0 }, + }).as("tags"); + + cy.intercept("GET", `${apiBase}/organizations/me/custom-fields*`, { + statusCode: 200, + body: [], + }).as("customFields"); + cy.intercept("GET", `${apiBase}/boards/b1/snapshot*`, { statusCode: 200, body: { @@ -146,6 +199,11 @@ describe("/boards/:id task board", () => { body: { ok: true }, }).as("deleteTask"); + cy.intercept("GET", `${apiBase}/boards/b1/tasks/t1/comments*`, { + statusCode: 200, + body: { items: [], total: 0, limit: 200, offset: 0 }, + }).as("taskComments"); + cy.visit("/sign-in"); cy.clerkLoaded(); cy.clerkSignIn({ strategy: "email_code", identifier: email }); @@ -153,7 +211,14 @@ describe("/boards/:id task board", () => { cy.visit("/boards/b1"); cy.waitForAppLoaded(); - cy.wait(["@snapshot"]); + cy.wait([ + "@snapshot", + "@membership", + "@me", + "@organizations", + "@tags", + "@customFields", + ]); // Existing task visible. cy.contains("Inbox task").should("be.visible"); @@ -162,36 +227,45 @@ describe("/boards/:id task board", () => { // Board page uses an icon-only button with aria-label="New task". cy.get('button[aria-label="New task"]').click({ force: true }); - cy.get("input") - .filter( - '[placeholder*="Title"], [name*="title"], [id*="title"], input[type="text"]', - ) - .first() - .type("New task"); - - cy.contains("button", /create|save|add/i).click({ force: true }); + cy.contains('[role="dialog"]', "New task") + .should("be.visible") + .within(() => { + cy.contains("label", "Title").parent().find("input").type("New task"); + cy.contains("button", /^Create task$/).click({ force: true }); + }); cy.wait(["@createTask"]); cy.contains("New task").should("be.visible"); // Open edit task dialog. cy.contains("Inbox task").click({ force: true }); - cy.contains("Edit task").should("be.visible"); + cy.wait(["@taskComments"]); + cy.contains(/task detail/i).should("be.visible"); + openEditTaskDialog(); // Change status via Status select. - cy.contains("label", "Status") - .parent() - .within(() => { - cy.get("button").first().click({ force: true }); - }); + cy.get('[aria-label="Edit task"]').within(() => { + cy.contains("label", "Status") + .parent() + .within(() => { + cy.get('[role="combobox"]').first().click({ force: true }); + }); + }); cy.contains("In progress").click({ force: true }); cy.contains("button", /save changes/i).click({ force: true }); cy.wait(["@updateTask"]); + cy.get('[aria-label="Edit task"]').should("not.exist"); + + // Save closes the edit dialog; reopen it from task detail. + cy.contains(/task detail/i).should("be.visible"); + openEditTaskDialog(); // Delete task via delete dialog. - cy.contains("button", /^Delete task$/).click({ force: true }); + cy.get('[aria-label="Edit task"]').within(() => { + cy.contains("button", /^Delete task$/).click({ force: true }); + }); cy.get('[aria-label="Delete task"]').should("be.visible"); cy.get('[aria-label="Delete task"]').within(() => { cy.contains("button", /^Delete task$/).click({ force: true }); diff --git a/frontend/cypress/e2e/boards_list.cy.ts b/frontend/cypress/e2e/boards_list.cy.ts index e8a01387..c68a8ab8 100644 --- a/frontend/cypress/e2e/boards_list.cy.ts +++ b/frontend/cypress/e2e/boards_list.cy.ts @@ -20,6 +20,41 @@ describe("/boards", () => { }); it("happy path: signed-in user sees boards list", () => { + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { + id: "m1", + organization_id: "o1", + user_id: "u1", + role: "owner", + all_boards_read: true, + all_boards_write: true, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + board_access: [], + }, + }).as("membership"); + + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: "u1", + clerk_user_id: "clerk_u1", + email, + name: "Jane Test", + preferred_name: "Jane", + timezone: "America/New_York", + is_super_admin: false, + }, + }).as("me"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [ + { id: "o1", name: "Personal", role: "owner", is_active: true }, + ], + }).as("organizations"); + cy.intercept("GET", `${apiBase}/boards*`, { statusCode: 200, body: { @@ -60,7 +95,7 @@ describe("/boards", () => { cy.visit("/boards"); cy.waitForAppLoaded(); - cy.wait(["@boards", "@boardGroups"]); + cy.wait(["@membership", "@me", "@organizations", "@boards", "@boardGroups"]); cy.contains(/boards/i).should("be.visible"); cy.contains("Demo Board").should("be.visible"); From 1045267d4ce3b26d42cc00f8338c810b528f6aeb Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 01:39:04 +0530 Subject: [PATCH 05/13] feat: add groupSnapshot alias and handle hydration mismatch in E2E tests --- frontend/cypress/e2e/board_tasks.cy.ts | 3 ++- frontend/cypress/support/e2e.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index 13e3f7c5..441b65cf 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -137,7 +137,7 @@ describe("/boards/:id task board", () => { cy.intercept("GET", `${apiBase}/boards/b1/group-snapshot*`, { statusCode: 200, body: { group: null, boards: [] }, - }); + }).as("groupSnapshot"); cy.intercept("POST", `${apiBase}/boards/b1/tasks`, (req) => { // Minimal assertion the UI sends expected fields. @@ -213,6 +213,7 @@ describe("/boards/:id task board", () => { cy.wait([ "@snapshot", + "@groupSnapshot", "@membership", "@me", "@organizations", diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts index 369182a5..a1b252f3 100644 --- a/frontend/cypress/support/e2e.ts +++ b/frontend/cypress/support/e2e.ts @@ -5,6 +5,15 @@ import { addClerkCommands } from "@clerk/testing/cypress"; +// Clerk/Next.js occasionally throws a non-deterministic hydration mismatch +// on /sign-in. Ignore this known UI noise so E2E assertions can proceed. +Cypress.on("uncaught:exception", (err) => { + if (err?.message?.includes("Hydration failed")) { + return false; + } + return true; +}); + addClerkCommands({ Cypress, cy }); import "./commands"; From 42b361ddd95ee9357d897f5e4938cdcf419a4030 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 02:32:42 +0530 Subject: [PATCH 06/13] test(frontend): stabilize auth boundary tests for local auth mode --- frontend/src/app/activity/page.test.tsx | 30 +++++++++++++++++------- frontend/src/app/approvals/page.test.tsx | 30 +++++++++++++++++------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/activity/page.test.tsx b/frontend/src/app/activity/page.test.tsx index 981934c1..388a5826 100644 --- a/frontend/src/app/activity/page.test.tsx +++ b/frontend/src/app/activity/page.test.tsx @@ -62,19 +62,33 @@ vi.mock("@clerk/nextjs", () => { describe("/activity auth boundary", () => { it("renders without ClerkProvider runtime errors when publishable key is a placeholder", () => { + const previousAuthMode = process.env.NEXT_PUBLIC_AUTH_MODE; + const previousPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + // Simulate CI/secretless env where an arbitrary placeholder value may be present. // AuthProvider should treat this as disabled, and the auth wrappers must not render // Clerk SignedOut/SignedIn components. + process.env.NEXT_PUBLIC_AUTH_MODE = "local"; process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = "placeholder"; + window.sessionStorage.clear(); - render( - - - - - , - ); + try { + render( + + + + + , + ); - expect(screen.getByText(/sign in to view the feed/i)).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: /local authentication/i }), + ).toBeInTheDocument(); + expect(screen.getByLabelText(/access token/i)).toBeInTheDocument(); + } finally { + process.env.NEXT_PUBLIC_AUTH_MODE = previousAuthMode; + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = previousPublishableKey; + window.sessionStorage.clear(); + } }); }); diff --git a/frontend/src/app/approvals/page.test.tsx b/frontend/src/app/approvals/page.test.tsx index f747b5bf..cb812fac 100644 --- a/frontend/src/app/approvals/page.test.tsx +++ b/frontend/src/app/approvals/page.test.tsx @@ -59,16 +59,30 @@ vi.mock("@clerk/nextjs", () => { describe("/approvals auth boundary", () => { it("renders without ClerkProvider runtime errors when publishable key is a placeholder", () => { + const previousAuthMode = process.env.NEXT_PUBLIC_AUTH_MODE; + const previousPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + + process.env.NEXT_PUBLIC_AUTH_MODE = "local"; process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = "placeholder"; + window.sessionStorage.clear(); - render( - - - - - , - ); + try { + render( + + + + + , + ); - expect(screen.getByText(/sign in to view approvals/i)).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: /local authentication/i }), + ).toBeInTheDocument(); + expect(screen.getByLabelText(/access token/i)).toBeInTheDocument(); + } finally { + process.env.NEXT_PUBLIC_AUTH_MODE = previousAuthMode; + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = previousPublishableKey; + window.sessionStorage.clear(); + } }); }); From 08341d26acdc89979946d21398482e5a73bc44f5 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 02:39:53 +0530 Subject: [PATCH 07/13] test(frontend): stabilize onboarding polling assertion --- frontend/src/components/BoardOnboardingChat.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/components/BoardOnboardingChat.test.tsx b/frontend/src/components/BoardOnboardingChat.test.tsx index 20487678..88dd5b9a 100644 --- a/frontend/src/components/BoardOnboardingChat.test.tsx +++ b/frontend/src/components/BoardOnboardingChat.test.tsx @@ -86,6 +86,9 @@ describe("BoardOnboardingChat polling", () => { ); await screen.findByText("What should we prioritize?"); + await waitFor(() => { + expect(screen.getByRole("button", { name: "Option A" })).toBeEnabled(); + }); const callsBeforeWait = getOnboardingMock.mock.calls.length; await act(async () => { From b35980618d46ed43d7374e9b3753da82681cb679 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 02:40:54 +0530 Subject: [PATCH 08/13] test(e2e): switch board task spec to local auth --- frontend/cypress/e2e/board_tasks.cy.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index 441b65cf..d368d587 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -2,8 +2,7 @@ describe("/boards/:id task board", () => { const apiBase = "**/api/v1"; - const email = - Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + const email = "local-auth-user@example.com"; const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); @@ -32,9 +31,11 @@ describe("/boards/:id task board", () => { cy.get('[aria-label="Edit task"]', { timeout: 20_000 }).should("be.visible"); } - it("auth negative: signed-out user is redirected to sign-in", () => { + it("auth negative: signed-out user is shown local auth login", () => { cy.visit("/boards/b1"); - cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + cy.contains("h1", /local authentication/i, { timeout: 30_000 }).should( + "be.visible", + ); }); it("happy path: renders tasks from snapshot and supports create + status update + delete (stubbed)", () => { @@ -204,10 +205,7 @@ describe("/boards/:id task board", () => { body: { items: [], total: 0, limit: 200, offset: 0 }, }).as("taskComments"); - cy.visit("/sign-in"); - cy.clerkLoaded(); - cy.clerkSignIn({ strategy: "email_code", identifier: email }); - + cy.loginWithLocalAuth(); cy.visit("/boards/b1"); cy.waitForAppLoaded(); From c03affe3e0a3954c09b3c7b728d1f9489bc71e5f Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 02:50:11 +0530 Subject: [PATCH 09/13] test(e2e): switch boards list spec to local auth --- frontend/cypress/e2e/boards_list.cy.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/cypress/e2e/boards_list.cy.ts b/frontend/cypress/e2e/boards_list.cy.ts index c68a8ab8..b1585cd2 100644 --- a/frontend/cypress/e2e/boards_list.cy.ts +++ b/frontend/cypress/e2e/boards_list.cy.ts @@ -2,7 +2,7 @@ describe("/boards", () => { const apiBase = "**/api/v1"; - const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + const email = "local-auth-user@example.com"; const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); @@ -14,9 +14,11 @@ describe("/boards", () => { Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); }); - it("auth negative: signed-out user is redirected to sign-in", () => { + it("auth negative: signed-out user is shown local auth login", () => { cy.visit("/boards"); - cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + cy.contains("h1", /local authentication/i, { timeout: 30_000 }).should( + "be.visible", + ); }); it("happy path: signed-in user sees boards list", () => { @@ -88,10 +90,7 @@ describe("/boards", () => { body: { items: [], total: 0, limit: 200, offset: 0 }, }).as("boardGroups"); - cy.visit("/sign-in"); - cy.clerkLoaded(); - cy.clerkSignIn({ strategy: "email_code", identifier: email }); - + cy.loginWithLocalAuth(); cy.visit("/boards"); cy.waitForAppLoaded(); From aa2ceba576e2dd551706d78527ecbfc4a5d136f6 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 02:50:57 +0530 Subject: [PATCH 10/13] test(e2e): scope board task SSE stubs to known endpoints --- frontend/cypress/e2e/board_tasks.cy.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index d368d587..cc397e71 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -15,12 +15,23 @@ describe("/boards/:id task board", () => { }); function stubEmptySse() { - // Any SSE endpoint should not hang the UI in tests. - cy.intercept("GET", `${apiBase}/**/stream*`, { + // Keep known board-related SSE endpoints quiet in tests. + 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 openEditTaskDialog() { From 33b705a9356d090363b85972793a0a751b61d1d8 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 02:59:15 +0530 Subject: [PATCH 11/13] test(e2e): remove force click for edit-task actionability --- frontend/cypress/e2e/board_tasks.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index cc397e71..567523a0 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -38,7 +38,7 @@ describe("/boards/:id task board", () => { cy.get('button[title="Edit task"]', { timeout: 20_000 }) .should("be.visible") .and("not.be.disabled") - .click({ force: true }); + .click(); cy.get('[aria-label="Edit task"]', { timeout: 20_000 }).should("be.visible"); } From 8b7c728f32aac15d57a5c53b9476457156ab97c0 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 03:01:15 +0530 Subject: [PATCH 12/13] test(e2e): replace forced clicks with actionability checks --- frontend/cypress/e2e/board_tasks.cy.ts | 31 +++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index 567523a0..5c5d9145 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -235,20 +235,26 @@ describe("/boards/:id task board", () => { // Open create task flow. // Board page uses an icon-only button with aria-label="New task". - cy.get('button[aria-label="New task"]').click({ force: true }); + cy.get('button[aria-label="New task"]') + .should("be.visible") + .and("not.be.disabled") + .click(); cy.contains('[role="dialog"]', "New task") .should("be.visible") .within(() => { cy.contains("label", "Title").parent().find("input").type("New task"); - cy.contains("button", /^Create task$/).click({ force: true }); + cy.contains("button", /^Create task$/) + .should("be.visible") + .and("not.be.disabled") + .click(); }); cy.wait(["@createTask"]); cy.contains("New task").should("be.visible"); // Open edit task dialog. - cy.contains("Inbox task").click({ force: true }); + cy.contains("Inbox task").scrollIntoView().should("be.visible").click(); cy.wait(["@taskComments"]); cy.contains(/task detail/i).should("be.visible"); openEditTaskDialog(); @@ -258,13 +264,16 @@ describe("/boards/:id task board", () => { cy.contains("label", "Status") .parent() .within(() => { - cy.get('[role="combobox"]').first().click({ force: true }); + cy.get('[role="combobox"]').first().should("be.visible").click(); }); }); - cy.contains("In progress").click({ force: true }); + cy.contains("In progress").should("be.visible").click(); - cy.contains("button", /save changes/i).click({ force: true }); + cy.contains("button", /save changes/i) + .should("be.visible") + .and("not.be.disabled") + .click(); cy.wait(["@updateTask"]); cy.get('[aria-label="Edit task"]').should("not.exist"); @@ -274,11 +283,17 @@ describe("/boards/:id task board", () => { // Delete task via delete dialog. cy.get('[aria-label="Edit task"]').within(() => { - cy.contains("button", /^Delete task$/).click({ force: true }); + cy.contains("button", /^Delete task$/) + .should("be.visible") + .and("not.be.disabled") + .click(); }); cy.get('[aria-label="Delete task"]').should("be.visible"); cy.get('[aria-label="Delete task"]').within(() => { - cy.contains("button", /^Delete task$/).click({ force: true }); + cy.contains("button", /^Delete task$/) + .should("be.visible") + .and("not.be.disabled") + .click(); }); cy.wait(["@deleteTask"]); From 9e4be7192e568cf12061f39d7dc7d824c9169f45 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 25 Feb 2026 03:14:56 +0530 Subject: [PATCH 13/13] test(e2e): ensure delete task button is visible and scrolls into view --- frontend/cypress/e2e/board_tasks.cy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts index 5c5d9145..c347c077 100644 --- a/frontend/cypress/e2e/board_tasks.cy.ts +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -284,6 +284,7 @@ describe("/boards/:id task board", () => { // Delete task via delete dialog. cy.get('[aria-label="Edit task"]').within(() => { cy.contains("button", /^Delete task$/) + .scrollIntoView() .should("be.visible") .and("not.be.disabled") .click(); @@ -291,6 +292,7 @@ describe("/boards/:id task board", () => { cy.get('[aria-label="Delete task"]').should("be.visible"); cy.get('[aria-label="Delete task"]').within(() => { cy.contains("button", /^Delete task$/) + .scrollIntoView() .should("be.visible") .and("not.be.disabled") .click();