tests/e2e/flows-fallback-model.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import {
E2E_SCOPE_USER,
enableScopedE2EAuth,
scopedPath,
} from "./helpers/auth";
import { linkedVercelCapability } from "./helpers/activation-fixtures";
import { capturePageErrors } from "./helpers/page-errors";
import type { Route } from "@playwright/test";
import type { FlowNode } from "../../lib/types";
const connectedUser = {
id: E2E_SCOPE_USER.id,
email: "alex@example.com",
username: E2E_SCOPE_USER.username,
name: "Alex",
avatar_url: "https://example.com/avatar.png",
github_connected: true,
github_app_connected: true,
github_app_available: true,
github_connection_mode: "app" as const,
vercel: linkedVercelCapability,
};
async function fulfillJson(route: Route, data: unknown, status = 200) {
await route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify(data),
});
}
const graph = {
nodes: [
{
id: "start",
type: "start",
position: { x: 120, y: 160 },
data: { label: "PR opened", event: "pr_opened" },
},
{
id: "agent-a",
type: "agent",
position: { x: 380, y: 160 },
data: {
label: "Reviewer A",
agentId: "agent-a",
modelOverride: "minimax/minimax-m2.5",
},
},
{
id: "end",
type: "end",
position: { x: 900, y: 160 },
data: { label: "Done" },
},
],
edges: [
{ id: "edge-1", source: "start", target: "agent-a" },
{ id: "edge-2", source: "agent-a", target: "end" },
],
viewport: { x: 0, y: 0, zoom: 1 },
};
const modelFixture = {
provider: "minimax",
context_length: 200000,
capabilities: ["text"],
is_available: true,
is_hidden: false,
};
test("flows inspector saves a fallback and restores inherited model defaults", async ({
page,
}, testInfo) => {
const pageErrors = capturePageErrors(page);
// Width must be >= 1520 so the flows container (minus ~240px sidebar) exceeds
// the 1280px dock-mode threshold.
await page.setViewportSize({ width: 1600, height: 900 });
await enableScopedE2EAuth(page);
let currentFlow = {
id: "flow-1",
installation_id: 101,
name: "Review Flow",
description: "Flow description",
notes: "Flow notes",
source_kind: "github",
status: "active",
last_run_status: "success",
published_version_id: "version-1",
published_version: {
id: "version-1",
flow_id: "flow-1",
version_number: 1,
graph,
created_at: "2026-03-28T17:00:00.000Z",
},
draft_graph: graph,
};
await page.route("**/api/auth/user", (route) =>
fulfillJson(route, { user: connectedUser })
);
await page.route("**/api/settings", (route) =>
fulfillJson(route, { default_model: "minimax/minimax-m2.5", theme: "dark" })
);
await page.route("**/api/models", (route) =>
fulfillJson(route, {
models: [
{ ...modelFixture, id: "minimax/minimax-m2.5", name: "MiniMax M2.5" },
{
...modelFixture,
id: "openai/gpt-5.4",
provider: "openai",
name: "GPT-5.4",
},
],
catalog: [
{
...modelFixture,
id: "minimax/minimax-m2.5",
name: "MiniMax M2.5",
is_enabled: true,
},
{
...modelFixture,
id: "openai/gpt-5.4",
provider: "openai",
name: "GPT-5.4",
is_enabled: true,
},
],
})
);
await page.route("**/api/agents", (route) =>
fulfillJson(route, [
{
id: "agent-a",
name: "Reviewer A",
slug: "reviewer-a",
model: "minimax/minimax-m2.5",
},
])
);
await page.route("**/api/github/installations", (route) =>
fulfillJson(route, [
{
id: "inst-1",
installation_id: 101,
account_login: "webrenew",
repositories: [{ id: "repo-1", full_name: "webrenew/blackbox" }],
},
])
);
await page.route("**/api/flows", (route) =>
fulfillJson(route, [currentFlow])
);
await page.route("**/api/flows/flow-1", async (route) => {
if (route.request().method() === "GET") {
await fulfillJson(route, currentFlow);
return;
}
const payload = route.request().postDataJSON() as Partial<
typeof currentFlow
>;
currentFlow = {
...currentFlow,
...payload,
draft_graph: payload.draft_graph ?? currentFlow.draft_graph,
};
await fulfillJson(route, currentFlow);
});
await page.route("**/api/flows/flow-1/runs?limit=12", (route) =>
fulfillJson(route, { runs: [] })
);
await page.goto(scopedPath("workflows"));
await page.waitForLoadState("networkidle");
const agentNode = page.getByTestId("rf__node-agent-a");
await agentNode.click();
await expect(page.locator(".flows-inspector")).toBeVisible();
// Unset by default: the shared fallback pool applies.
const fallbackSelect = page.getByLabel("Fallback model", { exact: true });
await expect(fallbackSelect).toBeVisible();
await expect(fallbackSelect).toHaveAttribute("data-value", "");
await fallbackSelect.click();
await page.locator('[role="option"][data-value="openai/gpt-5.4"]').click();
await expect(fallbackSelect).toHaveAttribute("data-value", "openai/gpt-5.4");
await page.getByTestId("flows-inspector-close").click();
await page.getByRole("button", { name: "Save" }).click();
await expect
.poll(() => {
const agent = (currentFlow.draft_graph.nodes as FlowNode[]).find(
(node): node is Extract<FlowNode, { type: "agent" }> =>
node.id === "agent-a" && node.type === "agent"
);
return agent?.data.fallbackModelOverride;
})
.toBe("openai/gpt-5.4");
expect(pageErrors).toEqual([]);
await page.getByTestId("rf__node-agent-a").click();
const modelSelect = page.getByLabel("Model", { exact: true });
await modelSelect.click();
await page
.getByRole("option", { name: "Default from settings", exact: true })
.click();
await expect(modelSelect).toHaveAttribute("data-value", "");
await expect(page.getByTestId("flows-missing-model-warning")).toHaveCount(0);
// Save feedback must leave the inspector close control immediately usable.
expect(
await page.getByTestId("flows-inspector-close").evaluate((button) => {
const bounds = button.getBoundingClientRect();
return button.contains(
document.elementFromPoint(
bounds.x + bounds.width / 2,
bounds.y + bounds.height / 2
)
);
})
).toBe(true);
await page.getByTestId("flows-inspector-close").click();
const saved = page.waitForResponse(
(response) =>
response.url().endsWith("/api/flows/flow-1") &&
response.request().method() === "PUT"
);
await page.getByRole("button", { name: "Save" }).click();
await saved;
expect(
(currentFlow.draft_graph.nodes as FlowNode[]).find(
(node) => node.type === "agent"
)?.data.modelOverride
).toBeNull();
await page.reload();
await page.getByTestId("rf__node-agent-a").click();
await expect(page.getByLabel("Model", { exact: true })).toHaveText(
"Default from settings"
);
await expect(page.getByTestId("flows-missing-model-warning")).toHaveCount(0);
expect(pageErrors).toEqual([]);
await expect(page.getByTestId("flows-legacy-model-banner")).toHaveCount(0);
await page.screenshot({ path: testInfo.outputPath("inherited-model.png") });
});