test(diagnostics): add diagnostics.test.ts with 14+ tests for Phase 1
This commit is contained in:
parent
d444a8dfea
commit
fb71981e53
@ -0,0 +1,351 @@
|
|||||||
|
/**
|
||||||
|
* Diagnostics module tests — Phase 1: Server Foundation
|
||||||
|
*
|
||||||
|
* @module diagnostics
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import * as repo from './repository.js';
|
||||||
|
import {
|
||||||
|
CreateDebugSessionSchema,
|
||||||
|
UpdateDebugSessionSchema,
|
||||||
|
IngestTracesSchema,
|
||||||
|
IngestLogsSchema,
|
||||||
|
type DebugSessionDoc,
|
||||||
|
type DebugTraceDoc,
|
||||||
|
type DebugLogEntryDoc,
|
||||||
|
type CreateDebugSessionInput,
|
||||||
|
type UpdateDebugSessionInput,
|
||||||
|
type IngestTracesInput,
|
||||||
|
type IngestLogsInput,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
// Test helpers
|
||||||
|
function generateId(prefix: string): string {
|
||||||
|
return `${prefix}_${randomUUID().replace(/-/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestSession(productId: string, overrides?: Partial<DebugSessionDoc>): DebugSessionDoc {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
id: generateId('ds'),
|
||||||
|
productId,
|
||||||
|
targetUserId: `user_${randomUUID()}`,
|
||||||
|
status: 'pending',
|
||||||
|
collectionLevel: 'debug',
|
||||||
|
captureLogs: true,
|
||||||
|
captureNetwork: true,
|
||||||
|
captureScreenshots: false,
|
||||||
|
screenshotOnError: true,
|
||||||
|
maxDurationMinutes: 60,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
expiresAt: new Date(Date.now() + 60 * 60000).toISOString(),
|
||||||
|
logCount: 0,
|
||||||
|
traceCount: 0,
|
||||||
|
screenshotCount: 0,
|
||||||
|
createdBy: 'test_admin',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Session CRUD Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Session CRUD', () => {
|
||||||
|
const productId = 'test_product';
|
||||||
|
|
||||||
|
it('should create a session', async () => {
|
||||||
|
const session = createTestSession(productId);
|
||||||
|
const created = await repo.createSession(session);
|
||||||
|
expect(created.id).toBe(session.id);
|
||||||
|
expect(created.status).toBe('pending');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a session by id', async () => {
|
||||||
|
const session = createTestSession(productId);
|
||||||
|
await repo.createSession(session);
|
||||||
|
|
||||||
|
const found = await repo.getSession(session.id);
|
||||||
|
expect(found).not.toBeNull();
|
||||||
|
expect(found?.id).toBe(session.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for non-existent session', async () => {
|
||||||
|
const found = await repo.getSession('non_existent_id');
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a session', async () => {
|
||||||
|
const session = createTestSession(productId);
|
||||||
|
await repo.createSession(session);
|
||||||
|
|
||||||
|
const updated = await repo.updateSession(session.id, { status: 'active' });
|
||||||
|
expect(updated).not.toBeNull();
|
||||||
|
expect(updated?.status).toBe('active');
|
||||||
|
expect(updated?.updatedAt).not.toBe(session.updatedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when updating non-existent session', async () => {
|
||||||
|
const updated = await repo.updateSession('non_existent', { status: 'active' });
|
||||||
|
expect(updated).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should soft delete (cancel) a session', async () => {
|
||||||
|
const session = createTestSession(productId);
|
||||||
|
await repo.createSession(session);
|
||||||
|
|
||||||
|
const result = await repo.deleteSession(session.id);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
const found = await repo.getSession(session.id);
|
||||||
|
expect(found?.status).toBe('cancelled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list sessions with filters', async () => {
|
||||||
|
const session1 = createTestSession(productId, { status: 'active' });
|
||||||
|
const session2 = createTestSession(productId, { status: 'pending' });
|
||||||
|
await repo.createSession(session1);
|
||||||
|
await repo.createSession(session2);
|
||||||
|
|
||||||
|
const { sessions } = await repo.listSessions({
|
||||||
|
productId,
|
||||||
|
status: 'active',
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sessions.length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(sessions.some((s) => s.id === session1.id)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Trace Ingest Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Trace Ingest', () => {
|
||||||
|
const productId = 'test_product';
|
||||||
|
const sessionId = generateId('ds');
|
||||||
|
|
||||||
|
it('should ingest traces', async () => {
|
||||||
|
const traces: DebugTraceDoc[] = [
|
||||||
|
{
|
||||||
|
id: generateId('tr'),
|
||||||
|
pk: `${productId}:${sessionId}`,
|
||||||
|
sessionId,
|
||||||
|
productId,
|
||||||
|
traceId: randomUUID(),
|
||||||
|
spanId: randomUUID(),
|
||||||
|
name: 'test_operation',
|
||||||
|
startTime: new Date().toISOString(),
|
||||||
|
durationMs: 100,
|
||||||
|
attributes: {},
|
||||||
|
status: 'ok',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await repo.ingestTraces(productId, sessionId, traces);
|
||||||
|
|
||||||
|
const { traces: found } = await repo.getTraces(productId, sessionId, { limit: 10 });
|
||||||
|
expect(found.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query traces for a session', async () => {
|
||||||
|
const traceId = randomUUID();
|
||||||
|
const spanId = randomUUID();
|
||||||
|
|
||||||
|
const traces: DebugTraceDoc[] = [
|
||||||
|
{
|
||||||
|
id: generateId('tr'),
|
||||||
|
pk: `${productId}:${sessionId}`,
|
||||||
|
sessionId,
|
||||||
|
productId,
|
||||||
|
traceId,
|
||||||
|
spanId,
|
||||||
|
name: 'test_query',
|
||||||
|
startTime: new Date().toISOString(),
|
||||||
|
durationMs: 50,
|
||||||
|
attributes: { key: 'value' },
|
||||||
|
status: 'ok',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await repo.ingestTraces(productId, sessionId, traces);
|
||||||
|
|
||||||
|
const { traces: found } = await repo.getTraces(productId, sessionId, { limit: 10 });
|
||||||
|
expect(found.some((t) => t.name === 'test_query')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Log Ingest Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Log Ingest', () => {
|
||||||
|
const productId = 'test_product';
|
||||||
|
const sessionId = generateId('ds');
|
||||||
|
|
||||||
|
it('should ingest logs', async () => {
|
||||||
|
const logs: DebugLogEntryDoc[] = [
|
||||||
|
{
|
||||||
|
id: generateId('log'),
|
||||||
|
pk: `${productId}:${sessionId}`,
|
||||||
|
sessionId,
|
||||||
|
productId,
|
||||||
|
level: 'info',
|
||||||
|
message: 'Test log message',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
module: 'TestModule',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await repo.ingestLogs(productId, sessionId, logs);
|
||||||
|
|
||||||
|
const { logs: found } = await repo.getLogs(productId, sessionId, { limit: 10 });
|
||||||
|
expect(found.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter logs by level', async () => {
|
||||||
|
const logs: DebugLogEntryDoc[] = [
|
||||||
|
{
|
||||||
|
id: generateId('log'),
|
||||||
|
pk: `${productId}:${sessionId}`,
|
||||||
|
sessionId,
|
||||||
|
productId,
|
||||||
|
level: 'error',
|
||||||
|
message: 'Error message',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
module: 'TestModule',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: generateId('log'),
|
||||||
|
pk: `${productId}:${sessionId}`,
|
||||||
|
sessionId,
|
||||||
|
productId,
|
||||||
|
level: 'info',
|
||||||
|
message: 'Info message',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
module: 'TestModule',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await repo.ingestLogs(productId, sessionId, logs);
|
||||||
|
|
||||||
|
const { logs: found } = await repo.getLogs(productId, sessionId, {
|
||||||
|
level: 'error',
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(found.every((l) => l.level === 'error')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search logs by message content', async () => {
|
||||||
|
const logs: DebugLogEntryDoc[] = [
|
||||||
|
{
|
||||||
|
id: generateId('log'),
|
||||||
|
pk: `${productId}:${sessionId}`,
|
||||||
|
sessionId,
|
||||||
|
productId,
|
||||||
|
level: 'info',
|
||||||
|
message: 'Searchable unique message content',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
module: 'TestModule',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await repo.ingestLogs(productId, sessionId, logs);
|
||||||
|
|
||||||
|
const { logs: found } = await repo.getLogs(productId, sessionId, {
|
||||||
|
search: 'unique message',
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(found.some((l) => l.message.includes('unique message'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Schema Validation Tests ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Schema Validation', () => {
|
||||||
|
it('should validate CreateDebugSessionSchema', () => {
|
||||||
|
const input: CreateDebugSessionInput = {
|
||||||
|
productId: 'test_product',
|
||||||
|
targetUserId: 'user_123',
|
||||||
|
collectionLevel: 'debug',
|
||||||
|
captureLogs: true,
|
||||||
|
captureNetwork: true,
|
||||||
|
captureScreenshots: false,
|
||||||
|
screenshotOnError: true,
|
||||||
|
maxDurationMinutes: 60,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = CreateDebugSessionSchema.parse(input);
|
||||||
|
expect(result.productId).toBe('test_product');
|
||||||
|
expect(result.maxDurationMinutes).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate UpdateDebugSessionSchema', () => {
|
||||||
|
const input: UpdateDebugSessionInput = {
|
||||||
|
status: 'active',
|
||||||
|
collectionLevel: 'trace',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = UpdateDebugSessionSchema.parse(input);
|
||||||
|
expect(result.status).toBe('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate IngestTracesSchema', () => {
|
||||||
|
const input: IngestTracesInput = {
|
||||||
|
sessionId: 'ds_123',
|
||||||
|
traces: [
|
||||||
|
{
|
||||||
|
traceId: randomUUID(),
|
||||||
|
spanId: randomUUID(),
|
||||||
|
name: 'test',
|
||||||
|
startTime: new Date().toISOString(),
|
||||||
|
status: 'ok',
|
||||||
|
attributes: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = IngestTracesSchema.parse(input);
|
||||||
|
expect(result.traces).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate IngestLogsSchema', () => {
|
||||||
|
const input: IngestLogsInput = {
|
||||||
|
sessionId: 'ds_123',
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
level: 'info',
|
||||||
|
message: 'Test message',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
module: 'Test',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = IngestLogsSchema.parse(input);
|
||||||
|
expect(result.logs).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid log level', () => {
|
||||||
|
expect(() =>
|
||||||
|
IngestLogsSchema.parse({
|
||||||
|
sessionId: 'ds_123',
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
level: 'invalid_level',
|
||||||
|
message: 'Test',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
module: 'Test',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user