693 lines
26 KiB
TypeScript
693 lines
26 KiB
TypeScript
import { test, expect } from "../../fixtures/test-base";
|
|
import { SessionPage } from "../../pages/session-page";
|
|
import type { ApiClient } from "../../helpers/api-client";
|
|
import type { SeedData } from "../../fixtures/test-base";
|
|
|
|
/**
|
|
* Config management via the dedicated config-chat endpoint.
|
|
*
|
|
* These tests verify the config-mode MCP flow: config chat sessions are
|
|
* created via POST /workspaces/:id/config-chat, which sets config_mode: true
|
|
* in session metadata. The agent then receives config-mode MCP tools
|
|
* (workflow, agent, and MCP management) that are NOT available in regular
|
|
* task sessions.
|
|
*
|
|
* The mock agent uses `e2e:mcp:kandev:<tool>(<json_args>)` script commands
|
|
* to call real MCP tools through the agentctl MCP server.
|
|
*/
|
|
|
|
/** Creates a config chat session via the dedicated config-chat endpoint. */
|
|
function startConfigSession(apiClient: ApiClient, seedData: SeedData, prompt: string) {
|
|
return apiClient.startConfigChat(seedData.workspaceId, seedData.agentProfileId, prompt);
|
|
}
|
|
|
|
/** Navigate to session page and wait for the final marker message. */
|
|
async function runAndWait(
|
|
testPage: import("@playwright/test").Page,
|
|
taskId: string,
|
|
marker: string,
|
|
) {
|
|
await testPage.goto(`/t/${taskId}`);
|
|
const page = new SessionPage(testPage);
|
|
await page.waitForLoad();
|
|
await expect(page.activeChat().getByText(marker, { exact: true })).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
return page;
|
|
}
|
|
|
|
/**
|
|
* After a turn completes, consecutive tool calls collapse into a "{N} tool
|
|
* call(s)" group header. Click each header so the per-tool-call titles below
|
|
* are rendered and assertions can target them.
|
|
*/
|
|
async function expandToolCallGroups(page: SessionPage) {
|
|
const headers = page.chat.getByRole("button", { name: /\d+ tool calls?$/ });
|
|
const count = await headers.count();
|
|
for (let i = 0; i < count; i++) {
|
|
await headers.nth(i).click();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Workflow management
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Config-mode MCP — workflow management", () => {
|
|
test("agent can list workspaces and workflows", async ({ testPage, apiClient, seedData }) => {
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Listing workspaces...")',
|
|
"e2e:mcp:kandev:list_workspaces_kandev({})",
|
|
`e2e:mcp:kandev:list_workflows_kandev({"workspace_id":"${seedData.workspaceId}"})`,
|
|
'e2e:message("Done listing")',
|
|
].join("\n"),
|
|
);
|
|
|
|
const page = await runAndWait(testPage, session.task_id, "Done listing");
|
|
await expandToolCallGroups(page);
|
|
await expect(page.chat.getByText("Kandev: List Workspaces")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.chat.getByText("Kandev: List Workflows")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("agent can create and list workflow steps", async ({ testPage, apiClient, seedData }) => {
|
|
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Step CRUD Workflow");
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Creating step...")',
|
|
`e2e:mcp:kandev:create_workflow_step_kandev({"workflow_id":"${workflow.id}","name":"QA Review","position":0})`,
|
|
`e2e:mcp:kandev:list_workflow_steps_kandev({"workflow_id":"${workflow.id}"})`,
|
|
'e2e:message("Steps listed")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Steps listed");
|
|
|
|
// Verify via API
|
|
const { steps } = await apiClient.listWorkflowSteps(workflow.id);
|
|
const qaStep = steps.find((s) => s.name === "QA Review");
|
|
expect(qaStep).toBeTruthy();
|
|
});
|
|
|
|
test("agent can create a step with all optional fields", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Full Fields Workflow");
|
|
|
|
const createArgs = JSON.stringify({
|
|
workflow_id: workflow.id,
|
|
name: "Deploy",
|
|
position: 0,
|
|
color: "#22c55e",
|
|
prompt: "Deploy to production",
|
|
is_start_step: true,
|
|
allow_manual_move: true,
|
|
show_in_command_panel: true,
|
|
events: {
|
|
on_enter: [{ type: "auto_start_agent" }],
|
|
},
|
|
});
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Creating step with all fields...")',
|
|
`e2e:mcp:kandev:create_workflow_step_kandev(${createArgs})`,
|
|
'e2e:message("Step created")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Step created");
|
|
|
|
// Verify all fields via API
|
|
const { steps } = await apiClient.listWorkflowSteps(workflow.id);
|
|
const deploy = steps.find((s) => s.name === "Deploy");
|
|
expect(deploy).toBeTruthy();
|
|
expect(deploy!.color).toBe("#22c55e");
|
|
expect(deploy!.prompt).toBe("Deploy to production");
|
|
expect(deploy!.is_start_step).toBe(true);
|
|
expect(deploy!.allow_manual_move).toBe(true);
|
|
expect(deploy!.show_in_command_panel).toBe(true);
|
|
expect(deploy!.events?.on_enter).toEqual([{ type: "auto_start_agent" }]);
|
|
});
|
|
|
|
test("agent can create a workflow", async ({ testPage, apiClient, seedData }) => {
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Creating workflow...")',
|
|
`e2e:mcp:kandev:create_workflow_kandev({"workspace_id":"${seedData.workspaceId}","name":"E2E Workflow","description":"Created by E2E test"})`,
|
|
'e2e:message("Workflow created")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Workflow created");
|
|
|
|
// Verify workflow was created via API.
|
|
// Filter by both name and description since the seed workflow shares the same name.
|
|
const { workflows } = await apiClient.listWorkflows(seedData.workspaceId);
|
|
const created = workflows.find(
|
|
(w) => w.name === "E2E Workflow" && w.description === "Created by E2E test",
|
|
);
|
|
expect(created).toBeTruthy();
|
|
});
|
|
|
|
test("agent can update a workflow", async ({ testPage, apiClient, seedData }) => {
|
|
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Before Update");
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Updating workflow...")',
|
|
`e2e:mcp:kandev:update_workflow_kandev({"workflow_id":"${workflow.id}","name":"After Update","description":"Updated description"})`,
|
|
'e2e:message("Workflow updated")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Workflow updated");
|
|
|
|
// Verify via API
|
|
const { workflows } = await apiClient.listWorkflows(seedData.workspaceId);
|
|
const updated = workflows.find((w) => w.id === workflow.id);
|
|
expect(updated?.name).toBe("After Update");
|
|
expect(updated?.description).toBe("Updated description");
|
|
});
|
|
|
|
test("agent can delete a workflow", async ({ testPage, apiClient, seedData }) => {
|
|
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Deletable Workflow");
|
|
// Create a bumper workflow so the config-chat session (which picks the newest
|
|
// workflow via ORDER BY created_at DESC) doesn't land on the deletable one.
|
|
// Otherwise ON DELETE CASCADE would wipe the config task/session mid-run.
|
|
await apiClient.createWorkflow(seedData.workspaceId, "Config Anchor");
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Deleting workflow...")',
|
|
`e2e:mcp:kandev:delete_workflow_kandev({"workflow_id":"${workflow.id}"})`,
|
|
'e2e:message("Workflow deleted")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Workflow deleted");
|
|
|
|
// Verify workflow was deleted via API
|
|
const { workflows } = await apiClient.listWorkflows(seedData.workspaceId);
|
|
expect(workflows.find((w) => w.id === workflow.id)).toBeUndefined();
|
|
});
|
|
|
|
test("agent can update a workflow step", async ({ testPage, apiClient, seedData }) => {
|
|
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Update Step Workflow");
|
|
const step = await apiClient.createWorkflowStep(workflow.id, "Draft", 0);
|
|
|
|
const updateArgs = JSON.stringify({
|
|
step_id: step.id,
|
|
name: "In Review",
|
|
color: "#3b82f6",
|
|
allow_manual_move: true,
|
|
show_in_command_panel: true,
|
|
auto_archive_after_hours: 48,
|
|
events: {
|
|
on_enter: [{ type: "enable_plan_mode" }],
|
|
on_turn_complete: [{ type: "move_to_next" }],
|
|
},
|
|
});
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Updating step...")',
|
|
`e2e:mcp:kandev:update_workflow_step_kandev(${updateArgs})`,
|
|
'e2e:message("Step updated")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Step updated");
|
|
|
|
// Verify via API
|
|
const { steps } = await apiClient.listWorkflowSteps(workflow.id);
|
|
const updated = steps.find((s) => s.id === step.id);
|
|
expect(updated?.name).toBe("In Review");
|
|
expect(updated?.color).toBe("#3b82f6");
|
|
expect(updated?.allow_manual_move).toBe(true);
|
|
expect(updated?.show_in_command_panel).toBe(true);
|
|
expect(updated?.auto_archive_after_hours).toBe(48);
|
|
expect(updated?.events?.on_enter).toEqual([{ type: "enable_plan_mode" }]);
|
|
expect(updated?.events?.on_turn_complete).toEqual([{ type: "move_to_next" }]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Agent management
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Config-mode MCP — agent management", () => {
|
|
test("agent can list agents and profiles", async ({ testPage, apiClient, seedData }) => {
|
|
const { agents } = await apiClient.listAgents();
|
|
const agent = agents[0];
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Listing agents...")',
|
|
"e2e:mcp:kandev:list_agents_kandev({})",
|
|
`e2e:mcp:kandev:list_agent_profiles_kandev({"agent_id":"${agent.id}"})`,
|
|
'e2e:message("Agents listed")',
|
|
].join("\n"),
|
|
);
|
|
|
|
const page = await runAndWait(testPage, session.task_id, "Agents listed");
|
|
await expandToolCallGroups(page);
|
|
await expect(page.chat.getByText("Kandev: List Agents")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.chat.getByText("Kandev: List Agent Profiles")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
});
|
|
|
|
test("agent can create and delete an agent profile", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const { agents } = await apiClient.listAgents();
|
|
const agent = agents[0];
|
|
const initialProfileCount = (agent.profiles ?? []).length;
|
|
|
|
// Create a new profile via MCP tool
|
|
const createSession = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Creating profile...")',
|
|
`e2e:mcp:kandev:create_agent_profile_kandev({"agent_id":"${agent.id}","name":"E2E Created Profile","model":"claude-sonnet-4-5-20250514"})`,
|
|
'e2e:message("Profile created")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, createSession.task_id, "Profile created");
|
|
|
|
// Verify profile was created via API
|
|
const { agents: afterCreate } = await apiClient.listAgents();
|
|
const agentAfterCreate = afterCreate.find((a) => a.id === agent.id);
|
|
const newProfiles = (agentAfterCreate?.profiles ?? []).filter(
|
|
(p) => p.name === "E2E Created Profile",
|
|
);
|
|
expect(newProfiles.length).toBe(1);
|
|
expect(newProfiles[0].model).toBe("claude-sonnet-4-5-20250514");
|
|
|
|
// Delete the profile via MCP tool
|
|
const newProfileId = newProfiles[0].id;
|
|
const deleteSession = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Deleting profile...")',
|
|
`e2e:mcp:kandev:delete_agent_profile_kandev({"profile_id":"${newProfileId}"})`,
|
|
'e2e:message("Profile deleted")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, deleteSession.task_id, "Profile deleted");
|
|
|
|
// Verify profile was deleted via API
|
|
const { agents: afterDelete } = await apiClient.listAgents();
|
|
const agentAfterDelete = afterDelete.find((a) => a.id === agent.id);
|
|
expect((agentAfterDelete?.profiles ?? []).length).toBe(initialProfileCount);
|
|
expect((agentAfterDelete?.profiles ?? []).find((p) => p.id === newProfileId)).toBeUndefined();
|
|
});
|
|
|
|
test("agent can update an agent", async ({ testPage, apiClient, seedData }) => {
|
|
const { agents } = await apiClient.listAgents();
|
|
const agent = agents[0];
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Updating agent...")',
|
|
`e2e:mcp:kandev:update_agent_kandev({"agent_id":"${agent.id}","supports_mcp":true})`,
|
|
'e2e:message("Agent updated")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Agent updated");
|
|
|
|
// Verify via API
|
|
const { agents: updated } = await apiClient.listAgents();
|
|
const updatedAgent = updated.find((a) => a.id === agent.id);
|
|
expect(updatedAgent?.supports_mcp).toBe(true);
|
|
});
|
|
|
|
test("agent can update an agent profile name", async ({ testPage, apiClient, seedData }) => {
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Updating profile...")',
|
|
`e2e:mcp:kandev:update_agent_profile_kandev({"profile_id":"${seedData.agentProfileId}","name":"Renamed Profile"})`,
|
|
'e2e:message("Profile updated")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Profile updated");
|
|
|
|
// Verify via API
|
|
const { agents } = await apiClient.listAgents();
|
|
const profile = agents
|
|
.flatMap((a) => a.profiles ?? [])
|
|
.find((p) => p.id === seedData.agentProfileId);
|
|
expect(profile?.name).toBe("Renamed Profile");
|
|
});
|
|
|
|
test("agent can update agent profile model via MCP", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
// Legacy `auto_approve` / `dangerously_skip_permissions` fields were
|
|
// removed when profile permission stance moved to ACP session modes.
|
|
// This test now verifies only the model update round-trip through the
|
|
// config-mode MCP tool.
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Updating profile settings...")',
|
|
`e2e:mcp:kandev:update_agent_profile_kandev({"profile_id":"${seedData.agentProfileId}","model":"claude-sonnet-4-5-20250514"})`,
|
|
'e2e:message("Profile settings updated")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Profile settings updated");
|
|
|
|
// Verify via API
|
|
const { agents } = await apiClient.listAgents();
|
|
const profile = agents
|
|
.flatMap((a) => a.profiles ?? [])
|
|
.find((p) => p.id === seedData.agentProfileId);
|
|
expect(profile?.model).toBe("claude-sonnet-4-5-20250514");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MCP server configuration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Config-mode MCP — MCP server configuration", () => {
|
|
test("agent can get and update MCP config", async ({ testPage, apiClient, seedData }) => {
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Reading MCP config...")',
|
|
`e2e:mcp:kandev:get_mcp_config_kandev({"profile_id":"${seedData.agentProfileId}"})`,
|
|
`e2e:mcp:kandev:update_mcp_config_kandev({"profile_id":"${seedData.agentProfileId}","enabled":true,"servers":{"test-server":{"command":"node","args":["server.js"]}}})`,
|
|
'e2e:message("MCP config updated")',
|
|
].join("\n"),
|
|
);
|
|
|
|
const page = await runAndWait(testPage, session.task_id, "MCP config updated");
|
|
await expandToolCallGroups(page);
|
|
await expect(page.chat.getByText("Kandev: Get MCP Config")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.chat.getByText("Kandev: Update MCP Config")).toBeVisible({ timeout: 10_000 });
|
|
|
|
// Verify via API
|
|
const config = await apiClient.getAgentProfileMcpConfig(seedData.agentProfileId);
|
|
expect(config.enabled).toBe(true);
|
|
expect(config.servers).toHaveProperty("test-server");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task management
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Config-mode MCP — task management", () => {
|
|
test("agent can list tasks in a workflow", async ({ testPage, apiClient, seedData }) => {
|
|
// Create a task so there's something to list
|
|
await apiClient.createTask(seedData.workspaceId, "Listable Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Listing tasks...")',
|
|
`e2e:mcp:kandev:list_tasks_kandev({"workflow_id":"${seedData.workflowId}"})`,
|
|
'e2e:message("Tasks listed")',
|
|
].join("\n"),
|
|
);
|
|
|
|
const page = await runAndWait(testPage, session.task_id, "Tasks listed");
|
|
await expandToolCallGroups(page);
|
|
await expect(page.chat.getByText("Kandev: List Tasks").first()).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
});
|
|
|
|
test("agent can move a task to a different step", async ({ testPage, apiClient, seedData }) => {
|
|
// Find a target step that is NOT the start step
|
|
const targetStep = seedData.steps.find((s) => s.id !== seedData.startStepId);
|
|
expect(targetStep).toBeTruthy();
|
|
|
|
// Create a task in the start step
|
|
const task = await apiClient.createTask(seedData.workspaceId, "Movable Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Moving task...")',
|
|
`e2e:mcp:kandev:move_task_kandev({"task_id":"${task.id}","workflow_id":"${seedData.workflowId}","workflow_step_id":"${targetStep!.id}"})`,
|
|
'e2e:message("Task moved")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Task moved");
|
|
|
|
// Verify the task is now in the target step
|
|
const { tasks } = await apiClient.listTasks(seedData.workspaceId);
|
|
const moved = tasks.find((t) => t.id === task.id);
|
|
expect(moved?.workflow_step_id).toBe(targetStep!.id);
|
|
});
|
|
|
|
test("agent can archive a task", async ({ testPage, apiClient, seedData }) => {
|
|
const task = await apiClient.createTask(seedData.workspaceId, "Archivable Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Archiving task...")',
|
|
`e2e:mcp:kandev:archive_task_kandev({"task_id":"${task.id}"})`,
|
|
'e2e:message("Task archived")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Task archived");
|
|
|
|
// Verify the task no longer appears in active task list
|
|
const { tasks } = await apiClient.listTasks(seedData.workspaceId);
|
|
expect(tasks.find((t) => t.id === task.id)).toBeUndefined();
|
|
});
|
|
|
|
test("agent can delete a task", async ({ testPage, apiClient, seedData }) => {
|
|
const task = await apiClient.createTask(seedData.workspaceId, "Deletable Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Deleting task...")',
|
|
`e2e:mcp:kandev:delete_task_kandev({"task_id":"${task.id}"})`,
|
|
'e2e:message("Task deleted")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Task deleted");
|
|
|
|
// Verify the task is gone
|
|
const { tasks } = await apiClient.listTasks(seedData.workspaceId);
|
|
expect(tasks.find((t) => t.id === task.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Executor management
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Config-mode MCP — executor management", () => {
|
|
test("agent can list executors", async ({ testPage, apiClient, seedData }) => {
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Listing executors...")',
|
|
"e2e:mcp:kandev:list_executors_kandev({})",
|
|
'e2e:message("Executors listed")',
|
|
].join("\n"),
|
|
);
|
|
|
|
const page = await runAndWait(testPage, session.task_id, "Executors listed");
|
|
await expandToolCallGroups(page);
|
|
await expect(page.chat.getByText("Kandev: List Executors", { exact: true })).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
});
|
|
|
|
test("agent can create and delete an executor profile", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
// Use the system "Local" executor for the profile
|
|
let { executors } = await apiClient.listExecutors();
|
|
const executor = executors.find((e) => e.type === "local")!;
|
|
|
|
// Create profile via MCP
|
|
const createSession = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Creating executor profile...")',
|
|
`e2e:mcp:kandev:create_executor_profile_kandev({"executor_id":"${executor.id}","name":"E2E Profile"})`,
|
|
'e2e:message("Executor profile created")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, createSession.task_id, "Executor profile created");
|
|
|
|
// Verify via API
|
|
({ executors } = await apiClient.listExecutors());
|
|
const exec = executors.find((e) => e.id === executor.id);
|
|
const profile = exec?.profiles?.find((p) => p.name === "E2E Profile");
|
|
expect(profile).toBeTruthy();
|
|
|
|
// Delete profile via MCP
|
|
const deleteSession = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Deleting executor profile...")',
|
|
`e2e:mcp:kandev:delete_executor_profile_kandev({"profile_id":"${profile!.id}"})`,
|
|
'e2e:message("Executor profile deleted")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, deleteSession.task_id, "Executor profile deleted");
|
|
|
|
// Verify deleted
|
|
const { executors: afterDelete } = await apiClient.listExecutors();
|
|
const execAfter = afterDelete.find((e) => e.id === executor.id);
|
|
expect(execAfter?.profiles?.find((p) => p.id === profile!.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Multi-tool workflows
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("Config-mode MCP — multi-tool workflow", () => {
|
|
test("agent executes multiple config tools in sequence", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Multi-Tool Workflow");
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Starting multi-tool config...")',
|
|
"e2e:mcp:kandev:list_workspaces_kandev({})",
|
|
`e2e:mcp:kandev:list_workflows_kandev({"workspace_id":"${seedData.workspaceId}"})`,
|
|
`e2e:mcp:kandev:create_workflow_step_kandev({"workflow_id":"${workflow.id}","name":"Agent Created Step","position":0})`,
|
|
"e2e:mcp:kandev:list_agents_kandev({})",
|
|
'e2e:message("Multi-tool config complete")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Multi-tool config complete");
|
|
|
|
// Verify the step was actually created
|
|
const { steps } = await apiClient.listWorkflowSteps(workflow.id);
|
|
const createdStep = steps.find((s) => s.name === "Agent Created Step");
|
|
expect(createdStep).toBeTruthy();
|
|
});
|
|
|
|
test("agent performs workflow setup with a step, profile, and MCP config", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Full Setup Workflow");
|
|
const { agents } = await apiClient.listAgents();
|
|
const agent = agents[0];
|
|
|
|
const session = await startConfigSession(
|
|
apiClient,
|
|
seedData,
|
|
[
|
|
'e2e:message("Setting up full workflow...")',
|
|
// Create a workflow step
|
|
`e2e:mcp:kandev:create_workflow_step_kandev({"workflow_id":"${workflow.id}","name":"Build","position":0,"color":"#3b82f6"})`,
|
|
// Create a new agent profile
|
|
`e2e:mcp:kandev:create_agent_profile_kandev({"agent_id":"${agent.id}","name":"CI Profile","model":"claude-sonnet-4-5-20250514"})`,
|
|
// Update MCP config on the test profile
|
|
`e2e:mcp:kandev:update_mcp_config_kandev({"profile_id":"${seedData.agentProfileId}","enabled":true,"servers":{"ci-tools":{"command":"npx","args":["-y","@ci/tools"]}}})`,
|
|
'e2e:message("Full setup complete")',
|
|
].join("\n"),
|
|
);
|
|
|
|
await runAndWait(testPage, session.task_id, "Full setup complete");
|
|
|
|
// Verify workflow step
|
|
const { steps } = await apiClient.listWorkflowSteps(workflow.id);
|
|
expect(steps).toHaveLength(1);
|
|
expect(steps).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: "Build",
|
|
position: 0,
|
|
color: "#3b82f6",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// Verify new profile was created
|
|
const { agents: updatedAgents } = await apiClient.listAgents();
|
|
const updatedAgent = updatedAgents.find((a) => a.id === agent.id);
|
|
expect((updatedAgent?.profiles ?? []).find((p) => p.name === "CI Profile")).toBeTruthy();
|
|
|
|
// Verify MCP config
|
|
const config = await apiClient.getAgentProfileMcpConfig(seedData.agentProfileId);
|
|
expect(config.enabled).toBe(true);
|
|
expect(config.servers).toHaveProperty("ci-tools");
|
|
});
|
|
});
|