Copy // file: tests/platform.test.mjs
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import grpc from '@grpc/grpc-js';
import protoLoader from '@grpc/proto-loader';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { createServer } from '../src/grpc-server.mjs';
import { cache, spanExporter, breaker } from '../src/platform-service.mjs';
import { LRUCache } from '../src/cache.js';
const require = createRequire(import.meta.url);
const __dirname = dirname(fileURLToPath(import.meta.url));
let server, client, port;
beforeAll(async () => {
const result = await createServer(0); // random port
server = result.server;
port = result.port;
const def = protoLoader.loadSync(join(__dirname, '../proto/platform.proto'), {
keepCase: true, longs: String, enums: String, defaults: true, oneofs: true
});
const pkg = grpc.loadPackageDefinition(def).platform;
client = new pkg.PlatformService(`127.0.0.1:${port}`, grpc.credentials.createInsecure());
}, 15000);
afterAll(async () => {
server?.forceShutdown();
spanExporter.reset();
});
beforeEach(() => { spanExporter.reset(); });
// Helper
function callRPC(method, request, deadline = 5000) {
return new Promise((resolve, reject) => {
const dl = new Date(Date.now() + deadline);
client[method](request, { deadline: dl }, (err, res) => err ? reject(err) : resolve(res));
});
}
// === TEST 1: Basic hash task ===
it('processes a hash task', async () => {
const res = await callRPC('processTask', {
task_id: 'test-hash-1',
type: 'hash',
payload: Buffer.from('hello world'),
user_id: 'user-001',
});
expect(res.task_id).toBe('test-hash-1');
expect(res.result).toBeInstanceOf(Buffer);
expect(res.result.length).toBe(32); // SHA-256 = 32 bytes
expect(res.trace_id).toMatch(/^[0-9a-f]{32}$/);
});
// === TEST 2: Cache hit on repeated request ===
it('returns cached result on second call', async () => {
const req = { task_id: 'cache-test', type: 'hash', payload: Buffer.from('cache-test-payload'), user_id: 'u1' };
const r1 = await callRPC('processTask', req);
const r2 = await callRPC('processTask', { ...req, task_id: 'cache-test-2' });
expect(r1.from_cache).toBe(false);
expect(r2.from_cache).toBe(true);
});
// === TEST 3: OpenTelemetry spans are recorded ===
it('records OpenTelemetry spans', async () => {
await callRPC('processTask', {
task_id: 'span-test',
type: 'hash',
payload: Buffer.from('span-data'),
user_id: 'u-span',
});
const spans = spanExporter.getFinishedSpans();
expect(spans.length).toBeGreaterThanOrEqual(1);
const taskSpan = spans.find(s => s.name === 'process-task');
expect(taskSpan).toBeDefined();
expect(taskSpan.attributes['task.id']).toBe('span-test');
expect(taskSpan.attributes['user.id']).toBe('u-span');
});
// === TEST 4: getStats returns valid metrics ===
it('returns valid stats', async () => {
// Run a few tasks first
await Promise.all([
callRPC('processTask', { task_id: 's1', type: 'hash', payload: Buffer.from('a'), user_id: 'u' }),
callRPC('processTask', { task_id: 's2', type: 'hash', payload: Buffer.from('b'), user_id: 'u' }),
]);
const stats = await callRPC('getStats', {});
expect(stats.requests_total).toBeGreaterThan(0);
expect(['OPEN', 'HALF-OPEN', 'CLOSED']).toContain(stats.circuit_state);
expect(stats.avg_latency_ms).toBeGreaterThanOrEqual(0);
});
// === TEST 5: LRU cache unit test ===
it('LRU cache evicts correctly', () => {
const c = new LRUCache(3);
c.set('a', 1); c.set('b', 2); c.set('c', 3);
c.get('a'); // promote a
c.set('d', 4); // should evict b
expect(c.get('b')).toBeNull();
expect(c.get('a')).toBe(1);
expect(c.get('d')).toBe(4);
});
// === TEST 6: Concurrent tasks complete correctly ===
it('handles concurrent task requests', async () => {
const tasks = Array.from({ length: 10 }, (_, i) =>
callRPC('processTask', {
task_id: `concurrent-${i}`,
type: 'hash',
payload: Buffer.from(`payload-${i}`),
user_id: 'u-concurrent',
})
);
const results = await Promise.all(tasks);
expect(results).toHaveLength(10);
results.forEach((r, i) => {
expect(r.task_id).toBe(`concurrent-${i}`);
expect(r.result.length).toBe(32);
});
});