import { describe, expect, it } from "vitest"; import { ProviderRegistry } from "@fetcha/providers"; import { CircuitBreaker, RoutingEngine, foldRouteStat, preferredRoute, routeKey } from "../src/index"; const env = { OXYLABS_USERNAME: "customer-test", OXYLABS_PASSWORD: "pw", DECODO_USERNAME: "testuser", DECODO_PASSWORD: "pw", } as NodeJS.ProcessEnv; const geo = { country: null, region: null, city: null }; describe("RoutingEngine", () => { it("only routes to configured providers", () => { const registry = new ProviderRegistry({ env }); expect(registry.available().map((p) => p.id).sort()).toEqual(["decodo", "oxylabs"]); expect(registry.availableNetworks()).toEqual(["residential"]); const engine = new RoutingEngine(registry, new CircuitBreaker()); const plan = engine.plan({ domain: "example.com", network: "auto", geo, plan: "unlimited", sessionRequired: false, browser: false }); expect(plan.candidates.length).toBeGreaterThan(0); expect(plan.candidates.every((c) => c.network === "residential")).toBe(true); expect(plan.maxAttempts).toBe(6); // unlimited plan: 5 retries + 1 }); it("prefers the cheaper provider when nothing else differs", () => { const registry = new ProviderRegistry({ env, prices: { oxylabs: { residential: 8 }, decodo: { residential: 7 } } }); const engine = new RoutingEngine(registry, new CircuitBreaker()); const plan = engine.plan({ domain: "example.com", network: "residential", geo, plan: "unlimited", sessionRequired: false, browser: false }); expect(plan.candidates[0]!.provider.id).toBe("decodo"); }); it("uses historical success from domain knowledge", () => { const registry = new ProviderRegistry({ env, prices: { oxylabs: { residential: 7 }, decodo: { residential: 7 } } }); const engine = new RoutingEngine(registry, new CircuitBreaker()); const stats = { [routeKey("oxylabs", "residential")]: { n: 50, ok: 50, blocked: 0, lat: 400, cost: 0.01 }, [routeKey("decodo", "residential")]: { n: 50, ok: 10, blocked: 40, lat: 1500, cost: 0.01 } }; const plan = engine.plan({ domain: "hard.example", network: "residential", geo, plan: "unlimited", sessionRequired: false, browser: false, knowledge: { domain: "hard.example", routeStats: stats } }); expect(plan.candidates[0]!.provider.id).toBe("oxylabs"); }); it("skips providers with an open circuit", () => { const registry = new ProviderRegistry({ env }); const circuit = new CircuitBreaker({ minSamples: 2, failureThreshold: 0.5 }); for (let i = 0; i < 4; i++) circuit.record(routeKey("decodo", "residential"), false); expect(circuit.state(routeKey("decodo", "residential"))).toBe("open"); const engine = new RoutingEngine(registry, circuit); const plan = engine.plan({ domain: "x.com", network: "residential", geo, plan: "unlimited", sessionRequired: false, browser: false }); expect(plan.candidates.every((c) => c.provider.id !== "decodo")).toBe(true); }); it("honours pinned domain policies", () => { const registry = new ProviderRegistry({ env, prices: { oxylabs: { residential: 20 }, decodo: { residential: 1 } } }); const engine = new RoutingEngine(registry, new CircuitBreaker()); const plan = engine.plan({ domain: "pinned.example", network: "residential", geo, plan: "unlimited", sessionRequired: false, browser: false, knowledge: { domain: "pinned.example", routeStats: {}, policy: { order: [routeKey("oxylabs", "residential")] } }, }); expect(plan.candidates[0]!.provider.id).toBe("oxylabs"); }); it("returns no candidates for unavailable network classes", () => { const registry = new ProviderRegistry({ env }); const engine = new RoutingEngine(registry, new CircuitBreaker()); const plan = engine.plan({ domain: "x.com", network: "mobile", geo, plan: "unlimited", sessionRequired: false, browser: false }); expect(plan.candidates).toHaveLength(0); }); }); describe("CircuitBreaker", () => { it("opens, half-opens after cooldown and closes on success", () => { const c = new CircuitBreaker({ minSamples: 3, failureThreshold: 0.6, cooldownMs: 1000 }); const now = 1_000_000; c.record("k", false, now); c.record("k", false, now); c.record("k", false, now); expect(c.state("k")).toBe("open"); expect(c.allow("k", now + 10)).toBe(false); expect(c.allow("k", now + 1001)).toBe(true); expect(c.state("k")).toBe("half_open"); c.record("k", true, now + 1002); expect(c.state("k")).toBe("closed"); expect(c.healthFactor("k")).toBe(1); }); }); describe("route stats", () => { it("folds attempts and picks the preferred route by success and cost", () => { const stats = {}; for (let i = 0; i < 5; i++) foldRouteStat(stats, "oxylabs:residential", { ok: true, blocked: false, latencyMs: 500, costUsd: 0.001 }); for (let i = 0; i < 5; i++) foldRouteStat(stats, "decodo:residential", { ok: i < 2, blocked: i >= 2, latencyMs: 900, costUsd: 0.001 }); expect(preferredRoute(stats)).toEqual({ provider: "oxylabs", network: "residential" }); }); });