import { describe, it, expect, beforeEach } from 'vitest'
import { mkdtempSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { openSessionsDb, type SessionsDb, type WorklogCommentRecord } from '../services/sessions/db.js'
import { parseWorklogHtml } from '../services/sessions/worklog-import.js'
import { detectActionsNeverExecuted, weekRange } from '../services/sessions/patterns.js'
const SAMPLE_H4 = `
2026-04-15 10:30 - Refactor API sessions
Projecto: DashDescomplicar
Tarefa: #2059 - Observabilidade Espelho
Duração: ~2h 15m
Trabalho Realizado
- Criar módulo worklog-import
- Integrar detectores cruzados
Ficheiros Modificados
api/services/sessions/db.tsapi/scripts/sessions-worklog-import.ts
Problemas / Soluções
- Parser HTML frágil → usar node-html-parser
Padrões Detectados
- MCP gateway responde em SSE ou JSON
Acções Sugeridas
- [Refactor] Extrair callMcpTool para módulo partilhado P2
`
const SAMPLE_H2 = `
2026-01-31 - Estratégia Stack
Duração: ~2h
Trabalho Realizado
- Stack Mapeado - 15 sistemas
Insights
- Posicionamento: Marketing alta performance
`
const SAMPLE_D33 = `
- [ ] [MCP] Corrigir bug desk-crm-v3 com tabelas de discussões
Origem: Sessão 2026-02-02
Prioridade: P1
`
describe('parseWorklogHtml', () => {
it('extrai campos de comentário formato ', () => {
const parsed = parseWorklogHtml(SAMPLE_H4, { id: 100, discussion_id: 31, created_at: '' })
expect(parsed.id).toBe(100)
expect(parsed.title).toMatch(/2026-04-15/)
expect(parsed.task_ref).toBe('#2059')
expect(parsed.duration_sec).toBe(2 * 3600 + 15 * 60)
expect(parsed.work_items.length).toBe(2)
expect(parsed.files_modified.length).toBe(2)
expect(parsed.patterns_text.length).toBe(1)
expect(parsed.actions.length).toBe(1)
expect(parsed.actions[0].tipo).toBe('Refactor')
expect(parsed.actions[0].prioridade).toBe('P2')
expect(parsed.created_at.startsWith('2026-04-15')).toBe(true)
})
it('extrai campos de comentário formato / (legacy)', () => {
const parsed = parseWorklogHtml(SAMPLE_H2, { id: 64, discussion_id: 31, created_at: '' })
expect(parsed.title).toMatch(/2026-01-31/)
expect(parsed.work_items.length).toBeGreaterThanOrEqual(1)
expect(parsed.duration_sec).toBe(2 * 3600)
expect(parsed.created_at.startsWith('2026-01-31')).toBe(true)
})
it('extrai acções em formato discussão #33 (lista crua)', () => {
const parsed = parseWorklogHtml(SAMPLE_D33, { id: 200, discussion_id: 33, created_at: '2026-02-02T00:00:00Z' })
expect(parsed.actions.length).toBe(1)
expect(parsed.actions[0].tipo).toBe('MCP')
})
})
describe('upsertWorklogComment idempotência', () => {
let db: SessionsDb
beforeEach(() => {
const dir = mkdtempSync(join(tmpdir(), 'obs-wl-'))
db = openSessionsDb(join(dir, 'sessions.db'))
})
it('insert primeiro, update depois', () => {
const base: WorklogCommentRecord = {
id: 42,
discussion_id: 31,
created_at: '2026-04-15T10:30:00Z',
staff_id: 25,
title: 'Test',
task_ref: '#100',
duration_sec: 600,
work_items: ['a'],
files_modified: [],
problems: [],
patterns_text: [],
actions: [],
raw_html: 'Test
',
imported_at: '2026-04-23T00:00:00Z',
}
const r1 = db.upsertWorklogComment(base)
expect(r1.inserted).toBe(true)
expect(db.countWorklogComments()).toBe(1)
const r2 = db.upsertWorklogComment({ ...base, title: 'Updated' })
expect(r2.inserted).toBe(false)
expect(db.countWorklogComments()).toBe(1)
const list = db.listWorklogComments({ discussion_id: 31 })
expect(list[0].title).toBe('Updated')
})
})
describe('detectActionsNeverExecuted', () => {
let db: SessionsDb
beforeEach(() => {
const dir = mkdtempSync(join(tmpdir(), 'obs-act-'))
db = openSessionsDb(join(dir, 'sessions.db'))
})
it('sinaliza acções P1/P2 antigas sem execução', () => {
const old = new Date('2026-03-01T00:00:00Z').toISOString()
for (let i = 0; i < 4; i++) {
db.upsertWorklogComment({
id: 300 + i,
discussion_id: 33,
created_at: old,
staff_id: 25,
title: `Acção ${i}`,
task_ref: `#${1000 + i}`,
duration_sec: null,
work_items: [],
files_modified: [],
problems: [],
patterns_text: [],
actions: [{ tipo: 'MCP', descricao: `Corrigir bug X${i}`, prioridade: i % 2 ? 'P1' : 'P2' }],
raw_html: '',
imported_at: '2026-04-23T00:00:00Z',
})
}
const range = weekRange(new Date('2026-04-22T00:00:00Z'))
const patterns = detectActionsNeverExecuted({
db: db.rawDb(),
weekStartIso: range.start.toISOString(),
weekEndIso: range.end.toISOString(),
})
expect(patterns.length).toBe(1)
expect(patterns[0].pattern_key).toBe('actions_never_executed')
expect(patterns[0].affected_count).toBeGreaterThanOrEqual(3)
})
it('não sinaliza se acções recentes (<14 dias)', () => {
const recent = new Date().toISOString()
for (let i = 0; i < 5; i++) {
db.upsertWorklogComment({
id: 400 + i,
discussion_id: 33,
created_at: recent,
staff_id: 25,
title: null,
task_ref: null,
duration_sec: null,
work_items: [],
files_modified: [],
problems: [],
patterns_text: [],
actions: [{ tipo: 'MCP', descricao: 'x', prioridade: 'P1' }],
raw_html: '',
imported_at: recent,
})
}
const range = weekRange(new Date())
const patterns = detectActionsNeverExecuted({
db: db.rawDb(),
weekStartIso: range.start.toISOString(),
weekEndIso: range.end.toISOString(),
})
expect(patterns.length).toBe(0)
})
})