f733998945
- Novos indexadores hermes-indexer.ts e opencode-indexer.ts para unificar sessões Claude, Hermes Agent e OpenCode num único sessions.db - SessionMeta alargado: source (obrigatório), model, input/output_tokens, estimated_cost; project_path/jsonl_path agora nullable - Fix TS: tipos explícitos, guard jsonl_path null, dependências instaladas Security Audit (Regra #47): - npm audit executado — 0 vulnerabilities após fix - vite 7→8.0.16 (breaking upgrade, resolve esbuild CVE GHSA-gv7w-rqvm-qjhr) - vitest 4.0.18→4.1.9 (resolve esbuild interno CVE GHSA-gv7w-rqvm-qjhr) - shell-quote override ^1.8.4 via package.json#overrides (CVE GHSA-w7jw-789q-3m8p) - react-router, joi, qs, form-data, ip-address, js-yaml resolvidos via npm audit fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
129 lines
4.3 KiB
TypeScript
129 lines
4.3 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),
|
|
source: 'claude',
|
|
model: null,
|
|
input_tokens: null,
|
|
output_tokens: null,
|
|
estimated_cost: null,
|
|
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')
|
|
})
|
|
})
|