Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
153 changes: 153 additions & 0 deletions ui-next/e2e/integration/agent-definition-diagram.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Integration tests — nested agent diagrams (definition + execution)
*
* Deploys a multi-level agent (sequential → parallel → leaves), asserts the
* definition Diagram tab renders nested grandchildren, then starts an
* execution and asserts the same nesting on the execution page Agent
* Definition tab (uses agentDef metadata — no OpenAI required).
*
* Execution-diagram nesting + COMPLETED status are covered by the unit test
* AgentExecutionDiagram.nesting.test.ts (fixture AgentRunData, no LLM).
*
* Requires conductor.integrations.ai.enabled=true on the ui-e2e server.
*
* Snapshot baselines live under e2e/integration/__snapshots__/ (platform-
* specific). Update with:
* pnpm test:e2e:integration -- --update-snapshots=all e2e/integration/agent-definition-diagram.spec.ts
*/

import { expect, test } from "../coverage-fixture";
import {
deleteAgent,
deployAgent,
isAgentApiAvailable,
startAgent,
} from "./api-client";

const ROOT = "e2e_def_nest_root";
const CHILD_A = "e2e_def_nest_a";
const CHILD_PAR = "e2e_def_nest_par";
const GRAND_B = "e2e_def_nest_b";
const GRAND_C = "e2e_def_nest_c";
const CHILD_E = "e2e_def_nest_e";

const DEFAULT_MODEL = "openai/gpt-4o-mini";
const AGENT_EXECUTIONS_URL = "/agentExecutions";

const NESTED_DEPLOY = {
model: DEFAULT_MODEL,
strategy: "sequential" as const,
synthesize: false,
instructions: "Root sequential coordinator for diagram nesting e2e.",
maxTurns: 1,
agents: [
{
name: CHILD_A,
model: DEFAULT_MODEL,
instructions: "classify",
},
{
name: CHILD_PAR,
model: DEFAULT_MODEL,
strategy: "parallel",
synthesize: false,
instructions: "parallel branch",
agents: [
{
name: GRAND_B,
model: DEFAULT_MODEL,
instructions: "infra cause",
},
{
name: GRAND_C,
model: DEFAULT_MODEL,
instructions: "code cause",
},
],
},
{
name: CHILD_E,
model: DEFAULT_MODEL,
instructions: "postmortem",
},
],
};

async function assertNestedDefinitionDiagram(
page: import("@playwright/test").Page,
snapshotName: string,
) {
const diagram = page.getByTestId("agent-definition-diagram");
await expect(diagram).toBeVisible({ timeout: 15_000 });

await expect(page.getByText(CHILD_A, { exact: true })).toBeVisible({
timeout: 15_000,
});
await expect(page.getByText(CHILD_PAR, { exact: true })).toBeVisible();
await expect(page.getByText(CHILD_E, { exact: true })).toBeVisible();
await expect(page.getByText(GRAND_B, { exact: true })).toBeVisible();
await expect(page.getByText(GRAND_C, { exact: true })).toBeVisible();

await expect(
diagram.getByRole("button", { name: "Fit to screen" }),
).toBeVisible({ timeout: 15_000 });
await diagram.getByRole("button", { name: "Fit to screen" }).click();
await page.mouse.move(0, 0);
await expect(page.getByRole("tooltip")).toHaveCount(0);
await expect(diagram).toHaveScreenshot(snapshotName);
}

test.beforeAll(async () => {
const available = await isAgentApiAvailable();
if (!available) {
throw new Error(
"GET /api/agent/list is not available. " +
"Ensure the ui-e2e Conductor server has conductor.integrations.ai.enabled=true " +
"(application.properties default; config-postgres.properties must not override it).",
);
}

await deleteAgent(ROOT).catch(() => {});
await deployAgent(ROOT, NESTED_DEPLOY);
});

test.afterAll(async () => {
await deleteAgent(ROOT).catch(() => {});
});

test("agent definition diagram shows nested parallel grandchildren", async ({
page,
}) => {
await page.goto(`/agents/${encodeURIComponent(ROOT)}`);
await page.waitForLoadState("networkidle");

await expect(page.getByRole("tab", { name: "Diagram" })).toBeVisible({
timeout: 15_000,
});

await assertNestedDefinitionDiagram(
page,
"agent-definition-nested-diagram.png",
);
});

test("execution page Agent Definition tab shows the same nested tree", async ({
page,
}) => {
const { executionId } = await startAgent(
ROOT,
"Classify then investigate in parallel, then write a short postmortem.",
);

await page.goto(`${AGENT_EXECUTIONS_URL}/${executionId}`);
await page.waitForLoadState("networkidle");

await expect(page.locator("#main-content")).toBeVisible();
await expect(page.getByText(ROOT).first()).toBeVisible({ timeout: 15_000 });

await page.getByRole("tab", { name: "Agent Definition" }).click();
await assertNestedDefinitionDiagram(
page,
"agent-execution-definition-nested-diagram.png",
);
});
56 changes: 48 additions & 8 deletions ui-next/e2e/integration/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ export async function listAgents(): Promise<AgentSummary[]> {
return request<AgentSummary[]>("GET", "/agent/list");
}

