2a523a505e
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>
124 lines
4.2 KiB
TypeScript
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')
|
|
})
|
|
})
|