Ctrl K
ComunidadGuías…

tests/e2e/models-catalog.spec.ts

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

Archivo original ↗Preguntar a KiBot
import { expect, test } from "@playwright/test";
import { enableScopedE2EAuth, scopedPath } from "./helpers/auth";
import { linkedVercelCapability } from "./helpers/activation-fixtures";
import type { Route } from "@playwright/test";
import { expectFullRouteWidth } from "./helpers/route-width";

const recommendedAt = new Date().toISOString();

const connectedUser = {
  id: "user-1",
  email: "[email protected]",
  username: "alex",
  name: "Alex",
  avatar_url: "https://example.com/avatar.png",
  github_connected: true,
  github_app_connected: false,
  github_app_available: false,
  github_connection_mode: "oauth" as const,
  vercel: linkedVercelCapability,
};

const catalog = [
  {
    id: "minimax/minimax-m2.7",
    provider: "minimax",
    name: "Minimax M2.7",
    context_length: 205000,
    pricing_input: 0.0000025,
    pricing_output: 0.00001,
    capabilities: ["vision", "tool-calling"],
    is_available: true,
    is_recommended: true,
    recommendation_bucket: "open",
    recommendation_rank: 1,
    recommendation_reason: "open_best_general",
    recommended_at: recommendedAt,
    is_enabled: true,
  },
  {
    id: "openai/gpt-oss-120b",
    provider: "openai",
    name: "GPT-OSS 120B",
    context_length: 128000,
    pricing_input: 0.00000015,
    pricing_output: 0.0000006,
    capabilities: ["tool-calling"],
    is_available: true,
    is_recommended: true,
    recommendation_bucket: "open",
    recommendation_rank: 2,
    recommendation_reason: "open_best_economy",
    recommended_at: recommendedAt,
    is_enabled: false,
  },
  {
    id: "openai/gpt-5.4",
    provider: "openai",
    name: "GPT-5.4",
    context_length: 1100000,
    pricing_input: 0.000015,
    pricing_output: 0.000075,
    capabilities: ["reasoning"],
    is_available: true,
    is_recommended: false,
    recommendation_bucket: null,
    recommendation_rank: null,
    recommendation_reason: null,
    recommended_at: null,
    is_enabled: false,
  },
  {
    id: "anthropic/claude-sonnet-4.6",
    provider: "anthropic",
    name: "Claude Sonnet 4.6",
    context_length: 1000000,
    pricing_input: 0.000003,
    pricing_output: 0.000015,
    capabilities: ["reasoning"],
    is_available: true,
    is_recommended: true,
    recommendation_bucket: "frontier",
    recommendation_rank: 1,
    recommendation_reason: "frontier_latest_general",
    recommended_at: recommendedAt,
    is_enabled: false,
  },
  {
    id: "anthropic/claude-opus-4.7",
    provider: "anthropic",
    name: "Claude Opus 4.7",
    context_length: 1000000,
    pricing_input: 0.000005,
    pricing_output: 0.000025,
    capabilities: ["reasoning"],
    is_available: false,
    is_recommended: false,
    recommendation_bucket: null,
    recommendation_rank: null,
    recommendation_reason: null,
    recommended_at: null,
    is_enabled: false,
    is_hidden: true,
  },
];

async function fulfillJson(route: Route, data: unknown, status = 200) {
  await route.fulfill({
    status,
    contentType: "application/json",
    body: JSON.stringify(data),
  });
}

