Files
kandev/apps/web/e2e/tests/workflow/workflow-session-targeting.spec.ts
2026-09-11 18:25:23 +01:00

387 lines
15 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { SessionPage } from "../../pages/session-page";
import { WorkflowSettingsPage } from "../../pages/workflow-settings-page";
import {
createWorkflowAgentProfiles,
waitForWorkflowProfileSession,
} from "./workflow-agent-switch-helpers";
async function waitForAgentMarker(
apiClient: InstanceType<typeof import("../../helpers/api-client").ApiClient>,
sessionId: string,
marker: string,
) {
await expect
.poll(
async () => {
const { messages } = await apiClient.listSessionMessages(sessionId);
return messages.some(
(message) => message.author_type === "agent" && message.content.includes(marker),
);
},
{ timeout: 30_000, message: `session ${sessionId} did not receive ${marker}` },
)
.toBe(true);
}
test.describe("Workflow session targeting", () => {
test("authors initial and source-session choices on desktop", async ({
testPage,
apiClient,
seedData,
}) => {
const { profileA, profileB } = await createWorkflowAgentProfiles(apiClient);
const workflow = await apiClient.createWorkflow(
seedData.workspaceId,
"Desktop Session Targets",
);
await apiClient.createWorkflowStep(workflow.id, "Plan", 0, { is_start_step: true });
const implement = await apiClient.createWorkflowStep(workflow.id, "Implement", 1);
const review = await apiClient.createWorkflowStep(workflow.id, "Review", 2);
await apiClient.updateWorkflowStep(implement.id, { agent_profile_id: profileA.id });
await apiClient.updateWorkflowStep(review.id, { agent_profile_id: profileB.id });
const settings = new WorkflowSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
const card = await settings.findWorkflowCard("Desktop Session Targets");
await settings.selectStep(card, "Review");
const selector = settings.stepAgentProfileSelect(card);
await selector.click();
await expect(testPage.getByTestId(`${review.id}-session-target-initial`)).toBeVisible();
await expect(
testPage.getByTestId(`${review.id}-session-target-step-${implement.id}`),
).toBeVisible();
const lifecycle = testPage.locator(
`[data-testid="${review.id}-profile-session-lifecycle-select"]:visible`,
);
await expect(lifecycle).toBeVisible();
const search = testPage.getByPlaceholder("Search agent profiles...");
await search.fill("does-not-exist");
await expect(testPage.getByText("Profile not found.", { exact: true })).toBeVisible();
await expect(lifecycle).toBeVisible();
await search.fill("");
await testPage.getByTestId(`${review.id}-session-target-initial`).click();
await expect(selector).toContainText("Initial agent session");
await selector.click();
await lifecycle.click();
await testPage.getByTestId(`${review.id}-profile-session-start-new`).click();
await testPage.getByTestId(`${review.id}-profile-session-end-park`).click();
await testPage.keyboard.press("Escape");
await settings.saveChanges();
const savedReview = (await apiClient.listWorkflowSteps(workflow.id)).steps.find(
(step) => step.id === review.id,
);
expect(savedReview).toMatchObject({
session_target: { kind: "initial" },
profile_session_start_policy: "new",
profile_session_end_policy: "park",
});
expect(savedReview?.agent_profile_id ?? "").toBe("");
await settings.goto(seedData.workspaceId);
const reloadedCard = await settings.findWorkflowCard("Desktop Session Targets");
await settings.selectStep(reloadedCard, "Review");
await expect(settings.stepAgentProfileSelect(reloadedCard)).toContainText(
"Initial agent session",
);
});
test("delivers reuse and fresh initial targets to the intended conversations", async ({
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const { profileA, profileB } = await createWorkflowAgentProfiles(apiClient);
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Initial Target Runtime");
const plan = await apiClient.createWorkflowStep(workflow.id, "Plan", 0);
const luna = await apiClient.createWorkflowStep(workflow.id, "Luna", 1);
const reviewReuse = await apiClient.createWorkflowStep(workflow.id, "Review reuse", 2);
const reviewFresh = await apiClient.createWorkflowStep(workflow.id, "Review fresh", 3);
await apiClient.updateWorkflowStep(plan.id, {
agent_profile_id: profileA.id,
profile_session_end_policy: "park",
events: { on_enter: [{ type: "auto_start_agent" }] },
});
await apiClient.updateWorkflowStep(luna.id, {
agent_profile_id: profileB.id,
profile_session_end_policy: "park",
events: { on_enter: [{ type: "auto_start_agent" }] },
});
await apiClient.updateWorkflowStep(reviewReuse.id, {
session_target: { kind: "initial" },
prompt: 'e2e:message("initial-target-reuse")',
profile_session_end_policy: "park",
events: { on_enter: [{ type: "auto_start_agent" }] },
});
await apiClient.updateWorkflowStep(reviewFresh.id, {
session_target: { kind: "initial" },
profile_session_start_policy: "new",
prompt: 'e2e:message("initial-target-fresh")',
events: { on_enter: [{ type: "auto_start_agent" }] },
});
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Initial target runtime",
profileA.id,
{
workflow_id: workflow.id,
workflow_step_id: plan.id,
repository_ids: [seedData.repositoryId],
},
);
const initialSessionId = await waitForWorkflowProfileSession(apiClient, task.id, profileA.id);
await apiClient.moveTask(task.id, workflow.id, luna.id);
const lunaSessionId = await waitForWorkflowProfileSession(apiClient, task.id, profileB.id);
await apiClient.moveTask(task.id, workflow.id, reviewReuse.id);
await waitForAgentMarker(apiClient, initialSessionId, "initial-target-reuse");
const afterReuse = (await apiClient.listTaskSessions(task.id)).sessions;
expect(afterReuse.filter((session) => session.agent_profile_id === profileA.id)).toHaveLength(
1,
);
expect(afterReuse.find((session) => session.id === initialSessionId)?.is_primary).toBe(true);
await apiClient.moveTask(task.id, workflow.id, reviewFresh.id);
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return sessions.find(
(session) =>
session.agent_profile_id === profileA.id && session.id !== initialSessionId,
)?.id;
},
{ timeout: 30_000, message: "fresh initial-target session was not created" },
)
.toBeDefined();
const finalSessions = (await apiClient.listTaskSessions(task.id)).sessions;
const freshSession = finalSessions.find(
(session) => session.agent_profile_id === profileA.id && session.id !== initialSessionId,
);
expect(freshSession).toBeDefined();
await waitForAgentMarker(apiClient, freshSession!.id, "initial-target-fresh");
expect(freshSession!.id).not.toBe(initialSessionId);
expect(finalSessions.find((session) => session.id === lunaSessionId)?.state).toBe(
"WAITING_FOR_INPUT",
);
});
test("honors same-profile new sessions from the task topbar", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const { profileA } = await createWorkflowAgentProfiles(apiClient);
const workflow = await apiClient.createWorkflow(
seedData.workspaceId,
"Same Profile Topbar Session",
);
const source = await apiClient.createWorkflowStep(workflow.id, "Source", 0, {
agent_profile_id: profileA.id,
profile_session_end_policy: "park",
});
const destination = await apiClient.createWorkflowStep(workflow.id, "Destination", 1, {
agent_profile_id: profileA.id,
profile_session_start_policy: "new",
profile_session_end_policy: "park",
});
const marker = "same-profile-new-topbar";
await apiClient.updateWorkflowStep(destination.id, {
prompt: `e2e:message("${marker}")`,
});
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Same profile topbar session",
profileA.id,
{
workflow_id: workflow.id,
workflow_step_id: source.id,
repository_ids: [seedData.repositoryId],
},
);
const sourceSessionId = await waitForWorkflowProfileSession(apiClient, task.id, profileA.id);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.moveToWorkflowStep({ id: destination.id, name: "Destination" });
let destinationSessionId = "";
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
const destinationSession = sessions.find(
(candidate) => candidate.id !== sourceSessionId && candidate.is_primary,
);
destinationSessionId = destinationSession?.id ?? "";
return destinationSessionId;
},
{ timeout: 30_000, message: "same-profile topbar move did not create a primary session" },
)
.not.toBe("");
await expect
.poll(async () => (await apiClient.getTask(task.id)).workflow_step_id, {
timeout: 15_000,
})
.toBe(destination.id);
await waitForAgentMarker(apiClient, destinationSessionId, marker);
const movedSessions = (await apiClient.listTaskSessions(task.id)).sessions;
expect(movedSessions).toHaveLength(2);
expect(movedSessions.find((candidate) => candidate.id === destinationSessionId)).toMatchObject({
agent_profile_id: profileA.id,
is_primary: true,
});
expect(movedSessions.find((candidate) => candidate.id === sourceSessionId)?.state).toBe(
"WAITING_FOR_INPUT",
);
await expect(session.activeChat()).toContainText(marker, { timeout: 15_000 });
await testPage.reload();
await session.waitForLoad();
const reloadedSessions = (await apiClient.listTaskSessions(task.id)).sessions;
expect(
reloadedSessions.find((candidate) => candidate.id === destinationSessionId),
).toMatchObject({
agent_profile_id: profileA.id,
is_primary: true,
});
await expect(session.activeChat()).toContainText(marker, { timeout: 15_000 });
});
test("delivers a source-step target to its recorded conversation", async ({
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const { agentId, profileA, profileB } = await createWorkflowAgentProfiles(apiClient);
const profileC = await apiClient.createAgentProfile(agentId, "Profile C (medium)", {
model: "mock-fast",
});
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Source Target Runtime");
const plan = await apiClient.createWorkflowStep(workflow.id, "Plan", 0);
const implement = await apiClient.createWorkflowStep(workflow.id, "Implement", 1);
const verify = await apiClient.createWorkflowStep(workflow.id, "Verify", 2);
const review = await apiClient.createWorkflowStep(workflow.id, "Review", 3);
await apiClient.updateWorkflowStep(plan.id, {
agent_profile_id: profileA.id,
profile_session_end_policy: "park",
events: { on_enter: [{ type: "auto_start_agent" }] },
});
await apiClient.updateWorkflowStep(implement.id, {
agent_profile_id: profileB.id,
profile_session_end_policy: "park",
events: { on_enter: [{ type: "auto_start_agent" }] },
});
await apiClient.updateWorkflowStep(verify.id, {
agent_profile_id: profileC.id,
profile_session_end_policy: "park",
events: { on_enter: [{ type: "auto_start_agent" }] },
});
await apiClient.updateWorkflowStep(review.id, {
session_target: { kind: "step", step_id: implement.id },
prompt: 'e2e:message("source-target-reuse")',
profile_session_end_policy: "park",
events: { on_enter: [{ type: "auto_start_agent" }] },
});
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Source target runtime",
profileA.id,
{
workflow_id: workflow.id,
workflow_step_id: plan.id,
repository_ids: [seedData.repositoryId],
},
);
await waitForWorkflowProfileSession(apiClient, task.id, profileA.id);
await apiClient.moveTask(task.id, workflow.id, implement.id);
const implementSessionId = await waitForWorkflowProfileSession(apiClient, task.id, profileB.id);
await apiClient.moveTask(task.id, workflow.id, verify.id);
await waitForWorkflowProfileSession(apiClient, task.id, profileC.id);
await apiClient.moveTask(task.id, workflow.id, review.id);
await waitForAgentMarker(apiClient, implementSessionId, "source-target-reuse");
const finalSessions = (await apiClient.listTaskSessions(task.id)).sessions;
expect(
finalSessions.filter((session) => session.agent_profile_id === profileB.id),
).toHaveLength(1);
expect(finalSessions.find((session) => session.id === implementSessionId)?.is_primary).toBe(
true,
);
expect(finalSessions.find((session) => session.agent_profile_id === profileC.id)?.state).toBe(
"WAITING_FOR_INPUT",
);
});
test("surfaces a repair state when a source step is moved after its dependent", async ({
testPage,
apiClient,
seedData,
}) => {
const { profileA } = await createWorkflowAgentProfiles(apiClient);
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Repair Session Target");
const source = await apiClient.createWorkflowStep(workflow.id, "Implement", 0);
const review = await apiClient.createWorkflowStep(workflow.id, "Review", 1);
await apiClient.updateWorkflowStep(source.id, { agent_profile_id: profileA.id });
await apiClient.updateWorkflowStep(review.id, {
session_target: { kind: "step", step_id: source.id },
});
const settings = new WorkflowSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
const card = await settings.findWorkflowCard("Repair Session Target");
await settings.selectStep(card, "Review");
const sourceNode = settings.stepNodeByName(card, "Implement");
const reviewNode = settings.stepNodeByName(card, "Review");
const sourceHandle = sourceNode.locator("button").first();
const sourceBox = await sourceHandle.boundingBox();
const reviewBox = await reviewNode.boundingBox();
expect(sourceBox).not.toBeNull();
expect(reviewBox).not.toBeNull();
await testPage.mouse.move(
sourceBox!.x + sourceBox!.width / 2,
sourceBox!.y + sourceBox!.height / 2,
);
await testPage.mouse.down();
await testPage.mouse.move(
reviewBox!.x + reviewBox!.width / 2,
reviewBox!.y + reviewBox!.height / 2,
{
steps: 8,
},
);
await testPage.mouse.up();
await expect(settings.stepNodeByName(card, "Review")).toHaveAttribute(
"data-workflow-invalid-session-target",
"true",
);
await expect(testPage.getByTestId("workflow-session-target-repair")).toBeVisible();
await expect(
settings.floatingSave.getByRole("button", { name: "Save changes" }),
).toBeDisabled();
await testPage.getByTestId("workflow-session-target-clear").click();
await settings.saveChanges();
const savedReview = (await apiClient.listWorkflowSteps(workflow.id)).steps.find(
(step) => step.id === review.id,
);
expect(savedReview?.session_target ?? null).toBeNull();
});
});