teqo-backup/tests/unit/issuesPanelRegression.unit.spec.ts
Francisco Solla ef2df24a80 feat(OPS116): painel aceita -ui-design.html com retrocompat -ui-draft.html (Closes #1059)
O painel `pnpm issues:tui` volta a enxergar o design UI depois do rename do
OPS114: `siblingPlanPaths` deriva `-ui-design.html` (vigente) e
`-ui-draft.html` (legado) e o novo `resolveUiDraft(planPath, exists)` — puro,
com a existência injetada pelo CLI — é dono da precedência (design vence,
draft é fallback, nenhum → design com `exists:false`). O `readPlans` sonda
design→draft no disco e o caminho REAL flui por `plansByNumber`/`buildIssueRow`
até o view model; o contrato `--json` segue `version: 1` com o shape
`{ path, exists }` e a chave histórica `uiDraft`. As superfícies do painel
trocam "rascunho UI" por "design UI".

Retrocompat é só de leitura — o acervo legado (30 `*-ui-draft.html`) não é
tocado. Unit dos dois specs cobre design-only, draft-only, ambos, nenhum, o
caminho real, body linkando o impl e o shape do JSON. Sem migration.
2026-09-16 01:14:21 -03:00

169 lines
5.1 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import {
buildPanelViewModel,
issueAction,
issueState,
planLabel,
toJsonPayload,
visibleWindow,
} from '../../scripts/lib/issues-panel.mjs'
const issue = (over: Record<string, unknown> = {}) => ({
number: 999,
title: 'Test',
body: '---\nid: TEST999\n---\nPlano: [`docs/plans/test999.md`](docs/plans/test999.md)\n',
state: 'OPEN',
createdAt: '2026-09-15T10:00:00.000Z',
labels: [{ name: 'ready', color: '' }],
...over,
})
describe('planLabel (OPS109)', () => {
it('says sem plano when both plans are null', () => {
expect(planLabel({ plan: { intention: null, impl: null } })).toBe('sem plano')
})
it('says sem plano when intention path is null and impl path is null', () => {
expect(
planLabel({
plan: {
intention: { path: null, exists: false, classification: 'none' },
impl: { path: null, exists: false, classification: 'none' },
},
}),
).toBe('sem plano')
})
it('shows the intention status when the intention exists but impl is missing', () => {
expect(
planLabel({
plan: {
intention: {
path: 'docs/plans/test.md',
exists: true,
classification: 'aprovado',
status: { raw: 'aprovado', classification: 'aprovado' },
},
impl: {
path: 'docs/plans/test-impl.md',
exists: false,
classification: 'não criado',
status: null,
},
},
}),
).toBe('intenção: aprovado')
})
it('prefers the impl when it exists', () => {
expect(
planLabel({
plan: {
intention: {
path: 'docs/plans/test.md',
exists: true,
classification: 'aprovado',
status: { raw: 'aprovado', classification: 'aprovado' },
},
impl: {
path: 'docs/plans/test-impl.md',
exists: true,
classification: 'andou',
status: { raw: 'em execução', classification: 'andou' },
},
},
}),
).toBe('impl em execução')
})
})
describe('issueAction (OPS109 — blocked wins)', () => {
it('says travada when blocked label is present even if plan is aguardando', () => {
expect(
issueAction({
state: 'blocked',
plan: { intention: { classification: 'aguardando' }, impl: null },
blockedBy: [],
}),
).toBe('travada')
})
})
describe('issueState (OPS109 — closed issues)', () => {
it('returns done for closed issues even when the label still says in-progress', () => {
expect(
issueState(issue({ state: 'CLOSED', labels: [{ name: 'in-progress', color: '' }] })),
).toBe('done')
})
it('returns in-prod for closed in-prod issues', () => {
expect(issueState(issue({ state: 'CLOSED', labels: [{ name: 'in-prod', color: '' }] }))).toBe(
'in-prod',
)
})
})
describe('visibleWindow (OPS109 — list scroll math)', () => {
it('returns the whole list when it fits', () => {
expect(visibleWindow({ count: 7, cursor: 3, capacity: 10 })).toEqual({ start: 0, end: 7 })
})
it('keeps the cursor centered in a long list', () => {
expect(visibleWindow({ count: 100, cursor: 50, capacity: 10 })).toEqual({
start: 45,
end: 55,
})
})
it('clamps at the top and the bottom', () => {
expect(visibleWindow({ count: 100, cursor: 0, capacity: 10 })).toEqual({ start: 0, end: 10 })
expect(visibleWindow({ count: 100, cursor: 99, capacity: 10 })).toEqual({
start: 90,
end: 100,
})
})
it('never slices the cursor out at the window edges', () => {
const { start, end } = visibleWindow({ count: 23, cursor: 22, capacity: 4 })
expect(start).toBeLessThanOrEqual(22)
expect(end).toBeGreaterThan(22)
expect(end - start).toBe(4)
})
it('degrades to a single row for an empty or zero capacity', () => {
expect(visibleWindow({ count: 0, cursor: 0, capacity: 0 })).toEqual({ start: 0, end: 0 })
expect(visibleWindow({ count: 5, cursor: 2, capacity: 0 })).toEqual({ start: 2, end: 3 })
})
})
describe('toJsonPayload / buildPanelViewModel (OPS109 — no regressions)', () => {
it('marks a missing impl as não criado in the JSON contract', () => {
const model = buildPanelViewModel({
issues: [
issue({
number: 999,
body: '---\nid: TEST999\n---\nPlano: [`docs/plans/test999.md`](docs/plans/test999.md)',
}),
],
plansByNumber: new Map([
[
999,
{
intention: 'Status: aprovado\n',
impl: null,
uiDraft: { path: 'docs/plans/test999-ui-draft.html', exists: true },
},
],
]),
limit: 200,
})
const payload = toJsonPayload(model)
const row = payload.rows[0]
expect(row.plan.intention).toMatchObject({ classification: 'aprovado', status: 'aprovado' })
expect(row.plan.impl).toMatchObject({ exists: false, classification: 'não criado' })
expect(row.uiDraft).toEqual({ path: 'docs/plans/test999-ui-draft.html', exists: true })
})
})