import { describe, it, expect, beforeEach } from 'vitest' import { mkdtempSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import { openSessionsDb, type SessionsDb, type PatternRecord } from '../services/sessions/db.js' import { detectPatterns, weekRange, toPatternRecord } from '../services/sessions/patterns.js' import type { SessionMeta } from '../types/session.js' function meta(overrides: Partial): SessionMeta { return { session_id: 's-' + Math.random().toString(36).slice(2, 10), project_path: '/tmp/project', project_slug: 'project', jsonl_path: '/tmp/' + Math.random().toString(36).slice(2) + '.jsonl', started_at: '2026-04-20T10:00:00Z', // segunda de 2026-W17 ended_at: '2026-04-20T10:30:00Z', duration_sec: 1800, event_count: 50, user_messages: 5, assistant_msgs: 10, tool_calls: 20, first_prompt: 'olá', tools_used: ['Bash'], skills_invoked: [], outcome: 'completed', permission_mode: 'default', file_size: 10000, indexed_at: '2026-04-20T10:31:00Z', ...overrides, } } describe('patterns detector', () => { let db: SessionsDb beforeEach(() => { const dir = mkdtempSync(join(tmpdir(), 'obs-pat-')) db = openSessionsDb(join(dir, 'sessions.db')) }) it('detecta skill com taxa elevada de erro (action)', () => { // 3 sessões skill X: 2 error, 1 completed → ratio 0.67 → severity=action db.upsertSession(meta({ session_id: 'a', skills_invoked: ['skillX'], outcome: 'error' })) db.upsertSession(meta({ session_id: 'b', skills_invoked: ['skillX'], outcome: 'interrupted' })) db.upsertSession(meta({ session_id: 'c', skills_invoked: ['skillX'], outcome: 'completed' })) const { start, end } = weekRange(new Date('2026-04-22T00:00:00Z')) const patterns = detectPatterns(db, start, end) const errorRate = patterns.find((p) => p.pattern_key === 'skill_error_rate:skillX') expect(errorRate).toBeDefined() expect(errorRate!.severity).toBe('action') expect(errorRate!.affected_count).toBe(2) }) it('detecta sessões abandonadas', () => { for (let i = 0; i < 6; i++) { db.upsertSession(meta({ session_id: `ab-${i}`, event_count: 1, outcome: 'unknown' })) } const { start, end } = weekRange(new Date('2026-04-22T00:00:00Z')) const patterns = detectPatterns(db, start, end) expect(patterns.some((p) => p.pattern_key === 'abandoned_sessions')).toBe(true) }) it('getConsecutiveWeeks devolve 3 após upserts em semanas sucessivas', () => { const key = 'skill_error_rate:Y' const weeks = ['2026-W15', '2026-W16', '2026-W17'] for (const w of weeks) { db.upsertPattern({ detected_at: new Date().toISOString(), week_iso: w, pattern_key: key, title: 't', description: 'd', severity: 'warning', metric_value: 0.5, sample_session_ids: ['x'], affected_count: 1, consecutive_weeks: 1, }) } expect(db.getConsecutiveWeeks(key, '2026-W17')).toBe(3) expect(db.getConsecutiveWeeks(key, '2026-W16')).toBe(2) }) it('upsertPattern é idempotente por (week_iso, pattern_key)', () => { const base: PatternRecord = { detected_at: '2026-04-20T00:00:00Z', week_iso: '2026-W17', pattern_key: 'test', title: 'v1', description: 'd', severity: 'info', metric_value: 1, sample_session_ids: ['a'], affected_count: 1, consecutive_weeks: 1, } db.upsertPattern(base) db.upsertPattern({ ...base, title: 'v2', affected_count: 5, consecutive_weeks: 2 }) const rows = db.getPatternsByWeek('2026-W17') expect(rows).toHaveLength(1) expect(rows[0].title).toBe('v2') expect(rows[0].affected_count).toBe(5) expect(rows[0].consecutive_weeks).toBe(2) }) it('toPatternRecord propaga week_iso e consecutive_weeks', () => { const rec = toPatternRecord( { pattern_key: 'k', title: 't', description: 'd', severity: 'warning', metric_value: 0.42, sample_session_ids: ['a', 'b'], affected_count: 2, }, '2026-W17', 3, ) expect(rec.week_iso).toBe('2026-W17') expect(rec.consecutive_weeks).toBe(3) expect(rec.severity).toBe('warning') }) })