Files
DashDescomplicar/api/tests/sessions-patterns.test.ts
ealmeida 2a523a505e feat(observabilidade): tabela patterns + 6 detectores SQL
Adiciona tabela 'patterns' à BD sessions (UNIQUE por week_iso+pattern_key)
e helpers upsertPattern/getPatternsByWeek/getConsecutiveWeeks no SessionsDb.

Módulo patterns.ts implementa 6 detectores heurísticos para deteccão semanal:
  1. skills_with_high_error_rate (ratio > 0.2, severity warning|action)
  2. tools_low_efficiency (tool_calls/event_count médio > 0.5)
  3. skill_tool_pairs (top 5 co-ocorrências)
  4. duration_outliers (sessões > p95 com outcome != completed)
  5. abandoned_sessions (event_count<3 AND outcome=unknown, >=5)
  6. growing_complexity (avg tool_calls actual > anterior*1.3)

5 testes cobrem detector de erro, abandonadas, consecutive_weeks,
idempotência do upsert e toPatternRecord.

Refs Fase 6A · Desk #2059 · Project #65

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 02:17:21 +01:00

124 lines
4.2 KiB
TypeScript

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>): 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')
})
})