| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- import { describe, expect, test, vi } from "vitest";
- import type { ProviderEndpoint } from "@/types/provider";
- function makeEndpoint(overrides: Partial<ProviderEndpoint>): ProviderEndpoint {
- return {
- id: 1,
- vendorId: 1,
- providerType: "claude",
- url: "https://example.com",
- label: null,
- sortOrder: 0,
- isEnabled: true,
- lastProbedAt: null,
- lastProbeOk: null,
- lastProbeStatusCode: null,
- lastProbeLatencyMs: null,
- lastProbeErrorType: null,
- lastProbeErrorMessage: null,
- createdAt: new Date(0),
- updatedAt: new Date(0),
- deletedAt: null,
- ...overrides,
- };
- }
- describe("provider-endpoints: endpoint-selector", () => {
- test("rankProviderEndpoints 应过滤 disabled/deleted,并按 lastProbeOk/sortOrder/latency/id 排序", async () => {
- vi.resetModules();
- vi.doMock("@/repository", () => ({
- findEnabledProviderEndpointsByVendorAndType: vi.fn(),
- findProviderEndpointsByVendorAndType: vi.fn(),
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: vi.fn(),
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: true }),
- }));
- const { rankProviderEndpoints } = await import("@/lib/provider-endpoints/endpoint-selector");
- const healthyHighOrder = makeEndpoint({
- id: 10,
- lastProbeOk: true,
- sortOrder: 1,
- lastProbeLatencyMs: 50,
- });
- const healthyLowOrder = makeEndpoint({
- id: 11,
- lastProbeOk: true,
- sortOrder: 0,
- lastProbeLatencyMs: 999,
- });
- const unknownFast = makeEndpoint({
- id: 20,
- lastProbeOk: null,
- sortOrder: 0,
- lastProbeLatencyMs: 10,
- });
- const unknownNoLatency = makeEndpoint({
- id: 21,
- lastProbeOk: null,
- sortOrder: 0,
- lastProbeLatencyMs: null,
- });
- const unhealthyFast30 = makeEndpoint({
- id: 30,
- lastProbeOk: false,
- sortOrder: 0,
- lastProbeLatencyMs: 1,
- });
- const unhealthyFast31 = makeEndpoint({
- id: 31,
- lastProbeOk: false,
- sortOrder: 0,
- lastProbeLatencyMs: 1,
- });
- const disabled = makeEndpoint({ id: 40, isEnabled: false, lastProbeOk: true });
- const deleted = makeEndpoint({ id: 41, deletedAt: new Date(1), lastProbeOk: true });
- const ranked = rankProviderEndpoints([
- healthyHighOrder,
- healthyLowOrder,
- unknownFast,
- unknownNoLatency,
- unhealthyFast30,
- unhealthyFast31,
- disabled,
- deleted,
- ]);
- expect(ranked.map((e) => e.id)).toEqual([11, 10, 20, 21, 30, 31]);
- });
- test("getPreferredProviderEndpoints 应排除禁用/已删除/显式 exclude/熔断 open 的端点,并返回排序结果", async () => {
- vi.resetModules();
- // findEnabledProviderEndpointsByVendorAndType 语义:只返回 isEnabled=true 且 deletedAt=null 的端点
- const endpoints: ProviderEndpoint[] = [
- makeEndpoint({ id: 1, lastProbeOk: true, sortOrder: 1, lastProbeLatencyMs: 20 }),
- makeEndpoint({ id: 2, lastProbeOk: true, sortOrder: 0, lastProbeLatencyMs: 999 }),
- makeEndpoint({ id: 3, lastProbeOk: null, sortOrder: 0, lastProbeLatencyMs: 10 }),
- makeEndpoint({ id: 6, lastProbeOk: false, sortOrder: 0, lastProbeLatencyMs: 1 }),
- ];
- const findMock = vi.fn(async () => endpoints);
- const getAllStatusMock = vi.fn(async (endpointIds: number[]) => {
- const status: Record<
- number,
- {
- failureCount: number;
- lastFailureTime: number | null;
- circuitState: "closed" | "open" | "half-open";
- circuitOpenUntil: number | null;
- halfOpenSuccessCount: number;
- }
- > = {};
- for (const endpointId of endpointIds) {
- status[endpointId] = {
- failureCount: 0,
- lastFailureTime: null,
- circuitState: endpointId === 2 ? "open" : "closed",
- circuitOpenUntil: endpointId === 2 ? Date.now() + 60_000 : null,
- halfOpenSuccessCount: 0,
- };
- }
- return status;
- });
- vi.doMock("@/repository", () => ({
- findEnabledProviderEndpointsByVendorAndType: findMock,
- findProviderEndpointsByVendorAndType: vi.fn(async () => []),
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: getAllStatusMock,
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: true }),
- }));
- const { getPreferredProviderEndpoints, pickBestProviderEndpoint } = await import(
- "@/lib/provider-endpoints/endpoint-selector"
- );
- const result = await getPreferredProviderEndpoints({
- vendorId: 123,
- providerType: "claude",
- excludeEndpointIds: [6],
- });
- expect(findMock).toHaveBeenCalledWith(123, "claude");
- expect(getAllStatusMock).toHaveBeenCalledWith([1, 2, 3]);
- expect(result.map((e) => e.id)).toEqual([1, 3]);
- const best = await pickBestProviderEndpoint({ vendorId: 123, providerType: "claude" });
- expect(best?.id).toBe(1);
- });
- test("getPreferredProviderEndpoints 过滤后无候选时返回空数组", async () => {
- vi.resetModules();
- const findMock = vi.fn(async () => []);
- const getAllStatusMock = vi.fn(async () => ({}));
- vi.doMock("@/repository", () => ({
- findEnabledProviderEndpointsByVendorAndType: findMock,
- findProviderEndpointsByVendorAndType: vi.fn(async () => []),
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: getAllStatusMock,
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: true }),
- }));
- const { getPreferredProviderEndpoints, pickBestProviderEndpoint } = await import(
- "@/lib/provider-endpoints/endpoint-selector"
- );
- const result = await getPreferredProviderEndpoints({ vendorId: 1, providerType: "claude" });
- expect(result).toEqual([]);
- const best = await pickBestProviderEndpoint({ vendorId: 1, providerType: "claude" });
- expect(best).toBeNull();
- expect(getAllStatusMock).not.toHaveBeenCalled();
- });
- });
- describe("getEndpointFilterStats", () => {
- test("should correctly count total, enabled, circuitOpen, and available endpoints", async () => {
- vi.resetModules();
- const endpoints: ProviderEndpoint[] = [
- makeEndpoint({ id: 1, isEnabled: true, lastProbeOk: true }),
- makeEndpoint({ id: 2, isEnabled: true, lastProbeOk: true }),
- makeEndpoint({ id: 3, isEnabled: true, lastProbeOk: false }),
- makeEndpoint({ id: 4, isEnabled: false }),
- makeEndpoint({ id: 5, deletedAt: new Date(1) }),
- ];
- const findMock = vi.fn(async () => endpoints);
- const getAllStatusMock = vi.fn(async (endpointIds: number[]) => {
- const status: Record<
- number,
- {
- failureCount: number;
- lastFailureTime: number | null;
- circuitState: "closed" | "open" | "half-open";
- circuitOpenUntil: number | null;
- halfOpenSuccessCount: number;
- }
- > = {};
- for (const endpointId of endpointIds) {
- status[endpointId] = {
- failureCount: 0,
- lastFailureTime: null,
- circuitState: endpointId === 2 ? "open" : "closed",
- circuitOpenUntil: endpointId === 2 ? Date.now() + 60_000 : null,
- halfOpenSuccessCount: 0,
- };
- }
- return status;
- });
- vi.doMock("@/repository", () => ({
- findProviderEndpointsByVendorAndType: findMock,
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: getAllStatusMock,
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: true }),
- }));
- const { getEndpointFilterStats } = await import("@/lib/provider-endpoints/endpoint-selector");
- const stats = await getEndpointFilterStats({ vendorId: 10, providerType: "claude" });
- expect(findMock).toHaveBeenCalledWith(10, "claude");
- expect(getAllStatusMock).toHaveBeenCalledWith([1, 2, 3]);
- expect(stats).toEqual({
- total: 5, // all endpoints
- enabled: 3, // id=1,2,3 (isEnabled && !deletedAt)
- circuitOpen: 1, // id=2
- available: 2, // enabled - circuitOpen = 3 - 1
- });
- });
- test("should return all zeros when no endpoints exist", async () => {
- vi.resetModules();
- const findMock = vi.fn(async () => []);
- const getAllStatusMock = vi.fn(async () => ({}));
- vi.doMock("@/repository", () => ({
- findProviderEndpointsByVendorAndType: findMock,
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: getAllStatusMock,
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: true }),
- }));
- const { getEndpointFilterStats } = await import("@/lib/provider-endpoints/endpoint-selector");
- const stats = await getEndpointFilterStats({ vendorId: 99, providerType: "codex" });
- expect(stats).toEqual({
- total: 0,
- enabled: 0,
- circuitOpen: 0,
- available: 0,
- });
- expect(getAllStatusMock).not.toHaveBeenCalled();
- });
- test("should count all enabled endpoints as circuitOpen when all are open", async () => {
- vi.resetModules();
- const endpoints: ProviderEndpoint[] = [
- makeEndpoint({ id: 1, isEnabled: true }),
- makeEndpoint({ id: 2, isEnabled: true }),
- ];
- const findMock = vi.fn(async () => endpoints);
- const getAllStatusMock = vi.fn(async (endpointIds: number[]) => {
- const status: Record<
- number,
- {
- failureCount: number;
- lastFailureTime: number | null;
- circuitState: "closed" | "open" | "half-open";
- circuitOpenUntil: number | null;
- halfOpenSuccessCount: number;
- }
- > = {};
- for (const endpointId of endpointIds) {
- status[endpointId] = {
- failureCount: 0,
- lastFailureTime: null,
- circuitState: "open",
- circuitOpenUntil: Date.now() + 60_000,
- halfOpenSuccessCount: 0,
- };
- }
- return status;
- });
- vi.doMock("@/repository", () => ({
- findEnabledProviderEndpointsByVendorAndType: vi.fn(async () => []),
- findProviderEndpointsByVendorAndType: findMock,
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: getAllStatusMock,
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: true }),
- }));
- const { getEndpointFilterStats } = await import("@/lib/provider-endpoints/endpoint-selector");
- const stats = await getEndpointFilterStats({ vendorId: 1, providerType: "openai-compatible" });
- expect(getAllStatusMock).toHaveBeenCalledWith([1, 2]);
- expect(stats).toEqual({
- total: 2,
- enabled: 2,
- circuitOpen: 2,
- available: 0,
- });
- });
- });
- describe("ENABLE_ENDPOINT_CIRCUIT_BREAKER disabled", () => {
- test("getPreferredProviderEndpoints skips circuit check when disabled", async () => {
- vi.resetModules();
- const endpoints: ProviderEndpoint[] = [
- makeEndpoint({ id: 1, lastProbeOk: true, sortOrder: 0, lastProbeLatencyMs: 100 }),
- makeEndpoint({ id: 2, lastProbeOk: true, sortOrder: 1, lastProbeLatencyMs: 50 }),
- makeEndpoint({ id: 3, lastProbeOk: false, sortOrder: 0, lastProbeLatencyMs: 10 }),
- makeEndpoint({ id: 4, isEnabled: false }),
- makeEndpoint({ id: 5, deletedAt: new Date(1) }),
- ];
- const findMock = vi.fn(async () => endpoints);
- const getAllStatusMock = vi.fn(async () => ({}));
- vi.doMock("@/repository", () => ({
- findEnabledProviderEndpointsByVendorAndType: findMock,
- findProviderEndpointsByVendorAndType: vi.fn(async () => []),
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: getAllStatusMock,
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: false }),
- }));
- const { getPreferredProviderEndpoints } = await import(
- "@/lib/provider-endpoints/endpoint-selector"
- );
- const result = await getPreferredProviderEndpoints({
- vendorId: 1,
- providerType: "claude",
- });
- expect(getAllStatusMock).not.toHaveBeenCalled();
- // All enabled, non-deleted endpoints returned (id=1,2,3), ranked by sortOrder/health
- expect(result.map((e) => e.id)).toEqual([1, 2, 3]);
- });
- test("getEndpointFilterStats returns circuitOpen=0 when disabled", async () => {
- vi.resetModules();
- const endpoints: ProviderEndpoint[] = [
- makeEndpoint({ id: 1, isEnabled: true, lastProbeOk: true }),
- makeEndpoint({ id: 2, isEnabled: true, lastProbeOk: false }),
- makeEndpoint({ id: 3, isEnabled: false }),
- makeEndpoint({ id: 4, deletedAt: new Date(1) }),
- ];
- const findMock = vi.fn(async () => endpoints);
- const getAllStatusMock = vi.fn(async () => ({}));
- vi.doMock("@/repository", () => ({
- findProviderEndpointsByVendorAndType: findMock,
- }));
- vi.doMock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: getAllStatusMock,
- }));
- vi.doMock("@/lib/config/env.schema", () => ({
- getEnvConfig: () => ({ ENABLE_ENDPOINT_CIRCUIT_BREAKER: false }),
- }));
- const { getEndpointFilterStats } = await import("@/lib/provider-endpoints/endpoint-selector");
- const stats = await getEndpointFilterStats({ vendorId: 10, providerType: "claude" });
- expect(getAllStatusMock).not.toHaveBeenCalled();
- expect(stats).toEqual({
- total: 4,
- enabled: 2, // id=1,2 (isEnabled && !deletedAt)
- circuitOpen: 0, // always 0 when disabled
- available: 2, // equals enabled when disabled
- });
- });
- });
|