/** Nested agentConfig payload accepted by POST /api/agent/deploy. */
export interface AgentConfigPayload {
name: string;
model?: string;
instructions?: string;
maxTurns?: number;
strategy?: string;
synthesize?: boolean;
agents?: AgentConfigPayload[];
}

/**
* Compiles and registers an agent definition via POST /api/agent/deploy.
* Does not start an execution.
Expand All @@ -253,17 +264,27 @@ export async function deployAgent(
model?: string;
instructions?: string;
maxTurns?: number;
strategy?: string;
synthesize?: boolean;
agents?: AgentConfigPayload[];
} = {},
): Promise<AgentDeployResponse> {
const agentConfig: AgentConfigPayload = {
name: agentName,
model: options.model ?? "openai/gpt-4o-mini",
instructions:
options.instructions ??
"You are a concise test agent. Answer in one sentence.",
maxTurns: options.maxTurns ?? 1,
};
if (options.strategy) agentConfig.strategy = options.strategy;
if (options.synthesize !== undefined) {
agentConfig.synthesize = options.synthesize;
}
if (options.agents) agentConfig.agents = options.agents;

return request<AgentDeployResponse>("POST", "/agent/deploy", {
agentConfig: {
name: agentName,
model: options.model ?? "openai/gpt-4o-mini",
instructions:
options.instructions ??
"You are a concise test agent. Answer in one sentence.",
maxTurns: options.maxTurns ?? 1,
},
agentConfig,
});
}

Expand All @@ -275,6 +296,25 @@ export async function deleteAgent(
await request<void>("DELETE", `/agent/${encodeURIComponent(agentName)}${qs}`);
}

export interface AgentStartResponse {
executionId: string;
agentName?: string;
}

/** Starts a deployed agent via POST /api/agent/start. */
export async function startAgent(
agentName: string,
prompt: string,
options: { version?: number } = {},
): Promise<AgentStartResponse> {
const body: Record<string, unknown> = {
name: agentName,
prompt,
};
if (options.version !== undefined) body.version = options.version;
return request<AgentStartResponse>("POST", "/agent/start", body);
}

export interface AgentStatus {
executionId: string;
status: string;
Expand Down
51 changes: 35 additions & 16 deletions ui-next/playwright.integration.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { defineConfig, devices } from "@playwright/test";

const CONDUCTOR_SERVER_URL =
process.env.CONDUCTOR_SERVER_URL ?? "http://localhost:8000";
const PLAYWRIGHT_BASE_URL =
process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:1234";

export default defineConfig({
testDir: "./e2e/integration",
Expand All @@ -48,14 +50,23 @@ export default defineConfig({
globalTeardown: "./e2e/integration/global-teardown.ts",

use: {
baseURL: "http://localhost:1234",
baseURL: PLAYWRIGHT_BASE_URL,
trace: "on-first-retry",
screenshot: "only-on-failure",
// Integration tests can be slower due to real API calls.
actionTimeout: 15_000,
navigationTimeout: 30_000,
},

// Visual assertions in integration specs (e.g. agent definition diagram).
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.02,
},
},
snapshotPathTemplate:
"{testDir}/__snapshots__/{testFilePath}/{arg}-{platform}{ext}",

projects: [
{
name: "chromium",
Expand All @@ -73,19 +84,27 @@ export default defineConfig({
// SKIP_WEBSERVER_BUILD=true so the webServer only runs the lightweight
// preview — vite build + E2E_COVERAGE sourcemaps can OOM on the runner when
// combined with Docker.
webServer: {
command:
process.env.SKIP_WEBSERVER_BUILD === "true"
? "pnpm preview"
: "pnpm build && pnpm preview",
url: "http://localhost:1234",
reuseExistingServer: !process.env.CI,
timeout: 300_000, // allow up to 5 min for a cold build
env: {
VITE_WF_SERVER: CONDUCTOR_SERVER_URL,
// Raise the heap for `vite build` (especially with E2E_COVERAGE sourcemaps).
// CI pre-builds with SKIP_WEBSERVER_BUILD so this mainly helps local runs.
NODE_OPTIONS: process.env.NODE_OPTIONS ?? "--max-old-space-size=8192",
},
},
//
// Set SKIP_WEBSERVER=true to point Playwright at an already-running preview
// (e.g. generating Linux snapshots from a Playwright Docker image on macOS).
...(process.env.SKIP_WEBSERVER === "true"
? {}
: {
webServer: {
command:
process.env.SKIP_WEBSERVER_BUILD === "true"
? "pnpm preview"
: "pnpm build && pnpm preview",
url: PLAYWRIGHT_BASE_URL,
reuseExistingServer: !process.env.CI,
timeout: 300_000, // allow up to 5 min for a cold build
env: {
VITE_WF_SERVER: CONDUCTOR_SERVER_URL,
// Raise the heap for `vite build` (especially with E2E_COVERAGE sourcemaps).
// CI pre-builds with SKIP_WEBSERVER_BUILD so this mainly helps local runs.
NODE_OPTIONS:
process.env.NODE_OPTIONS ?? "--max-old-space-size=8192",
},
},
}),
});
Loading
Loading