Files
kandev/apps/web/e2e/tests/workflow/dynamic-workflow-completion.spec.ts

168 lines
5.7 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { SessionPage } from "../../pages/session-page";
async function createDynamicProfile(
apiClient: InstanceType<typeof import("../../helpers/api-client").ApiClient>,
candidateProfileId: string,
) {
const response = await apiClient.rawRequest("POST", "/api/v1/agents/dynamic/profiles", {
name: "Dynamic workflow completion",
dynamic: {
version: 1,
candidates: [
{
position: 0,
execution_profile_id: candidateProfileId,
enabled: true,
},
],
},
});
if (!response.ok) {
throw new Error(
`Dynamic profile creation failed (${response.status}): ${await response.text()}`,
);
}
return (await response.json()) as { id: string };
}
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: 45_000, message: `session ${sessionId} did not receive ${marker}` },
)
.toBe(true);
}
test.describe("Dynamic workflow completion", () => {
test("reuses the logical session after completion and reload", async ({
testPage,
apiClient,
backend,
seedData,
}) => {
test.setTimeout(120_000);
const releaseFeature = await backend.useEnv({
KANDEV_FEATURES_DYNAMIC_AGENT_ROUTING: "true",
});
let dynamicProfileId = "";
try {
const dynamicProfile = await createDynamicProfile(apiClient, seedData.agentProfileId);
dynamicProfileId = dynamicProfile.id;
const workflow = await apiClient.createWorkflow(
seedData.workspaceId,
"Dynamic completion reuse",
);
const source = await apiClient.createWorkflowStep(workflow.id, "Implement", 0, {
is_start_step: true,
agent_profile_id: dynamicProfile.id,
profile_session_start_policy: "reuse",
profile_session_end_policy: "park",
events: { on_turn_complete: [{ type: "move_to_next" }] },
});
const review = await apiClient.createWorkflowStep(workflow.id, "Review", 1, {
agent_profile_id: dynamicProfile.id,
profile_session_start_policy: "reuse",
profile_session_end_policy: "park",
});
const firstMarker = "dynamic-completion-first-turn";
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Dynamic completion reuse task",
dynamicProfile.id,
{
description: `e2e:message("${firstMarker}")`,
workflow_id: workflow.id,
workflow_step_id: source.id,
repository_ids: [seedData.repositoryId],
},
);
let sessionId = "";
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
const session = sessions.find(
(candidate) => candidate.agent_profile_id === dynamicProfile.id,
);
sessionId = session?.id ?? "";
return session?.state === "WAITING_FOR_INPUT";
},
{ timeout: 45_000, message: "Dynamic workflow session did not become ready" },
)
.toBe(true);
await waitForAgentMarker(apiClient, sessionId, firstMarker);
await expect
.poll(() => apiClient.getTask(task.id).then((current) => current.workflow_step_id), {
timeout: 30_000,
message: "completed turn did not advance to Review",
})
.toBe(review.id);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await testPage.reload();
await session.waitForLoad();
await expect(session.stepperStep("Review")).toHaveAttribute("aria-current", "step");
await session.sendMessage('e2e:message("dynamic-completion-second-turn")');
await waitForAgentMarker(apiClient, sessionId, "dynamic-completion-second-turn");
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
const completedSession = sessions.find((candidate) => candidate.id === sessionId);
return completedSession
? {
id: completedSession.id,
agent_profile_id: completedSession.agent_profile_id,
is_primary: completedSession.is_primary,
state: completedSession.state,
}
: null;
},
{
timeout: 45_000,
message: "completed second turn did not settle on the reused session",
},
)
.toEqual({
id: sessionId,
agent_profile_id: dynamicProfile.id,
is_primary: true,
state: "WAITING_FOR_INPUT",
});
const finalSessions = (await apiClient.listTaskSessions(task.id)).sessions;
expect(finalSessions).toHaveLength(1);
expect(finalSessions[0]).toMatchObject({
id: sessionId,
agent_profile_id: dynamicProfile.id,
is_primary: true,
state: "WAITING_FOR_INPUT",
});
expect((await apiClient.getTask(task.id)).workflow_step_id).toBe(review.id);
await expect(session.activeChat()).toContainText("dynamic-completion-second-turn");
} finally {
try {
if (dynamicProfileId) await apiClient.deleteAgentProfile(dynamicProfileId, true);
} finally {
await releaseFeature();
}
}
});
});