mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
buildIssueRow,
|
|
buildPanelViewModel,
|
|
classifyPlanStatus,
|
|
filterByState,
|
|
filterCounts,
|
|
issueAction,
|
|
issueCode,
|
|
issueDepends,
|
|
issueState,
|
|
issueSummary,
|
|
parsePlanStatus,
|
|
planLabel,
|
|
siblingPlanPaths,
|
|
sortRows,
|
|
toJsonPayload,
|
|
} from '../../scripts/lib/issues-panel.mjs'
|
|
|
|
const issue = (over: Record<string, unknown> = {}) => ({
|
|
number: 1020,
|
|
title: 'OPS109 — Painel de issues e planos no terminal (via SSH)',
|
|
body: [
|
|
'---',
|
|
'id: OPS109',
|
|
'depends: [OPS110]',
|
|
'priority: P2',
|
|
'---',
|
|
'Plano: [`docs/plans/ops109-painel-issues-no-terminal.md`](docs/plans/ops109-painel-issues-no-terminal.md)',
|
|
'',
|
|
'Ferramenta local para visualizar o andamento.',
|
|
].join('\n'),
|
|
state: 'OPEN',
|
|
createdAt: '2026-09-15T10:00:00.000Z',
|
|
labels: [
|
|
{ name: 'in-progress', color: '' },
|
|
{ name: 'prio:P2', color: '' },
|
|
{ name: 'kind:feature', color: '' },
|
|
],
|
|
...over,
|
|
})
|
|
|
|
describe('parsePlanStatus (OPS109)', () => {
|
|
it('reads the raw and normalized Status line', () => {
|
|
expect(parsePlanStatus('# T\n\nStatus: aprovado\n')).toEqual({
|
|
raw: 'aprovado',
|
|
normalized: 'aprovado',
|
|
})
|
|
})
|
|
|
|
it('strips bold, backticks and parenthetical suffixes', () => {
|
|
expect(parsePlanStatus('Status: **entregue** (2026-07-29)')?.raw).toBe('entregue (2026-07-29)')
|
|
expect(parsePlanStatus('Status: `aprovado (gate humano 2026-09-10)`')?.raw).toBe(
|
|
'aprovado (gate humano 2026-09-10)',
|
|
)
|
|
})
|
|
|
|
it('returns null when there is no Status line', () => {
|
|
expect(parsePlanStatus('# só título')).toBeNull()
|
|
expect(parsePlanStatus('')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('classifyPlanStatus (OPS109 — leitura de produto)', () => {
|
|
it.each([
|
|
['aprovado', 'aprovado'],
|
|
['aprovado (gate humano 2026-09-10)', 'aprovado'],
|
|
['rascunho', 'aguardando'],
|
|
['registrado', 'aguardando'],
|
|
['blocked', 'aguardando'],
|
|
['em execução', 'andou'],
|
|
['executado', 'andou'],
|
|
['entregue', 'andou'],
|
|
['entregue (2026-07-29)', 'andou'],
|
|
['plano — registrado (blocked até plano em main)', 'aguardando'],
|
|
['qualquer coisa nova', 'desconhecido'],
|
|
])('%s → %s', (raw, expected) => {
|
|
expect(classifyPlanStatus({ raw, normalized: raw.toLowerCase() })).toBe(expected)
|
|
})
|
|
|
|
it('fails safe to desconhecido when there is no status', () => {
|
|
expect(classifyPlanStatus(null)).toBe('desconhecido')
|
|
expect(classifyPlanStatus({ raw: '', normalized: '' })).toBe('desconhecido')
|
|
})
|
|
})
|
|
|
|
describe('siblingPlanPaths (OPS109)', () => {
|
|
it('derives impl and ui-draft from the intent path', () => {
|
|
expect(siblingPlanPaths('docs/plans/ops109-x.md')).toEqual({
|
|
impl: 'docs/plans/ops109-x-impl.md',
|
|
uiDraft: 'docs/plans/ops109-x-ui-draft.html',
|
|
})
|
|
})
|
|
|
|
it('does not double-suffix when given the impl path', () => {
|
|
expect(siblingPlanPaths('docs/plans/ops109-x-impl.md').impl).toBe('docs/plans/ops109-x-impl.md')
|
|
})
|
|
|
|
it('returns nulls for a non-markdown path', () => {
|
|
expect(siblingPlanPaths(null)).toEqual({ impl: null, uiDraft: null })
|
|
expect(siblingPlanPaths('docs/plans/x.html')).toEqual({ impl: null, uiDraft: null })
|
|
})
|
|
})
|
|
|
|
describe('issue field derivation (OPS109)', () => {
|
|
it('reads the frontmatter id and depends', () => {
|
|
expect(issueCode(issue())).toBe('OPS109')
|
|
expect(issueDepends(issue())).toEqual(['OPS110'])
|
|
})
|
|
|
|
it('derives state/priority/kind from labels', () => {
|
|
expect(issueState(issue())).toBe('in-progress')
|
|
expect(issueState(issue({ state: 'CLOSED' }))).toBe('done')
|
|
expect(issueState(issue({ labels: [{ name: 'ready', color: '' }] }))).toBe('ready')
|
|
})
|
|
|
|
it('summarizes the body after the frontmatter and plan link', () => {
|
|
expect(issueSummary(issue())).toBe('Ferramenta local para visualizar o andamento.')
|
|
})
|
|
|
|
it('returns null id and empty depends for an id-less body', () => {
|
|
const bare = issue({ body: 'sem frontmatter' })
|
|
expect(issueCode(bare)).toBeNull()
|
|
expect(issueDepends(bare)).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('buildIssueRow (OPS109)', () => {
|
|
const deps = {
|
|
doneIds: new Set<string>(),
|
|
knownIds: new Set(['OPS110']),
|
|
}
|
|
|
|
it('marks a plan present with the classification', () => {
|
|
const row = buildIssueRow(issue(), {
|
|
...deps,
|
|
intention: 'Status: aprovado\n',
|
|
impl: 'Status: em execução\n',
|
|
})
|
|
expect(row.plan.intention).toMatchObject({
|
|
path: 'docs/plans/ops109-painel-issues-no-terminal.md',
|
|
exists: true,
|
|
classification: 'aprovado',
|
|
})
|
|
expect(row.plan.impl).toMatchObject({
|
|
path: 'docs/plans/ops109-painel-issues-no-terminal-impl.md',
|
|
exists: true,
|
|
classification: 'andou',
|
|
})
|
|
expect(row.uiDraft).toEqual({
|
|
path: 'docs/plans/ops109-painel-issues-no-terminal-ui-draft.html',
|
|
exists: false,
|
|
})
|
|
})
|
|
|
|
it('explains missing intention and impl as first-class states', () => {
|
|
const row = buildIssueRow(issue(), { ...deps, intention: null, impl: null })
|
|
expect(row.plan.intention).toMatchObject({ exists: false, classification: 'não criado' })
|
|
expect(row.plan.impl).toMatchObject({ exists: false, classification: 'não criado' })
|
|
expect(planLabel({ plan: { intention: null, impl: null } })).toBe('sem plano')
|
|
})
|
|
|
|
it('reports sem plano when the body has no plan link', () => {
|
|
const row = buildIssueRow(issue({ body: '---\nid: OPS999\n---\nsem plano' }), deps)
|
|
expect(row.plan.intention.path).toBeNull()
|
|
expect(row.plan.intention.exists).toBe(false)
|
|
})
|
|
|
|
it('ties the session by issue number and falls back to none', () => {
|
|
const sessions = [
|
|
{ sessionID: 'ses_abc', issue: 1020, branch: 'OPS109-x', dir: '/tmp/x', status: 'working' },
|
|
{ sessionID: 'ses_def', issue: 9999, branch: 'OPS999-y', dir: '/tmp/y', status: 'idle' },
|
|
]
|
|
const withSession = buildIssueRow(issue(), { ...deps, sessions })
|
|
expect(withSession.session).toMatchObject({ status: 'working', sessionID: 'ses_abc' })
|
|
|
|
const withoutSession = buildIssueRow(issue({ number: 4242 }), { ...deps, sessions })
|
|
expect(withoutSession.session).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('issueAction (OPS109)', () => {
|
|
const base = {
|
|
state: 'ready',
|
|
plan: { intention: null, impl: null },
|
|
blockedBy: [],
|
|
}
|
|
|
|
it('prefers the blocked dependency reason', () => {
|
|
expect(issueAction({ ...base, blockedBy: ['OPS111'] })).toBe('travada (depends OPS111)')
|
|
})
|
|
|
|
it('reads in-progress, waiting, done and ready', () => {
|
|
expect(issueAction({ ...base, state: 'in-progress' })).toBe('em andamento')
|
|
expect(
|
|
issueAction({
|
|
...base,
|
|
plan: { intention: { classification: 'aguardando' }, impl: null },
|
|
}),
|
|
).toBe('aguardando aprovação')
|
|
expect(issueAction({ ...base, state: 'done' })).toBe('concluída')
|
|
expect(issueAction({ ...base, state: 'ready' })).toBe('pronta')
|
|
})
|
|
|
|
it('does not treat em execução as waiting', () => {
|
|
expect(
|
|
issueAction({
|
|
...base,
|
|
state: 'blocked',
|
|
plan: { intention: { classification: 'andou' }, impl: { classification: 'andou' } },
|
|
}),
|
|
).toBe('travada')
|
|
})
|
|
})
|
|
|
|
describe('filterByState / sortRows / filterCounts (OPS109)', () => {
|
|
const rows: { number: number; code: string; state: string; priority: string; action: string }[] =
|
|
[
|
|
{ number: 1, code: 'A', state: 'in-progress', priority: 'P1', action: 'em andamento' },
|
|
{ number: 2, code: 'B', state: 'ready', priority: 'P0', action: 'aguardando aprovação' },
|
|
{ number: 3, code: 'C', state: 'done', priority: 'P2', action: 'concluída' },
|
|
{ number: 4, code: 'D', state: 'blocked', priority: 'P2', action: 'travada' },
|
|
]
|
|
|
|
it('filters by the four keys and fails open on unknown', () => {
|
|
expect(filterByState(rows, 'all').length).toBe(4)
|
|
expect(filterByState(rows, 'in-progress').map((row) => row.number)).toEqual([1])
|
|
expect(filterByState(rows, 'waiting').map((row) => row.number)).toEqual([2])
|
|
expect(filterByState(rows, 'done').map((row) => row.number)).toEqual([3])
|
|
expect(filterByState(rows, 'nonsense').length).toBe(4)
|
|
})
|
|
|
|
it('counts each band', () => {
|
|
expect(filterCounts(rows)).toEqual({ all: 4, 'in-progress': 1, waiting: 1, done: 1 })
|
|
})
|
|
|
|
it('orders open work before blocked before done', () => {
|
|
expect(sortRows(rows).map((row) => row.number)).toEqual([1, 2, 4, 3])
|
|
})
|
|
})
|
|
|
|
describe('buildPanelViewModel / toJsonPayload (OPS109)', () => {
|
|
const issues = [
|
|
issue(),
|
|
issue({
|
|
number: 1019,
|
|
state: 'CLOSED',
|
|
labels: [
|
|
{ name: 'done', color: '' },
|
|
{ name: 'in-prod', color: '' },
|
|
{ name: 'prio:P2', color: '' },
|
|
],
|
|
body: '---\nid: OPS110\n---\nPlano: [`docs/plans/ops110.md`](docs/plans/ops110.md)',
|
|
}),
|
|
]
|
|
const plansByNumber = new Map([
|
|
[
|
|
1020,
|
|
{
|
|
intention: 'Status: aprovado\n',
|
|
impl: 'Status: em execução\n',
|
|
uiDraft: true,
|
|
},
|
|
],
|
|
])
|
|
|
|
it('assembles rows, counts and the truncation flag', () => {
|
|
const model = buildPanelViewModel({ issues, sessions: [], plansByNumber, limit: 200 })
|
|
expect(model.rows.map((row) => row.code)).toEqual(['OPS109', 'OPS110'])
|
|
expect(model.truncated).toBe(false)
|
|
expect(model.counts.all).toBe(2)
|
|
expect(model.counts['in-progress']).toBe(1)
|
|
})
|
|
|
|
it('flags truncation when the page fills the limit', () => {
|
|
const model = buildPanelViewModel({ issues, limit: 2 })
|
|
expect(model.truncated).toBe(true)
|
|
})
|
|
|
|
it('produces a JSON-safe payload with both plans', () => {
|
|
const model = buildPanelViewModel({ issues, plansByNumber, limit: 200 })
|
|
const payload = toJsonPayload(model)
|
|
expect(payload.version).toBe(1)
|
|
const row = payload.rows.find((entry: { code?: string }) => entry.code === 'OPS109')
|
|
expect(row.plan.intention).toMatchObject({ classification: 'aprovado', status: 'aprovado' })
|
|
expect(row.plan.impl).toMatchObject({ classification: 'andou', status: 'em execução' })
|
|
expect(row.uiDraft.exists).toBe(true)
|
|
expect(row.session).toBeNull()
|
|
expect(JSON.parse(JSON.stringify(payload))).toEqual(payload)
|
|
})
|
|
})
|