brainy/tests/statistics-storage.test.ts
dpsifr 23c34d5e55 **feat(tests, docs, storage): add statistics storage tests and enhance documentation**
- **Tests**: Added new `statistics-storage.test.ts` to validate statistics storage functionality across scenarios including saving, retrieving, time-based partitioning, and backward compatibility. Ensured tests dynamically handle missing environment variables by skipping S3-related tests when credentials are unavailable.
- **Docs**: Enhanced `statistics.md` with detailed explanations of scalability improvements, including adaptive flush timing, batched updates, and time-based partitioning. Improved readability and structure.
- **Storage**: Updated all storage adapters to integrate time-based partitioning and maintain backward compatibility with legacy statistics storage formats.
- **Dependencies**: Added `dotenv` to support environmental variable management for storage adapter tests.

**Purpose**: Strengthen system reliability by adding comprehensive test coverage for statistics storage, improve scalability documentation, and ensure consistency across storage adapters with robust implementations.
2025-07-24 16:24:02 -07:00

158 lines
No EOL
5.5 KiB
TypeScript

/**
* Test script for the statistics storage implementation
*
* This script tests:
* 1. Saving statistics data
* 2. Retrieving statistics data
* 3. Verifying that the data is correctly saved and retrieved
* 4. Checking that time-based partitioning works correctly
* 5. Checking that backward compatibility is maintained
*/
// Import required modules
// @ts-expect-error - dotenv doesn't have TypeScript types
import { config } from 'dotenv'
import { setTimeout } from 'timers/promises'
import { describe, it, expect, beforeAll, beforeEach } from 'vitest'
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3'
import * as process from 'process'
// Define types for statistics data
interface ServiceStatistics {
nounCount: number
verbCount: number
metadataCount: number
}
interface StatisticsData {
nounCount: Record<string, number>
verbCount: Record<string, number>
metadataCount: Record<string, number>
hnswIndexSize: number
lastUpdated: string
}
// Define types for storage configuration
interface S3StorageConfig {
endpoint: string
region: string
bucketName: string
accessKeyId: string
secretAccessKey: string
prefix: string
serviceType?: string
sessionToken?: string
accountId?: string
}
// Load environment variables
config()
// Create test statistics data
const testStatistics: StatisticsData = {
nounCount: { 'test-service': 100, 'another-service': 50 },
verbCount: { 'test-service': 75, 'another-service': 25 },
metadataCount: { 'test-service': 100, 'another-service': 50 },
hnswIndexSize: 150,
lastUpdated: new Date().toISOString()
}
// Test configuration
const storageConfig: S3StorageConfig = {
endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000',
region: process.env.S3_REGION || 'us-east-1',
bucketName: process.env.S3_BUCKET || 'test-bucket',
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
prefix: 'test-statistics/'
}
// Check if required S3 credentials are available
const hasS3Credentials = !!process.env.S3_ACCESS_KEY && !!process.env.S3_SECRET_KEY;
// Use conditional describe to skip all tests if credentials are missing
(hasS3Credentials ? describe : describe.skip)('Statistics Storage', () => {
let storage: any
let s3Client: S3Client
beforeAll(async () => {
if (!hasS3Credentials) {
console.log('Skipping S3 storage tests: S3_ACCESS_KEY or S3_SECRET_KEY environment variables not set')
return
}
try {
// Import S3CompatibleStorage dynamically to avoid issues with dynamic imports
const { S3CompatibleStorage } = await import('../dist/storage/adapters/s3CompatibleStorage')
// Create storage instance
storage = new S3CompatibleStorage(storageConfig)
await storage.init()
// Initialize S3 client for checking files
s3Client = new S3Client({
endpoint: storageConfig.endpoint,
region: storageConfig.region,
credentials: {
accessKeyId: storageConfig.accessKeyId,
secretAccessKey: storageConfig.secretAccessKey
}
})
} catch (error) {
console.log('Error initializing S3 storage:', error)
throw error // Let the test fail with a clear error message
}
})
it('should save statistics data', async () => {
await storage.saveStatistics(testStatistics)
expect(true).toBe(true) // If no error is thrown, the test passes
})
it('should retrieve statistics data after batch update completes', async () => {
// Wait for the batch update to complete (longer than MAX_FLUSH_DELAY_MS)
await setTimeout(35000)
const retrievedStats = await storage.getStatistics()
expect(retrievedStats).not.toBeNull()
// Check that all properties match
expect(JSON.stringify(retrievedStats.nounCount)).toBe(JSON.stringify(testStatistics.nounCount))
expect(JSON.stringify(retrievedStats.verbCount)).toBe(JSON.stringify(testStatistics.verbCount))
expect(JSON.stringify(retrievedStats.metadataCount)).toBe(JSON.stringify(testStatistics.metadataCount))
expect(retrievedStats.hnswIndexSize).toBe(testStatistics.hnswIndexSize)
})
it('should store statistics in time-partitioned files', async () => {
// Get current date in YYYYMMDD format
const now = new Date()
const year = now.getUTCFullYear()
const month = String(now.getUTCMonth() + 1).padStart(2, '0')
const day = String(now.getUTCDate()).padStart(2, '0')
const dateStr = `${year}${month}${day}`
// Check if the file exists in the expected location
const listResponse = await s3Client.send(new ListObjectsV2Command({
Bucket: storageConfig.bucketName,
Prefix: `${storageConfig.prefix}index/statistics_${dateStr}`
}))
expect(listResponse.Contents).toBeDefined()
expect(listResponse.Contents?.length).toBeGreaterThan(0)
})
it('should maintain backward compatibility with legacy statistics file', async () => {
// Check if the legacy file exists
const legacyListResponse = await s3Client.send(new ListObjectsV2Command({
Bucket: storageConfig.bucketName,
Prefix: `${storageConfig.prefix}index/statistics.json`
}))
// This test is informational - the legacy file may not exist if the 10% random update didn't trigger
if (legacyListResponse.Contents && legacyListResponse.Contents.length > 0) {
expect(legacyListResponse.Contents.length).toBeGreaterThan(0)
} else {
console.log('Legacy statistics file not found. This is expected if the 10% random update didn\'t trigger.')
}
})
})