import { describe, expect, it } from "vitest"; import { intervalMs, parseConnectorConfig, validateAgainstRegistry } from "./config.js"; import { registerParser } from "./registry.js"; const BASE = `id: t\nname: T\ndomain: x.com\nkind: operator\n`; describe("connector config cross-field validation", () => { it("rejects level > maxLevel", () => { expect(() => parseConnectorConfig(`${BASE}fetch: { level: 3, maxLevel: 2 }`)).toThrow(/fetch.level 3 exceeds fetch.maxLevel 2/); }); it("rejects a per-run credit budget above the daily one on premium connectors", () => { expect(() => parseConnectorConfig(`${BASE}fetch: { maxLevel: 4, maxCreditsPerRun: 200, maxCreditsPerDay: 60 }`)).toThrow(/maxCreditsPerRun 200 exceeds fetch.maxCreditsPerDay 60/); expect(() => parseConnectorConfig(`${BASE}fetch: { maxLevel: 2, maxCreditsPerRun: 200, maxCreditsPerDay: 0 }`)).not.toThrow(); }); it("compiles every discovery / extractor regex at parse time", () => { expect(() => parseConnectorConfig(`${BASE}discovery: { include: ["/ok/", "(unclosed"] }`)).toThrow(/discovery.include.1: invalid regex/); expect(() => parseConnectorConfig(`${BASE}discovery: { classify: [{ pattern: "[a-", group: g }] }`)).toThrow(/classify.0.pattern/); expect(() => parseConnectorConfig(`${BASE}extractors: { f: { parser: p, match: ["*bad"] } }`)).toThrow(/extractors.f.match.0/); expect(() => parseConnectorConfig(`${BASE}extractors: { f: { fields: { name: { selector: h1, regex: "(" } } } }`)).toThrow(/fields.name.regex/); }); it("rejects seeds / classify rules whose minLevel exceeds maxLevel and extractors without parser or fields", () => { expect(() => parseConnectorConfig(`${BASE}fetch: { maxLevel: 2 }\ndiscovery: { seeds: [{ url: /a, minLevel: 4 }] }`)).toThrow(/minLevel 4 exceeds/); expect(() => parseConnectorConfig(`${BASE}extractors: { f: { kind: facility } }`)).toThrow(/needs either/); }); it("rejects bad schedule intervals and accepts the documented forms", () => { expect(() => parseConnectorConfig(`${BASE}schedule: { newsroom: soonish }`)).toThrow(/bad interval "soonish"/); const cfg = parseConnectorConfig(`${BASE}schedule: { a: daily, b: 6h, c: 30m, d: never, e: quarterly }`); expect(cfg.schedule.d).toBe("never"); expect(intervalMs("6h")).toBe(6 * 3_600_000); expect(intervalMs("never")).toBe(Number.POSITIVE_INFINITY); expect(() => intervalMs("2y")).toThrow(/bad interval/); }); it("applies defaults", () => { const cfg = parseConnectorConfig(BASE); expect(cfg.fetch).toMatchObject({ level: 1, maxLevel: 2, rpm: 30, respectRobots: true, maxCreditsPerRun: 200 }); expect(cfg.schedule).toEqual({ seed: "weekly" }); }); }); describe("validateAgainstRegistry", () => { it("is a no-op while nothing is registered, then reports unknown parsers / implementations", () => { const cfg = parseConnectorConfig(`${BASE}extractors: { f: { parser: nope_v1 } }`); expect(validateAgainstRegistry(cfg)).toEqual([]); registerParser({ name: "known_v1", version: "1", parse: () => [] }); expect(validateAgainstRegistry(cfg)).toEqual(['extractors.f.parser "nope_v1" is not registered']); expect(validateAgainstRegistry(parseConnectorConfig(`${BASE}extractors: { f: { parser: known_v1 } }`))).toEqual([]); expect(validateAgainstRegistry(parseConnectorConfig(`${BASE}implementation: ghost`))).toEqual(['implementation "ghost" is not registered']); }); });