test(web): align workflow-agent-switch e2e tests with PR #743 (#751)

* test(web): align workflow-agent-switch e2e tests with PR #743 behavior

PR #743 split agent.boot_ready from agent.ready and added pinnedSessionId
focus protection, breaking four expectations in workflow-agent-switch.spec.ts:

- on_turn_start no longer fires from StartCreatedSession; it now fires from
  dispatchPromptAsync (user-message path). Test :204 now waits for the
  profileA agent to be ready and sends a message to trigger the transition.
- SSR-load setActiveSession pins the initial primary, so workflow profile
  switches no longer auto-yank the active dockview tab. Tests :420, :490,
  :546, :614 now assert on the primary star marker (which still moves to
  the new session) instead of the active-tab text.

Also fixes primaryStarInTab page-object helper: the IconStar is rendered as
a sibling of .dv-default-tab inside the [data-testid=session-tab-*] wrapper,
so the locator now anchors on the wrapper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(web): harden workflow-agent-switch assertions

Add message-visible guard after sendMessage to surface silent failures
early. Align primaryStarInTab timeout with profileBTab (45 s) where the
cascade latency budget was already set at 45 s.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(web): drive on_turn_start via chat message in kanban-flow test

The HTTP create-with-start_agent path no longer fires on_turn_start; only
WS user messages do (dispatchPromptAsync). Switch the kanban-flow test to
create the task in Backlog and send the initial prompt as a chat message
so on_turn_start triggers. Verifies the same workflow behavior — Backlog
→ In Progress on on_turn_start, invalid step_id in on_turn_complete is
gracefully skipped — without depending on the dialog "Start Agent" path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
João Salavisa
2026-05-01 00:25:08 +01:00
committed by GitHub
parent 7e8b4bb30e
commit 45a4f0b016
3 changed files with 103 additions and 71 deletions
+4 -3
View File
@@ -670,10 +670,11 @@ export class SessionPage {
return this.page.getByRole("alertdialog");
}
/** Primary star icon inside a dockview tab. */
/** Primary star icon inside a dockview session tab. The star is rendered as a
* sibling of `.dv-default-tab` inside the `data-testid="session-tab-<id>"`
* wrapper, so we anchor on that wrapper rather than `.dv-default-tab` itself. */
primaryStarInTab(text: string): Locator {
const tab = this.page.locator(`.dv-default-tab:has-text('${text}')`);
return tab.locator(".tabler-icon-star").first();
return this.sessionTabByText(text).locator(".tabler-icon-star").first();
}
/** "Move to next step" button in the chat status bar. */
@@ -202,13 +202,17 @@ test.describe("Workflow agent profile switching", () => {
});
test("on_turn_start transition to step with agent override uses correct profile", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(60_000);
const { profileA, profileB } = await createProfiles(apiClient);
// Backlog (on_turn_start: move_to_next) → Step1 (profileB, auto_start)
// Backlog (on_turn_start: move_to_next) → Step1 (profileB)
// Per PR #743, on_turn_start fires on user-message dispatch (not agent boot),
// and on_turn_start transitions skip on_enter — the user's prompt is delivered
// to the new step's profile via maybeSwitchSessionForProfile.
const workflow = await apiClient.createWorkflow(
seedData.workspaceId,
"Agent Switch OnTurnStart",
@@ -222,10 +226,8 @@ test.describe("Workflow agent profile switching", () => {
});
await apiClient.updateWorkflowStep(step1.id, {
agent_profile_id: profileB.id,
events: { on_enter: [{ type: "auto_start_agent" }] },
});
// Create task with profileA — on_turn_start should move to Step1 and switch to profileB
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"OnTurnStart Switch",
@@ -237,10 +239,35 @@ test.describe("Workflow agent profile switching", () => {
},
);
// Poll for sessions — on_turn_start creates an intermediate session then switches
const sessions = await pollSessions(apiClient, task.id, 2, 45_000);
const profileBSession = sessions.find((s) => s.agent_profile_id === profileB.id);
expect(profileBSession).toBeDefined();
// Wait for profileA session to be ready before sending the message
// that triggers on_turn_start.
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await expect(session.chat).toBeVisible({ timeout: 15_000 });
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.some((s) => s.state === "WAITING_FOR_INPUT");
},
{ timeout: 30_000, message: "Waiting for profileA agent to be ready" },
)
.toBe(true);
// User message → dispatchPromptAsync → on_turn_start → move_to_next → Step1 (profileB)
await session.sendMessage("hello");
await expect(session.chat.getByText("hello")).toBeVisible({ timeout: 10_000 });
// Profile switch produces (or reuses) a session with profileB on this task.
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.some((s) => s.agent_profile_id === profileB.id);
},
{ timeout: 30_000, message: "Waiting for profileB session after on_turn_start" },
)
.toBe(true);
});
test("auto-launches agent when step has profile override and prompt", async ({
@@ -414,10 +441,15 @@ test.describe("Workflow agent profile switching", () => {
/**
* Verifies the switchSessionForStep WS event fix: when a task is manually moved
* to a step with a different agent profile, the new session tab becomes the
* active dockview tab.
* to a step with a different agent profile, the new session is created and
* promoted to primary.
*
* Note: Per PR #743 (pinnedSessionId), navigating to /t/<taskId> calls
* setActiveSession which pins the initial primary. Workflow profile switches
* no longer yank focus from a pinned session, so this test asserts on the
* primary star indicator (not the active dockview tab).
*/
test("manual step move switches active session tab to new agent", async ({
test("manual step move promotes new agent session to primary", async ({
testPage,
apiClient,
seedData,
@@ -476,18 +508,22 @@ test.describe("Workflow agent profile switching", () => {
// Move to Step2 — triggers new session with profileB
await apiClient.moveTask(task.id, workflow.id, step2.id);
// The new "Profile B" tab should appear and become the active dockview tab
const activeProfileBTab = testPage.locator(
'.dv-tab.dv-active-tab [data-testid^="session-tab-"]',
);
await expect(activeProfileBTab).toContainText("Profile B", { timeout: 30_000 });
// The new "Profile B" tab should appear and the primary star should move to it
// (the active tab stays on Profile A because the SSR-load pin protects it).
const profileBTab = session.sessionTabByText("Profile B");
await expect(profileBTab).toBeVisible({ timeout: 30_000 });
await expect(session.primaryStarInTab("Profile B")).toBeVisible({ timeout: 30_000 });
});
/**
* Verifies that an on_turn_complete cascade that switches agent profiles
* also switches the active dockview session tab.
* promotes the new session to primary.
*
* Note: Per PR #743 (pinnedSessionId), the user's pinned tab is preserved.
* The active dockview tab stays on the pinned session; this test asserts on
* the primary star marker.
*/
test("on_turn_complete cascade switches active session tab to new agent", async ({
test("on_turn_complete cascade promotes new agent session to primary", async ({
testPage,
apiClient,
seedData,
@@ -530,11 +566,10 @@ test.describe("Workflow agent profile switching", () => {
// Move to Step1 → auto_start with profileA → on_turn_complete → Step2 (profileB)
await apiClient.moveTask(task.id, workflow.id, step1.id);
// After the cascade, the active tab should be "Profile B" (the final step's agent)
const activeProfileBTab = testPage.locator(
'.dv-tab.dv-active-tab [data-testid^="session-tab-"]',
);
await expect(activeProfileBTab).toContainText("Profile B", { timeout: 45_000 });
// After the cascade, the Profile B tab should appear and own the primary star.
const profileBTab = session.sessionTabByText("Profile B");
await expect(profileBTab).toBeVisible({ timeout: 45_000 });
await expect(session.primaryStarInTab("Profile B")).toBeVisible({ timeout: 45_000 });
});
/**
@@ -596,13 +631,12 @@ test.describe("Workflow agent profile switching", () => {
// Move to Step2
await apiClient.moveTask(task.id, workflow.id, step2.id);
// The Profile B tab should become active
const activeTab = testPage.locator('.dv-tab.dv-active-tab [data-testid^="session-tab-"]');
await expect(activeTab).toContainText("Profile B", { timeout: 30_000 });
// The star icon should be visible inside the active tab (no reload needed)
const starIcon = activeTab.locator("svg.tabler-icon-star");
await expect(starIcon).toBeVisible({ timeout: 5_000 });
// The Profile B tab should appear and own the primary star (no reload needed).
// Per PR #743, the active dockview tab stays pinned to Profile A — the star
// moving is what proves SetPrimarySession's WS broadcast landed.
const profileBTab = session.sessionTabByText("Profile B");
await expect(profileBTab).toBeVisible({ timeout: 30_000 });
await expect(session.primaryStarInTab("Profile B")).toBeVisible({ timeout: 30_000 });
});
/**
@@ -668,9 +702,12 @@ test.describe("Workflow agent profile switching", () => {
// Move to Step2 (no override) — should revert to profileA
await apiClient.moveTask(task.id, workflow.id, step2.id);
// The active tab should show Profile A (the task's default agent)
const activeTab = testPage.locator('.dv-tab.dv-active-tab [data-testid^="session-tab-"]');
await expect(activeTab).toContainText("Profile A", { timeout: 30_000 });
// A Profile A session should appear and own the primary star (no reload needed).
// Per PR #743, the user's pinned tab — Profile B from initial load — stays
// active; the primary marker is what proves the revert-to-default landed.
const profileATab = session.sessionTabByText("Profile A");
await expect(profileATab).toBeVisible({ timeout: 30_000 });
await expect(session.primaryStarInTab("Profile A")).toBeVisible({ timeout: 30_000 });
});
test("reset context checkbox is disabled when step has agent profile override", async ({
@@ -462,8 +462,9 @@ test.describe("Workflow automation", () => {
* "in-progress") which don't resolve to actual step UUIDs — they're no-ops.
*
* Flow:
* 1. Create task with description via dialog + Start Agent → task starts in Backlog
* 2. on_turn_start fires (move_to_next) → task moves to In Progress
* 1. Create task in Backlog (no start_agent) and navigate to the task page
* 2. Send a chat message — only the WS message path runs on_turn_start via
* dispatchPromptAsync, so the Backlog → In Progress move triggers here
* 3. Agent completes → on_turn_complete fires with move_to_step("review")
* which is gracefully skipped (invalid step_id) → task stays in In Progress
*/
@@ -506,48 +507,41 @@ test.describe("Workflow automation", () => {
enable_preview_on_click: false,
});
// --- Create task via dialog with description + Start Agent ---
const kanban = new KanbanPage(testPage);
await kanban.goto();
await kanban.createTaskButton.first().click();
const dialog = testPage.getByTestId("create-task-dialog");
await expect(dialog).toBeVisible();
await testPage.getByTestId("task-title-input").fill("Kanban Flow Task");
await testPage.getByTestId("task-description-input").fill("/e2e:simple-message");
const startBtn = testPage.getByTestId("submit-start-agent");
await expect(startBtn).toBeEnabled({ timeout: 15_000 });
await startBtn.click();
await expect(dialog).not.toBeVisible({ timeout: 10_000 });
// Task should be in In Progress: on_turn_start fires on the initial prompt
// and moves the task from Backlog → In Progress via move_to_next.
// After the agent completes, on_turn_complete's move_to_step("review") is
// gracefully skipped (invalid step_id), so the task stays in In Progress.
const card = kanban.taskCardInColumn("Kanban Flow Task", inProgressStep.id);
await expect(card).toBeVisible({ timeout: 15_000 });
// Navigate to session to verify agent completed successfully
await card.click();
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
// Create the task in Backlog without starting the agent — the prompt is
// sent below as a chat message so on_turn_start fires.
const task = await apiClient.createTask(seedData.workspaceId, "Kanban Flow Task", {
workflow_id: workflow.id,
workflow_step_id: backlogStep.id,
agent_profile_id: seedData.agentProfileId,
repository_ids: [seedData.repositoryId],
});
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await expect(session.chat).toBeVisible({ timeout: 15_000 });
// Agent response confirms the mock agent ran
await expect(session.chat.getByText("simple mock response", { exact: false })).toBeVisible({
// User message → dispatchPromptAsync → on_turn_start (move_to_next) →
// Backlog → In Progress.
await session.sendMessage("/e2e:simple-message");
await expect(session.chat.getByText("/e2e:simple-message")).toBeVisible({ timeout: 10_000 });
// Stepper reflects the move to In Progress.
await expect(session.stepperStep("In Progress")).toHaveAttribute("aria-current", "step", {
timeout: 30_000,
});
// Session transitions to idle — task is still functional
// Agent response confirms the mock agent ran; on_turn_complete then fires
// with move_to_step("review") — invalid step_id → gracefully skipped.
await expect(session.chat.getByText("simple mock response", { exact: false })).toBeVisible({
timeout: 30_000,
});
await expect(session.idleInput()).toBeVisible({ timeout: 15_000 });
// Stepper shows In Progress as current step
await expect(session.stepperStep("In Progress")).toHaveAttribute("aria-current", "step", {
timeout: 15_000,
});
// Task stays in In Progress on the kanban board (invalid step_id is a no-op).
const kanban = new KanbanPage(testPage);
await kanban.goto();
const card = kanban.taskCardInColumn("Kanban Flow Task", inProgressStep.id);
await expect(card).toBeVisible({ timeout: 15_000 });
});
/**