import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// Mock fetch globally
const mockFetch = vi.fn()
global.fetch = mockFetch
describe('Sites Verification API', () => {
beforeEach(() => {
mockFetch.mockReset()
})
afterEach(() => {
vi.clearAllMocks()
})
describe('checkLlmsTxtUrl logic', () => {
it('should detect valid llms.txt content', () => {
const validContent = `# Project Documentation
## Getting Started
This is the documentation for our project.
`
// Content starts with # - valid
expect(validContent.trim().startsWith('#')).toBe(true)
})
it('should reject HTML error pages', () => {
const htmlContent = `
404 Not Found
Page not found
`
// Contains {
const shortContent = '# Title\nShort doc'
const longContent = '# Title\n' + 'Documentation line\n'.repeat(100)
// Short content is standard
expect(shortContent.split('\n').length > 50).toBe(false)
// Long content is full
expect(longContent.split('\n').length > 50).toBe(true)
})
it('should detect full llms.txt by heading depth', () => {
const standardContent = '# Title\nSome content'
const fullContent = '# Title\n## Section\n### Subsection\nContent'
expect(fullContent.includes('## ')).toBe(true)
expect(fullContent.includes('### ')).toBe(true)
})
})
describe('URL normalization', () => {
it('should extract origin from URL', () => {
const testUrls = [
{ input: 'https://docs.example.com/some/path', expected: 'https://docs.example.com' },
{ input: 'https://example.com/docs', expected: 'https://example.com' },
{ input: 'https://api.example.com:8080/v1', expected: 'https://api.example.com:8080' },
]
testUrls.forEach(({ input, expected }) => {
const url = new URL(input)
expect(url.origin).toBe(expected)
})
})
it('should handle various llms.txt locations', () => {
const baseUrl = 'https://example.com'
const locations = [
`${baseUrl}/llms.txt`,
`${baseUrl}/llms-full.txt`,
`${baseUrl}/docs/llms.txt`,
`${baseUrl}/docs/llms-full.txt`,
]
expect(locations).toHaveLength(4)
expect(locations[0]).toBe('https://example.com/llms.txt')
expect(locations[1]).toBe('https://example.com/llms-full.txt')
})
})
describe('Verification result structure', () => {
it('should have correct structure for online status', () => {
const result = {
status: 'online' as const,
llmsTxt: {
exists: true,
url: 'https://example.com/llms.txt',
size: 1024,
type: 'full' as const,
},
responseTime: 250,
checkedAt: new Date().toISOString(),
}
expect(result.status).toBe('online')
expect(result.llmsTxt?.exists).toBe(true)
expect(result.responseTime).toBeGreaterThan(0)
expect(result.checkedAt).toBeDefined()
})
it('should have correct structure for offline status', () => {
const result = {
status: 'offline' as const,
checkedAt: new Date().toISOString(),
responseTime: 100,
}
expect(result.status).toBe('offline')
expect(result.checkedAt).toBeDefined()
})
it('should have correct structure for error status', () => {
const result = {
status: 'error' as const,
checkedAt: new Date().toISOString(),
responseTime: 50,
error: 'Connection timeout',
}
expect(result.status).toBe('error')
expect(result.error).toBeDefined()
})
})
describe('Cache behavior', () => {
it('should cache results with TTL', () => {
const cache = new Map()
const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
const result = { status: 'online' }
const timestamp = Date.now()
cache.set('https://example.com', { result, timestamp })
// Check if cache is fresh
const cached = cache.get('https://example.com')
expect(cached).toBeDefined()
expect(Date.now() - cached!.timestamp < CACHE_TTL_MS).toBe(true)
})
it('should expire stale cache entries', () => {
const CACHE_TTL_MS = 5 * 60 * 1000
const staleTimestamp = Date.now() - CACHE_TTL_MS - 1000 // 1 second past expiry
const isFresh = Date.now() - staleTimestamp < CACHE_TTL_MS
expect(isFresh).toBe(false)
})
})
describe('Batch verification', () => {
it('should limit batch size', () => {
const maxBatchSize = 20
const largeArray = Array.from({ length: 50 }, (_, i) => `site-${i}`)
const limitedArray = largeArray.slice(0, maxBatchSize)
expect(limitedArray).toHaveLength(20)
})
it('should handle empty batch', () => {
const targets: string[] = []
expect(targets.length === 0).toBe(true)
})
})
})
describe('Sites Health API', () => {
describe('Health check result structure', () => {
it('should categorize status correctly', () => {
const categorize = (responseTime: number, found: boolean): string => {
if (!found) return 'offline'
if (responseTime > 5000) return 'slow'
return 'online'
}
expect(categorize(200, true)).toBe('online')
expect(categorize(6000, true)).toBe('slow')
expect(categorize(200, false)).toBe('offline')
})
it('should calculate summary statistics', () => {
const results = [
{ status: 'online', responseTime: 200 },
{ status: 'online', responseTime: 300 },
{ status: 'offline', responseTime: 100 },
{ status: 'error', responseTime: 50 },
{ status: 'slow', responseTime: 6000 },
]
const online = results.filter(r => r.status === 'online').length
const offline = results.filter(r => r.status === 'offline').length
const errors = results.filter(r => r.status === 'error').length
const slow = results.filter(r => r.status === 'slow').length
const avgResponseTime = results.reduce((sum, r) => sum + r.responseTime, 0) / results.length
expect(online).toBe(2)
expect(offline).toBe(1)
expect(errors).toBe(1)
expect(slow).toBe(1)
expect(avgResponseTime).toBe(1330)
})
})
describe('Cooldown mechanism', () => {
it('should enforce cooldown between health checks', () => {
const COOLDOWN = 60 * 1000 // 1 minute
let lastCheckTime = Date.now() - 30 * 1000 // 30 seconds ago
const canCheck = Date.now() - lastCheckTime >= COOLDOWN
expect(canCheck).toBe(false)
lastCheckTime = Date.now() - 61 * 1000 // 61 seconds ago
const canCheckNow = Date.now() - lastCheckTime >= COOLDOWN
expect(canCheckNow).toBe(true)
})
})
})
describe('Known Sites Data', () => {
it('should have required fields for each site', () => {
const requiredFields = ['id', 'name', 'url', 'category', 'description', 'llmsTxtType']
const site = {
id: 'example',
name: 'Example',
url: 'https://example.com',
category: 'developer-tools',
description: 'Example documentation',
llmsTxtType: 'full',
verified: true,
}
requiredFields.forEach(field => {
expect(site).toHaveProperty(field)
})
})
it('should have valid category values', () => {
const validCategories = ['ai', 'developer-tools', 'documentation', 'cloud', 'other']
const testCategories = ['ai', 'developer-tools', 'cloud']
testCategories.forEach(cat => {
expect(validCategories.includes(cat)).toBe(true)
})
})
it('should have valid llmsTxtType values', () => {
const validTypes = ['full', 'standard']
expect(validTypes.includes('full')).toBe(true)
expect(validTypes.includes('standard')).toBe(true)
expect(validTypes.includes('invalid')).toBe(false)
})
})