Files
DashDescomplicar/api/tests/sessions-parser.test.ts
ealmeida ac4e9c6f35 fix(observabilidade): parser extrai skills de tool_result.content (string e array)
Antes: skills_invoked vazio em 1608/1608 sessões porque detectSkillInvoked
apenas era aplicado ao text extraído de content[type=text]. A string
'Launching skill: X' vive dentro de tool_result.content (string ou array
de text blocks), que era ignorada.

Fix: adicionar helper extractResultText(r) que trata ambos os casos e
aplicar detectSkillInvoked + detectHook também ao tool_result. Após
re-indexação full, 526/1616 sessões têm agora skills detectadas e o
detector de padrões devolve 6 padrões (vs 2 baseline), incluindo
skills_with_high_error_rate reais.

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

139 lines
4.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { parseSessionFile } from '../services/sessions/parser.js'
import { mkdtempSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
function writeJsonl(lines: object[]): string {
const dir = mkdtempSync(join(tmpdir(), 'obs-test-'))
const path = join(dir, 'session-test.jsonl')
writeFileSync(path, lines.map((l) => JSON.stringify(l)).join('\n'))
return path
}
describe('parseSessionFile', () => {
it('extrai metadata básica de sessão mínima', async () => {
const path = writeJsonl([
{ type: 'permission-mode', permissionMode: 'default', sessionId: 's1' },
{
type: 'user',
timestamp: '2026-04-23T10:00:00Z',
message: { role: 'user', content: [{ type: 'text', text: 'olá mundo' }] },
},
{
type: 'assistant',
timestamp: '2026-04-23T10:00:30Z',
message: { role: 'assistant', content: [{ type: 'text', text: 'olá' }] },
},
])
const result = await parseSessionFile(path)
expect(result.meta.session_id).toBe('s1')
expect(result.meta.user_messages).toBe(1)
expect(result.meta.assistant_msgs).toBe(1)
expect(result.meta.tool_calls).toBe(0)
expect(result.meta.first_prompt).toBe('olá mundo')
expect(result.meta.permission_mode).toBe('default')
expect(result.meta.outcome).toBe('completed')
})
it('conta tool_calls e recolhe tools_used', async () => {
const path = writeJsonl([
{
type: 'assistant',
timestamp: '2026-04-23T10:00:00Z',
message: {
role: 'assistant',
content: [
{ type: 'tool_use', name: 'Bash', input: { command: 'ls' } },
{ type: 'tool_use', name: 'Read', input: { file_path: '/tmp/x' } },
],
},
},
])
const result = await parseSessionFile(path)
expect(result.meta.tool_calls).toBe(2)
expect(result.meta.tools_used).toEqual(expect.arrayContaining(['Bash', 'Read']))
})
it('detecta skill invocation em system-reminder', async () => {
const path = writeJsonl([
{
type: 'system',
timestamp: '2026-04-23T10:00:00Z',
message: { role: 'system', content: [{ type: 'text', text: 'Launching skill: superpowers:brainstorming' }] },
},
])
const result = await parseSessionFile(path)
expect(result.meta.skills_invoked).toContain('superpowers:brainstorming')
})
it('detecta skill invocation em tool_result.content (string)', async () => {
const path = writeJsonl([
{
type: 'user',
timestamp: '2026-04-23T10:00:00Z',
message: {
role: 'user',
content: [
{ type: 'tool_result', tool_use_id: 'abc', content: 'Launching skill: infraestrutura:easypanel-monitor\nOther log output' },
],
},
},
])
const result = await parseSessionFile(path)
expect(result.meta.skills_invoked).toContain('infraestrutura:easypanel-monitor')
})
it('detecta skill invocation em tool_result.content (array de text blocks)', async () => {
const path = writeJsonl([
{
type: 'user',
timestamp: '2026-04-23T10:00:00Z',
message: {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'abc',
content: [{ type: 'text', text: 'Launching skill: superpowers:brainstorming' }],
},
],
},
},
])
const result = await parseSessionFile(path)
expect(result.meta.skills_invoked).toContain('superpowers:brainstorming')
})
it('ignora linhas JSON inválidas silenciosamente', async () => {
const path = writeJsonl([
{ type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'válido' }] } },
])
const { writeFileSync } = await import('fs')
writeFileSync(path, 'linha inválida\n' + JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'válido' }] } }))
const result = await parseSessionFile(path)
expect(result.meta.user_messages).toBe(1)
})
it('devolve duration_sec null quando timestamps são inválidos', async () => {
const path = writeJsonl([
{ type: 'user', timestamp: 'not-a-date', message: { role: 'user', content: [{ type: 'text', text: 'a' }] } },
{ type: 'assistant', timestamp: 'also-not-a-date', message: { role: 'assistant', content: [{ type: 'text', text: 'b' }] } },
])
const result = await parseSessionFile(path)
expect(result.meta.duration_sec).toBeNull()
})
it('classifica max_tokens como interrupted', async () => {
const path = writeJsonl([
{
type: 'assistant',
timestamp: '2026-04-23T10:00:00Z',
message: { role: 'assistant', content: [{ type: 'text', text: 'resposta cortada' }], stop_reason: 'max_tokens' },
},
])
const result = await parseSessionFile(path)
expect(result.meta.outcome).toBe('interrupted')
})
})