test("models catalog supports provider, state, pricing filters, and state sorting", async ({
  page,
}, testInfo) => {
  await enableScopedE2EAuth(page);
  let defaultModel = "minimax/minimax-m2.7";
  await page.route("**/api/settings/model-chain", async (route) => {
    if (route.request().method() === "PATCH")
      defaultModel = route.request().postDataJSON().primary;
    await fulfillJson(route, { primary: defaultModel, fallbacks: [] });
  });
  const catalogState = catalog.map((model) => ({ ...model }));

  await page.route("**/api/auth/user", (route) =>
    fulfillJson(route, { user: connectedUser })
  );
  await page.route(/\/api\/settings(?:\?.*)?$/, async (route) => {
    await fulfillJson(route, { default_model: defaultModel, theme: "dark" });
  });
  await page.route("**/api/models", async (route) => {
    if (route.request().method() === "PATCH") {
      const payload = JSON.parse(route.request().postData() || "{}") as {
        model_id?: string;
        is_enabled?: boolean;
      };
      if (payload.model_id && typeof payload.is_enabled === "boolean") {
        const model = catalogState.find(
          (entry) => entry.id === payload.model_id
        );
        if (model) {
          model.is_enabled = payload.is_enabled;
        }
      }
      await fulfillJson(route, { ok: true });
      return;
    }

    await fulfillJson(route, {
      models: catalogState.filter((model) => model.is_enabled),
      catalog: catalogState,
    });
  });
  await page.route("**/api/github/installations", (route) =>
    fulfillJson(route, [])
  );
  await page.route("**/api/connections", (route) =>
    fulfillJson(route, { connections: [] })
  );
  await page.route("**/api/models/provider-icons", (route) =>
    fulfillJson(route, { providers: ["anthropic", "minimax", "openai"] })
  );
  // Serve the icon bytes too: the src points at storage that does not exist
  // in CI, and a failed load makes ProviderIcon swap the <img> for the
  // letter fallback before the src assertion runs.
  await page.route(
    /\/storage\/v1\/object\/public\/provider-icons\/.*\.png$/,
    (route) =>
      route.fulfill({
        status: 200,
        contentType: "image/png",
        body: Buffer.from(
          "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
          "base64"
        ),
      })
  );

  await page.goto(scopedPath("/models/catalog"));
  await page.waitForLoadState("networkidle");

  const primaryRole = (id: string) =>
    page.getByTestId(`models-chain-role-${id}`);
  await expect(primaryRole("minimax/minimax-m2.7")).toHaveText("Primary");
  await expect(
    page.getByTestId("models-set-default-minimax/minimax-m2.7")
  ).toHaveCount(0);
  await expect(page.getByTestId("app-nav-models")).toHaveAttribute(
    "aria-current",
    "page"
  );
  await expect(
    page.getByTestId("models-recommendation-freshness")
  ).toContainText("Recommendations refreshed");

  // Hidden models stay in the API catalog payload but are excluded from the
  // preferences table.
  await expect(
    page.getByTestId("models-recommendation-freshness")
  ).toContainText("4 of 4 models");
  await expect(page.getByText("Claude Opus 4.7")).toHaveCount(0);
  const minimaxProviderIcon = page.getByTestId(
    "models-provider-icon-minimax/minimax-m2.7"
  );
  await expect(minimaxProviderIcon).toBeVisible();
  await expect(minimaxProviderIcon.locator("img")).toHaveAttribute(
    "src",
    /\/storage\/v1\/object\/public\/provider-icons\/minimax\.png$/
  );

  await expect(
    page.getByTestId("models-set-default-openai/gpt-oss-120b")
  ).toBeDisabled();
  const toggle = page.getByTestId("models-toggle-openai/gpt-oss-120b");
  // Same shared switch as every other toggle, not a bespoke pill.
  await expect(toggle).toHaveAttribute("data-slot", "switch");
  await expect(toggle).not.toBeChecked();
  await toggle.click();
  await expect(toggle).toBeChecked();

  await page.getByTestId("models-set-default-openai/gpt-oss-120b").click();
  await expect(primaryRole("openai/gpt-oss-120b")).toHaveText("Primary");
  await expect(primaryRole("minimax/minimax-m2.7")).toHaveCount(0);
  expect(defaultModel).toBe("minimax/minimax-m2.7");
  const draftBar = page.getByTestId("models-chain-draft-bar");
  await expect(draftBar).toContainText("GPT-OSS 120B");
  await draftBar.getByRole("button", { name: "Save chain" }).click();
  await expect(draftBar.getByRole("status")).toHaveText("Chain saved.");
  expect(defaultModel).toBe("openai/gpt-oss-120b");
  await expect(page.getByTestId("models-draft-indicator")).toHaveCount(0);

  await page.setViewportSize({ width: 1920, height: 1000 });
  await expectFullRouteWidth(page, page.getByTestId("models-content"));
  await page.screenshot({ path: testInfo.outputPath("models-catalog.png") });
  await expect(page.getByText("In $2.50")).toBeVisible();
  await expect(page.getByText("Out $75")).toBeVisible();

  await page.getByTestId("models-provider-filter").selectOption("openai");
  let visibleNames = await page
    .getByTestId("models-row-name")
    .allTextContents();
  expect(visibleNames).toEqual(["GPT-OSS 120B", "GPT-5.4"]);

  await page.getByTestId("models-enabled-filter").selectOption("enabled");
  const providerFilteredNames = await page
    .getByTestId("models-row-name")
    .allTextContents();
  expect(providerFilteredNames).toEqual(["GPT-OSS 120B"]);

  await page.getByTestId("models-enabled-filter").selectOption("default");
  const defaultFilteredNames = await page
    .getByTestId("models-row-name")
    .allTextContents();
  expect(defaultFilteredNames).toEqual(["GPT-OSS 120B"]);

  await page.getByTestId("models-enabled-filter").selectOption("all");
  await page.getByTestId("models-provider-filter").selectOption("all");
  await page.getByTestId("models-sort-key").selectOption("state");
  const stateSortedNames = await page
    .getByTestId("models-row-name")
    .allTextContents();
  expect(stateSortedNames.slice(0, 2)).toEqual([
    "GPT-OSS 120B",
    "Minimax M2.7",
  ]);

  await page.getByTestId("models-sort-key").selectOption("pricing_input");
  const sortedNames = await page
    .getByTestId("models-row-name")
    .allTextContents();
  expect(sortedNames[0]).toBe("GPT-5.4");

  await page.getByTestId("models-search").fill("mini");
  visibleNames = await page.getByTestId("models-row-name").allTextContents();
  expect(visibleNames).toEqual(["Minimax M2.7"]);

  await page.getByTestId("models-set-default-minimax/minimax-m2.7").click();
  await expect(primaryRole("minimax/minimax-m2.7")).toHaveText("Primary");
  await page.getByRole("button", { name: "Discard", exact: true }).click();
  await page.getByTestId("models-search").fill("");
  await expect(primaryRole("openai/gpt-oss-120b")).toHaveText("Primary");
  await expect(primaryRole("minimax/minimax-m2.7")).toHaveCount(0);
});