Ctrl K
ComunidadGuías…

tests/e2e/flows-fallback-model.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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") });
});