* 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:
@@ -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 });
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user