import { readFileSync } from "node:fs"; import { join } from "node:path"; import type { RawObservation } from "@market-atlas/market-model"; import { NormalizedObservationSchema } from "@market-atlas/market-model"; import { HttpClient } from "./http.js"; import { RateLimiter } from "./ratelimit.js"; import type { ConnectorContext, ConnectorDefinition, ConnectorLogger, NormalizedBatch } from "./types.js"; import { ManagedWebSocket, type ManagedWebSocketOptions } from "./ws.js"; /** Load a fixture file (JSON parsed when the extension is .json, raw text otherwise). */ export function loadFixture(dir: string, name: string): unknown { const text = readFileSync(join(dir, name), "utf8"); return name.endsWith(".json") ? JSON.parse(text) : text; } export function rawFromFixture(def: ConnectorDefinition, kind: string, payload: unknown, receivedAt = 1_789_200_000_000): RawObservation { return { connectorId: def.metadata.id, sourceId: def.metadata.sourceId, kind, payload, receivedAt }; } /** Runs normalize() on a payload and validates every observation against the canonical schema. */ export function normalizeFixture(def: ConnectorDefinition, kind: string, payload: unknown): NormalizedBatch { const batch = def.normalize(rawFromFixture(def, kind, payload)); for (const o of batch.observations) { const res = NormalizedObservationSchema.safeParse(o); if (!res.success) throw new Error(`invalid observation from ${def.metadata.id}: ${res.error.message}`); } return batch; } const silentLogger: ConnectorLogger = { debug() {}, info() {}, warn() {}, error() {} }; /** Offline connector context for tests: no network unless a fetchImpl is provided. */ export function makeTestContext( def: ConnectorDefinition, opts: { fetchImpl?: typeof fetch; marketOpen?: boolean; secrets?: Record; symbols?: string[] } = {}, ): ConnectorContext & { emitted: RawObservation[]; errors: unknown[]; sockets: ManagedWebSocket[] } { const emitted: RawObservation[] = []; const errors: unknown[] = []; const sockets: ManagedWebSocket[] = []; const store = new Map(); const http = new HttpClient({ userAgent: "MarketAtlas-test/0.1", limiter: new RateLimiter({ ratePerSec: 1000 }), fetchImpl: opts.fetchImpl ?? (async () => { throw new Error("network disabled in tests"); }), }); return { connectorId: def.metadata.id, logger: silentLogger, http, state: { async get(k) { return store.get(k) as never; }, async set(k, v) { store.set(k, v); }, }, emit(r) { emitted.push(...(Array.isArray(r) ? r : [r])); }, openWebSocket(url: string, o: ManagedWebSocketOptions) { const s = new ManagedWebSocket(url, o); sockets.push(s); return s; }, isMarketOpen: () => opts.marketOpen ?? true, watchedSymbols: () => opts.symbols ?? def.defaultSymbols ?? [], reportError: (e) => errors.push(e), secret: (n) => opts.secrets?.[n], now: () => Date.now(), emitted, errors, sockets, }; } /** Build a fake fetch that serves canned responses by URL substring. */ export function fakeFetch(routes: Record }>): typeof fetch { return (async (input: string | URL | Request) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const key = Object.keys(routes).find((k) => url.includes(k)); if (!key) return new Response("not found", { status: 404 }); const r = routes[key]!; const spec = typeof r === "string" ? { body: r } : r; return new Response(spec.body, { status: spec.status ?? 200, headers: spec.headers ?? {} }); }) as typeof fetch; }