Files
kandev/apps/web/e2e/helpers/clarification.ts
T

155 lines
4.8 KiB
TypeScript

import { expect, type Page } from "@playwright/test";
import type { SeedData } from "../fixtures/test-base";
import type { ApiClient } from "./api-client";
import { SessionPage } from "../pages/session-page";
import { waitForSessionDone, waitForSessionState } from "./session";
type SeedOptions = {
/** Mock-agent scenario slug, e.g. "clarification" or "simple-message". */
scenario: string;
/**
* Wait for the chat to go idle after load. Leave false for blocking
* clarification scenarios where the agent parks on the MCP call and the idle
* input never appears until the question is answered.
*/
waitForIdle?: boolean;
};
/**
* Create a task + session running a mock-agent scenario, wait for blocking
* scenarios to reach their causal backend state, navigate to it, and return a
* ready SessionPage. Shared by the clarification specs (overlay, resize, and
* mobile) so the seed → wait → navigate → waitForLoad flow lives in one place.
*/
export async function seedClarificationSession(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
title: string,
{ scenario, waitForIdle = false }: SeedOptions,
): Promise<SessionPage> {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
title,
seedData.agentProfileId,
{
description: `/e2e:${scenario}`,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!task.session_id) throw new Error("createTaskWithAgent did not return a session_id");
if (!waitForIdle) {
await waitForSessionState(apiClient, {
taskId: task.id,
sessionId: task.session_id,
expectedState: "WAITING_FOR_INPUT",
message: "clarification session should reach its blocking state before navigation",
timeout: 60_000,
});
}
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
if (waitForIdle) await session.waitForChatIdle();
return session;
}
export type SecondaryClarificationTask = {
id: string;
title: string;
primarySessionId: string;
clarificationSessionId: string;
};
/**
* Create a task with a clean primary session and a newer secondary session
* blocked on clarification. The original session is restored as primary so
* task navigation must follow pending ownership instead of its normal default.
*/
export async function seedSecondaryClarificationTask(
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<SecondaryClarificationTask> {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
title,
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
executor_profile_id: seedData.worktreeExecutorProfileId,
},
);
if (!task.session_id) throw new Error("secondary clarification setup has no primary session");
await waitForSessionDone(
apiClient,
task.id,
task.session_id,
"primary session should finish before secondary clarification starts",
60_000,
);
const secondary = await apiClient.launchSession(
{
task_id: task.id,
agent_profile_id: seedData.agentProfileId,
executor_profile_id: seedData.worktreeExecutorProfileId,
workflow_step_id: seedData.startStepId,
prompt: "/e2e:clarification",
},
60_000,
);
await waitForSessionState(apiClient, {
taskId: task.id,
sessionId: secondary.session_id,
expectedState: "WAITING_FOR_INPUT",
message: "secondary session should wait on clarification",
timeout: 60_000,
});
await apiClient.setPrimarySession(task.session_id);
await expect
.poll(async () => (await apiClient.getTask(task.id)).primary_session_id ?? null, {
message: "clean original session should remain primary",
timeout: 30_000,
})
.toBe(task.session_id);
await expect
.poll(
async () => {
const { tasks } = await apiClient.listTasks(seedData.workspaceId);
return tasks.find((candidate) => candidate.id === task.id)?.status_summary?.pending_action;
},
{ message: "task summary should advertise secondary clarification", timeout: 30_000 },
)
.toBe("clarification");
return {
id: task.id,
title,
primarySessionId: task.session_id,
clarificationSessionId: secondary.session_id,
};
}
export async function activeSessionId(page: Page): Promise<string | null> {
return page.evaluate(
() =>
(
window as unknown as {
__KANDEV_E2E_STORE__?: { getState: () => { tasks: { activeSessionId: string | null } } };
}
).__KANDEV_E2E_STORE__?.getState().tasks.activeSessionId ?? null,
);
}