Ctrl K
ComunidadGuías…

tests/e2e/flows-legacy-models.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),
  });
}

test("flows inspector highlights legacy hidden model overrides and supports quick replacement", async ({
  page,
}) => {
  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: {
        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: "openai/o1",
            },
          },
          {
            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 },
      },
      created_at: "2026-03-28T17:00:00.000Z",
    },
    draft_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: "openai/o1",
          },
        },
        {
          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 },
    },
  };

  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: [
        {
          id: "minimax/minimax-m2.5",
          provider: "minimax",
          name: "MiniMax M2.5",
          context_length: 200000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: false,
        },
        {
          id: "openai/gpt-5.4",
          provider: "openai",
          name: "GPT-5.4",
          context_length: 200000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: false,
        },
      ],
      catalog: [
        {
          id: "minimax/minimax-m2.5",
          provider: "minimax",
          name: "MiniMax M2.5",
          context_length: 200000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: false,
          is_enabled: true,
        },
        {
          id: "openai/gpt-5.4",
          provider: "openai",
          name: "GPT-5.4",
          context_length: 200000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: false,
          is_enabled: true,
        },
        {
          id: "openai/o1",
          provider: "openai",
          name: "OpenAI o1",
          context_length: 200000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: true,
          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", async (route) => {
    if (route.request().method() === "GET") {
      await fulfillJson(route, [currentFlow]);
      return;
    }

    const payload = route.request().postDataJSON() as typeof currentFlow;
    currentFlow = {
      ...currentFlow,
      ...payload,
      draft_graph: payload.draft_graph,
    };
    await fulfillJson(route, currentFlow, 201);
  });
  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
    > & { draft_graph: typeof currentFlow.draft_graph };
    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");

  await expect(page.getByTestId("flows-legacy-model-banner")).toBeVisible();

  const agentNode = page.getByTestId("rf__node-agent-a");
  await agentNode.click();
  // Selecting a node opens the inspector on its own now; the separate
  // `.flows-inspector-toggle` control no longer exists.
  await expect(page.locator(".flows-inspector")).toBeVisible();
  await expect(page.getByTestId("flows-legacy-model-warning")).toBeVisible();
  await expect(
    page.getByText("This node override is not enabled for this account.")
  ).toBeVisible();
  // The override is kept selected and labelled rather than silently blanked --
  // see `buildAgentModelOptions`, which prepends a "Legacy · " entry for a
  // current model that is no longer in the enabled catalogue. Blanking it would
  // misreport what the node is actually configured to run.
  const modelSelect = page.getByLabel("Model", { exact: true });
  await expect(modelSelect).toHaveAttribute("data-value", "openai/o1");
  await modelSelect.click();
  const legacyOption = page.locator('[role="option"][data-value="openai/o1"]');
  await expect(legacyOption).toHaveCount(1);
  await expect(legacyOption).toHaveText("Legacy · openai/o1");
  await legacyOption.click();

  await page.getByTestId("flows-legacy-model-replace").click();
  await expect(modelSelect).toHaveAttribute(
    "data-value",
    "minimax/minimax-m2.5"
  );
  await modelSelect.click();
  await expect(
    page.locator('[role="option"][data-value="openai/o1"]')
  ).toHaveCount(0);
  await page
    .locator('[role="option"][data-value="minimax/minimax-m2.5"]')
    .click();
  await expect(page.getByTestId("flows-legacy-model-banner")).toHaveCount(0);

  // The backdrop is overlay-only -- docked it is `display: none` -- so close
  // through the header control, which works in both modes.
  await page.getByTestId("flows-inspector-close").click();
  // Docked, the aside stays mounted and falls back to its empty state rather
  // than unmounting, so assert on that instead of on the aside disappearing.
  await expect(page.getByTestId("flows-inspector-empty")).toBeVisible();
  await page.getByRole("button", { name: "Save" }).click();
  await expect
    .poll(() => {
      const agentNode = (currentFlow.draft_graph.nodes as FlowNode[]).find(
        (node): node is Extract<FlowNode, { type: "agent" }> =>
          node.id === "agent-a" && node.type === "agent"
      );
      return agentNode?.data.modelOverride;
    })
    .toBe("minimax/minimax-m2.5");
  expect(pageErrors).toEqual([]);
});

test("workflows model override selector flags a disabled current override as legacy", async ({
  page,
}) => {
  const pageErrors = capturePageErrors(page);
  await enableScopedE2EAuth(page);

  const disabledModelId = "anthropic/claude-sonnet-4.6";
  const enabledModelId = "minimax/minimax-m2.7";
  const 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: {
        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: disabledModelId,
            },
          },
          {
            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 },
      },
      created_at: "2026-03-28T17:00:00.000Z",
    },
    draft_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: disabledModelId,
          },
        },
        {
          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 },
    },
  };

  await page.route("**/api/auth/user", (route) =>
    fulfillJson(route, { user: connectedUser })
  );
  await page.route("**/api/settings", (route) =>
    fulfillJson(route, { default_model: enabledModelId, theme: "dark" })
  );
  await page.route("**/api/models", (route) =>
    fulfillJson(route, {
      models: [
        {
          id: enabledModelId,
          provider: "minimax",
          name: "MiniMax M2.7",
          context_length: 1_000_000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: false,
        },
      ],
      catalog: [
        {
          id: enabledModelId,
          provider: "minimax",
          name: "MiniMax M2.7",
          context_length: 1_000_000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: false,
          is_enabled: true,
        },
        {
          id: disabledModelId,
          provider: "anthropic",
          name: "Claude Sonnet 4.6",
          context_length: 1_000_000,
          capabilities: ["text"],
          is_available: true,
          is_hidden: false,
          is_enabled: false,
        },
      ],
    })
  );
  await page.route("**/api/agents", (route) =>
    fulfillJson(route, [
      {
        id: "agent-a",
        name: "Reviewer A",
        slug: "reviewer-a",
        model: enabledModelId,
      },
    ])
  );
  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", (route) =>
    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();

  await expect(page.getByTestId("flows-legacy-model-warning")).toBeVisible();
  await expect(
    page.getByText("This node override is not enabled for this account.")
  ).toBeVisible();
  const modelSelect = page.getByLabel("Model", { exact: true });
  await expect(modelSelect).toHaveAttribute("data-value", disabledModelId);

  // A disabled current override is carried as a flagged legacy entry, not as a
  // normal choice: it is there once, marked, and it is not what the selector
  // offers for a fresh pick.
  await modelSelect.click();
  const disabledOption = page.locator(
    `[role="option"][data-value="${disabledModelId}"]`
  );
  await expect(disabledOption).toHaveCount(1);
  await expect(disabledOption).toHaveText(`Legacy · ${disabledModelId}`);

  const enabledOption = page.locator(
    `[role="option"][data-value="${enabledModelId}"]`
  );
  await expect(enabledOption).toHaveCount(1);
  await expect(enabledOption).not.toHaveText(/^Legacy · /);
  expect(pageErrors).toEqual([]);
});