SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
5.0 KB · 99 lines typescript
Raw Blame History
1import { describe, expect, it } from "vitest";2import { ProviderRegistry } from "@fetcha/providers";3import { CircuitBreaker, RoutingEngine, foldRouteStat, preferredRoute, routeKey } from "../src/index";45const env = {6  OXYLABS_USERNAME: "customer-test",7  OXYLABS_PASSWORD: "pw",8  DECODO_USERNAME: "testuser",9  DECODO_PASSWORD: "pw",10} as NodeJS.ProcessEnv;1112const geo = { country: null, region: null, city: null };1314describe("RoutingEngine", () => {15  it("only routes to configured providers", () => {16    const registry = new ProviderRegistry({ env });17    expect(registry.available().map((p) => p.id).sort()).toEqual(["decodo", "oxylabs"]);18    expect(registry.availableNetworks()).toEqual(["residential"]);19    const engine = new RoutingEngine(registry, new CircuitBreaker());20    const plan = engine.plan({ domain: "example.com", network: "auto", geo, plan: "unlimited", sessionRequired: false, browser: false });21    expect(plan.candidates.length).toBeGreaterThan(0);22    expect(plan.candidates.every((c) => c.network === "residential")).toBe(true);23    expect(plan.maxAttempts).toBe(6); // unlimited plan: 5 retries + 124  });2526  it("prefers the cheaper provider when nothing else differs", () => {27    const registry = new ProviderRegistry({ env, prices: { oxylabs: { residential: 8 }, decodo: { residential: 7 } } });28    const engine = new RoutingEngine(registry, new CircuitBreaker());29    const plan = engine.plan({ domain: "example.com", network: "residential", geo, plan: "unlimited", sessionRequired: false, browser: false });30    expect(plan.candidates[0]!.provider.id).toBe("decodo");31  });3233  it("uses historical success from domain knowledge", () => {34    const registry = new ProviderRegistry({ env, prices: { oxylabs: { residential: 7 }, decodo: { residential: 7 } } });35    const engine = new RoutingEngine(registry, new CircuitBreaker());36    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 } };37    const plan = engine.plan({ domain: "hard.example", network: "residential", geo, plan: "unlimited", sessionRequired: false, browser: false, knowledge: { domain: "hard.example", routeStats: stats } });38    expect(plan.candidates[0]!.provider.id).toBe("oxylabs");39  });4041  it("skips providers with an open circuit", () => {42    const registry = new ProviderRegistry({ env });43    const circuit = new CircuitBreaker({ minSamples: 2, failureThreshold: 0.5 });44    for (let i = 0; i < 4; i++) circuit.record(routeKey("decodo", "residential"), false);45    expect(circuit.state(routeKey("decodo", "residential"))).toBe("open");46    const engine = new RoutingEngine(registry, circuit);47    const plan = engine.plan({ domain: "x.com", network: "residential", geo, plan: "unlimited", sessionRequired: false, browser: false });48    expect(plan.candidates.every((c) => c.provider.id !== "decodo")).toBe(true);49  });5051  it("honours pinned domain policies", () => {52    const registry = new ProviderRegistry({ env, prices: { oxylabs: { residential: 20 }, decodo: { residential: 1 } } });53    const engine = new RoutingEngine(registry, new CircuitBreaker());54    const plan = engine.plan({55      domain: "pinned.example",56      network: "residential",57      geo,58      plan: "unlimited",59      sessionRequired: false,60      browser: false,61      knowledge: { domain: "pinned.example", routeStats: {}, policy: { order: [routeKey("oxylabs", "residential")] } },62    });63    expect(plan.candidates[0]!.provider.id).toBe("oxylabs");64  });6566  it("returns no candidates for unavailable network classes", () => {67    const registry = new ProviderRegistry({ env });68    const engine = new RoutingEngine(registry, new CircuitBreaker());69    const plan = engine.plan({ domain: "x.com", network: "mobile", geo, plan: "unlimited", sessionRequired: false, browser: false });70    expect(plan.candidates).toHaveLength(0);71  });72});7374describe("CircuitBreaker", () => {75  it("opens, half-opens after cooldown and closes on success", () => {76    const c = new CircuitBreaker({ minSamples: 3, failureThreshold: 0.6, cooldownMs: 1000 });77    const now = 1_000_000;78    c.record("k", false, now);79    c.record("k", false, now);80    c.record("k", false, now);81    expect(c.state("k")).toBe("open");82    expect(c.allow("k", now + 10)).toBe(false);83    expect(c.allow("k", now + 1001)).toBe(true);84    expect(c.state("k")).toBe("half_open");85    c.record("k", true, now + 1002);86    expect(c.state("k")).toBe("closed");87    expect(c.healthFactor("k")).toBe(1);88  });89});9091describe("route stats", () => {92  it("folds attempts and picks the preferred route by success and cost", () => {93    const stats = {};94    for (let i = 0; i < 5; i++) foldRouteStat(stats, "oxylabs:residential", { ok: true, blocked: false, latencyMs: 500, costUsd: 0.001 });95    for (let i = 0; i < 5; i++) foldRouteStat(stats, "decodo:residential", { ok: i < 2, blocked: i >= 2, latencyMs: 900, costUsd: 0.001 });96    expect(preferredRoute(stats)).toEqual({ provider: "oxylabs", network: "residential" });97  });98});99