SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
3 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
5.7 KB · 105 lines typescript
Raw Blame History
1import { afterEach, describe, expect, it } from "vitest";2import { capDiscovered } from "@dci/connectors";3import type { DiscoveredUrl } from "@dci/connectors";4import { fetchOutcome, metrics, renderMetrics, resetMetrics } from "./prom.js";5import { parseQueues } from "./env.js";6import { isDiscoveryGroup, premiumAllowedAfterErrors } from "./scheduling.js";7import { discoveryDue } from "./scheduler.js";8import { budgetKey, connectorBudgetKey } from "./budget.js";910afterEach(() => resetMetrics());1112describe("prometheus registry", () => {13  it("renders counters, gauges and histograms in exposition format", () => {14    metrics.fetches.inc({ connector: "equinix", level: "L1", outcome: "ok" });15    metrics.fetches.inc({ connector: "equinix", level: "L1", outcome: "ok" });16    metrics.credits.inc({ provider: "scrapfly" }, 5.5);17    metrics.dailyBudget.set(400, { provider: "scrapfly" });18    metrics.fetchDuration.observe(0.3);19    metrics.fetchDuration.observe(7);20    metrics.events.inc(undefined, 3);21    const out = renderMetrics();22    expect(out).toContain('dci_crawl_fetches_total{connector="equinix",level="L1",outcome="ok"} 2');23    expect(out).toContain('dci_crawl_credits_total{provider="scrapfly"} 5.5');24    expect(out).toContain('dci_crawl_daily_budget{provider="scrapfly"} 400');25    expect(out).toContain('dci_crawl_fetch_duration_seconds_bucket{le="0.5"} 1');26    expect(out).toContain('dci_crawl_fetch_duration_seconds_bucket{le="+Inf"} 2');27    expect(out).toContain("dci_crawl_fetch_duration_seconds_count 2");28    expect(out).toContain("dci_events_total 3");29    expect(out).toContain("# TYPE dci_queue_jobs gauge");30    expect(out.endsWith("\n")).toBe(true);31  });32  it("escapes label values and ignores zero increments", () => {33    metrics.ingest.inc({ connector: 'a"b\\c', result: "created" }, 0);34    metrics.ingest.inc({ connector: 'a"b\\c', result: "created" }, 2);35    expect(renderMetrics()).toContain('dci_ingest_entities_total{connector="a\\"b\\\\c",result="created"} 2');36  });37  it("classifies fetch outcomes", () => {38    expect(fetchOutcome({ notModified: true, status: 304 })).toBe("not_modified");39    expect(fetchOutcome({ notModified: false, status: 200 })).toBe("ok");40    expect(fetchOutcome({ notModified: false, status: 403 })).toBe("blocked");41    expect(fetchOutcome({ notModified: false, status: 500 })).toBe("error");42    expect(fetchOutcome({ notModified: false, status: 0, error: { code: "robots_disallow" } })).toBe("blocked");43    expect(fetchOutcome({ notModified: false, status: 0, error: { code: "timeout" } })).toBe("error");44  });45});4647describe("env / budgets", () => {48  it("parses DCI_QUEUES", () => {49    expect(parseQueues(undefined)).toEqual(["crawl", "maintenance"]);50    expect(parseQueues("maintenance")).toEqual(["maintenance"]);51    expect(parseQueues(" Crawl , bogus ")).toEqual(["crawl"]);52    expect(parseQueues("bogus")).toEqual(["crawl", "maintenance"]);53  });54  it("builds the Redis budget keys the API reads", () => {55    expect(budgetKey("scrapfly", "2026-09-11")).toBe("dci:budget:scrapfly:2026-09-11");56    expect(connectorBudgetKey("equinix", "2026-09-11")).toBe("dci:budget:connector:equinix:2026-09-11");57  });58});5960describe("cost control rules", () => {61  it("discovery groups are direct-only", () => {62    expect(isDiscoveryGroup("sitemap")).toBe(true);63    expect(isDiscoveryGroup("rss")).toBe(true);64    expect(isDiscoveryGroup("facility_pages")).toBe(false);65    expect(isDiscoveryGroup(undefined)).toBe(false);66  });67  it("premium retries are gated after two consecutive failures", () => {68    expect(premiumAllowedAfterErrors(0)).toBe(true);69    expect(premiumAllowedAfterErrors(1)).toBe(true);70    expect(premiumAllowedAfterErrors(2)).toBe(false);71    expect(premiumAllowedAfterErrors(3)).toBe(false);72    expect(premiumAllowedAfterErrors(4)).toBe(true);73    expect(premiumAllowedAfterErrors(5)).toBe(false);74    expect(premiumAllowedAfterErrors(8)).toBe(true);75  });76});7778describe("scheduler", () => {79  const now = new Date("2026-09-11T12:00:00Z");80  const H = 3_600_000;81  it("a never-discovered connector is due (full), one that was just discovered with zero docs is not", () => {82    expect(discoveryDue({ docCount: 0, lastDiscoverAt: null }, 6 * H, now)).toEqual({ due: true, task: "full" });83    expect(discoveryDue({ docCount: 0, lastDiscoverAt: new Date(now.getTime() - 5 * H).toISOString() }, 6 * H, now)).toEqual({ due: false, task: "full" });84    expect(discoveryDue({ docCount: 0, lastDiscoverAt: new Date(now.getTime() - 7 * H).toISOString() }, 6 * H, now)).toEqual({ due: true, task: "full" });85  });86  it("a populated connector re-discovers on cadence", () => {87    expect(discoveryDue({ docCount: 10, lastDiscoverAt: new Date(now.getTime() - 5 * H).toISOString() }, 6 * H, now)).toEqual({ due: false, task: "discover" });88    expect(discoveryDue({ docCount: 10, lastDiscoverAt: new Date(now.getTime() - 6 * H).toISOString() }, 6 * H, now)).toEqual({ due: true, task: "discover" });89  });90});9192describe("discovery cap", () => {93  const u = (url: string, group: string, priority: number): DiscoveredUrl => ({ url, group, priority });94  it("keeps the highest-priority groups instead of the first N sitemap entries", () => {95    const urls = [u("/blog/1", "default", 30), u("/blog/2", "default", 30), u("/dc/a", "facility_pages", 60), u("/news/x", "newsroom", 50), u("/dc/b", "facility_pages", 60)];96    expect(capDiscovered(urls, 3).map((x) => x.url)).toEqual(["/dc/a", "/dc/b", "/news/x"]);97    expect(capDiscovered(urls, 10)).toHaveLength(5);98    expect(capDiscovered(urls, 0)).toHaveLength(5);99  });100  it("is stable for equal priorities", () => {101    const urls = [u("/a", "default", 30), u("/b", "default", 30), u("/c", "default", 30)];102    expect(capDiscovered(urls, 2).map((x) => x.url)).toEqual(["/a", "/b"]);103  });104});105