aegida-console / lib / chat / catalog.test.ts
catalog.test.ts
Raw
// @vitest-environment node

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("server-only", () => ({}));

import {
  CatalogError,
  listGateModels,
  parseOpenAIModelList,
} from "@/lib/chat/catalog";

beforeEach(() => {
  vi.stubEnv("AEGIDA_GATE_URL", "https://gate.example.test");
});

afterEach(() => {
  vi.unstubAllEnvs();
  vi.unstubAllGlobals();
});

describe("Aegida model catalog", () => {
  it("parses provider-native models, removes invalid duplicates and guarantees auto", () => {
    expect(
      parseOpenAIModelList({
        object: "list",
        data: [
          { id: "qwen", object: "model", created: 1_785_758_400, owned_by: "qwen" },
          { id: "QWEN", object: "model", created: 1_785_758_400, owned_by: "qwen" },
          { id: "qwen", object: "model", created: 1_785_758_401, owned_by: "duplicate" },
          { id: "claude-sonnet.4", object: "model", created: 1_785_758_402, owned_by: "anthropic" },
        ],
      }),
    ).toEqual([
      {
        id: "auto",
        name: "Aegida Auto",
        description: "Автоматический выбор модели через Aegida Gate.",
      },
      { id: "qwen", name: "Qwen", description: "Владелец модели: qwen." },
      {
        id: "claude-sonnet.4",
        name: "Claude Sonnet 4",
        description: "Владелец модели: anthropic.",
      },
    ]);
  });

  it.each([
    null,
    {},
    { object: "collection", data: [] },
    { object: "list", data: "not-an-array" },
  ])("rejects a malformed top-level OpenAI model envelope %j", (value) => {
    expect(() => parseOpenAIModelList(value)).toThrow(CatalogError);
  });

  it("relays only the exact user bearer token with no-store semantics", async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      Response.json({
        object: "list",
        data: [
          { id: "auto", object: "model", created: 1_785_758_400, owned_by: "aegida" },
        ],
      }),
    );
    vi.stubGlobal("fetch", fetchMock);
    const signal = new AbortController().signal;

    await expect(listGateModels("Bearer exact-user-jwt", signal)).resolves.toHaveLength(1);

    expect(fetchMock).toHaveBeenCalledWith("https://gate.example.test/v1/models", {
      method: "GET",
      headers: { Accept: "application/json", Authorization: "Bearer exact-user-jwt" },
      cache: "no-store",
      redirect: "error",
      signal,
    });
    expect(JSON.stringify(fetchMock.mock.calls[0])).not.toContain("API_KEY");
  });

  it("classifies upstream authentication separately and never exposes dependency bodies", async () => {
    vi.stubGlobal(
      "fetch",
      vi
        .fn()
        .mockResolvedValueOnce(new Response("private-auth-body", { status: 401 }))
        .mockResolvedValueOnce(new Response("private-dependency-body", { status: 500 })),
    );

    await expect(listGateModels("Bearer user", new AbortController().signal)).rejects.toMatchObject({
      reason: "unauthorized",
      message: "Model catalog is unavailable",
    });
    await expect(listGateModels("Bearer user", new AbortController().signal)).rejects.toMatchObject({
      reason: "unavailable",
      message: "Model catalog is unavailable",
    });
  });
});