import { describe, expect, it } from 'vitest'; import { ConnectorMetaSchema } from '../types.js'; import { HostGates, CircuitOpenError } from '../circuit.js'; import { policyFor, setDomains } from '../domains.js'; import { isChallengePage } from '../quality.js'; import { parseRobots, robotsAllows, parseSitemapIndex, parseUrlset, isSitemapIndex } from './sitemap.js'; import { parseFeed } from './rss.js'; import { productsFromHtml } from './schemaorg.js'; import { parsePricesRealised } from './pdf.js'; import { identifiersFromBarcode, mapProduct, StorefrontConfigSchema } from './storefront.js'; import { ShopifyStoreConnector } from './shopify.js'; import { WooCommerceStoreConnector } from './woocommerce.js'; describe('domain policy', () => { it('merges defaults → parent domain → host', () => { setDomains({ version: '1', defaults: { minIntervalMs: 1000, concurrency: 2 }, domains: { 'example.com': { minIntervalMs: 3000 }, 'shop.example.com': { concurrency: 1 } } }); const p = policyFor('https://www.shop.example.com/products.json'); expect(p.minIntervalMs).toBe(3000); expect(p.concurrency).toBe(1); expect(policyFor('other.org').minIntervalMs).toBe(1000); }); }); describe('circuit breaker', () => { it('opens after consecutive failures and half-opens after cooldown', async () => { const gates = new HostGates(() => ({ concurrency: 2, minIntervalMs: 0, circuitFailures: 2, circuitCooldownMs: 50 })); (await gates.acquire('h'))(); gates.report('h', false); (await gates.acquire('h'))(); gates.report('h', false); await expect(gates.acquire('h')).rejects.toBeInstanceOf(CircuitOpenError); await new Promise((r) => setTimeout(r, 60)); const rel = await gates.acquire('h'); rel(); gates.report('h', true); expect(gates.snapshot('h').state).toBe('closed'); }); }); describe('challenge detection', () => { it('flags short cloudflare interstitials, not long pages mentioning cloudflare', () => { expect(isChallengePage({ html: '
Holo
', published_at: '2026-08-01T00:00:00Z', vendor: 'Pokemon', product_type: 'Single', tags: ['Base Set'], variants: [{ id: 1, title: 'Near Mint', sku: 'PK-4-NM', price: '1999.99', available: true }, { id: 2, title: 'Lightly Played', sku: 'PK-4-LP', price: '1500.00', available: false }], images: [{ src: 'https://cdn/1.jpg' }] }, }; const out = await c.normalize({ url: 'https://demo.example/products/charizard-base-4', kind: 'listing', engine: 'api', payload, fetchedAt: new Date('2026-09-08T00:00:00Z') }); expect(out).toHaveLength(2); const [a, b] = out; expect(a?.kind).toBe('listing'); if (a?.kind === 'listing' && b?.kind === 'listing') { expect(a.currency).toBe('CAD'); expect(a.price).toBe(1999.99); expect(a.externalId).toBe('71:1'); expect(a.attributes.identifiers.sku).toBe('PK-4-NM'); expect(a.grade.grader).toBe('psa'); expect(a.grade.grade).toBe('9'); expect(a.condition.conditionRaw).toBe('Near Mint'); expect(b.availability).toBe('ended'); expect(a.attributes.franchise).toBe('Pokémon'); } }); }); describe('woocommerce adapter', () => { it('normalises Store API minor-unit prices with the payload currency', async () => { const c = new WooCommerceStoreConnector({ ...meta, config: { currency: 'USD', collections: [{ handle: 'sealed', categorySlug: 'pokemon' }] } }); const out = await c.normalize({ url: 'https://demo.example/product/x', kind: 'listing', engine: 'api', fetchedAt: new Date(), payload: { category: 'sealed', product: { id: 5, name: 'Booster Box Evolving Skies', permalink: 'https://demo.example/product/x', sku: 'BB-ES', prices: { price: '24999', regular_price: '29999', currency_code: 'GBP', currency_minor_unit: 2 }, images: [], categories: [{ name: 'Sealed', slug: 'sealed' }], tags: [], is_in_stock: true } } }); expect(out).toHaveLength(1); if (out[0]?.kind === 'listing') { expect(out[0].price).toBe(249.99); expect(out[0].currency).toBe('GBP'); expect(out[0].condition.completeness).toBeNull(); } }); });