teqo/tests/unit/agentPlanLifecycle.unit.spec.ts
Cursor Agent b7f4e16c0f
feat(ops): OPS18 — Action promote Related #N no merge
Safety net dual com agent:ready: no merge em main, workflow lê Related #N
e promove Issues ainda aguardando plano (canPromotePlanIssue), soft-skip
idempotente. Helper parseRelatedIssueNumbers + pins unitários.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-03 00:20:48 +00:00

110 lines
3.2 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import {
canPromotePlanIssue,
parseRelatedIssueNumbers,
resolveRegisterStateLabel,
} from '../../scripts/lib/agent-plan-lifecycle.mjs'
type TestIssue = {
number: number
title: string
body: string
state: string
labels: Array<{ name: string }>
}
const issue = (over: Partial<TestIssue> = {}): TestIssue => ({
number: 1,
title: 'Issue de teste',
body: 'Plano: [`docs/plans/x.md`](docs/plans/x.md)',
state: 'OPEN',
labels: [{ name: 'blocked' }, { name: 'prio:P2' }],
...over,
})
describe('resolveRegisterStateLabel', () => {
it('keeps chores without a plan as ready', () => {
expect(resolveRegisterStateLabel({ hasPlan: false })).toBe('ready')
})
it('starts plan-linked issues as blocked (OPS17)', () => {
expect(resolveRegisterStateLabel({ hasPlan: true })).toBe('blocked')
})
it('honors explicit --blocked even without a plan', () => {
expect(resolveRegisterStateLabel({ hasPlan: false, explicitBlocked: true })).toBe('blocked')
})
it('stays blocked when both --plan and --blocked are set', () => {
expect(resolveRegisterStateLabel({ hasPlan: true, explicitBlocked: true })).toBe('blocked')
})
})
describe('canPromotePlanIssue', () => {
it('accepts an open blocked issue with a docs/plans/ link', () => {
expect(canPromotePlanIssue(issue())).toEqual({ ok: true })
})
it('rejects closed issues', () => {
expect(canPromotePlanIssue(issue({ state: 'CLOSED' }))).toEqual({
ok: false,
reason: 'not-open',
})
})
it('rejects issues that are not blocked', () => {
expect(canPromotePlanIssue(issue({ labels: [{ name: 'ready' }] }))).toEqual({
ok: false,
reason: 'not-blocked',
})
})
it.each(['in-progress', 'done', 'in-prod'])(
'rejects when %s coexists with blocked',
(stateLabel) => {
expect(
canPromotePlanIssue(issue({ labels: [{ name: 'blocked' }, { name: stateLabel }] })),
).toEqual({ ok: false, reason: 'state-label' })
},
)
it('rejects human-gated blocked issues even when they link a plan', () => {
expect(
canPromotePlanIssue(
issue({
labels: [{ name: 'blocked' }, { name: 'needs:consent' }],
}),
),
).toEqual({ ok: false, reason: 'needs-human' })
})
it('rejects blocked issues without a plan link (product block, not awaiting plan)', () => {
expect(canPromotePlanIssue(issue({ body: 'bloqueio jurídico' }))).toEqual({
ok: false,
reason: 'no-plan-link',
})
})
})
describe('parseRelatedIssueNumbers', () => {
it('extracts Related #N case-insensitively and dedupes', () => {
expect(parseRelatedIssueNumbers('Related #296\nrelated #296 and RELATED #301')).toEqual([
296, 301,
])
})
it('ignores closing keywords (Closes/Fixes/Resolves)', () => {
expect(parseRelatedIssueNumbers('Closes #10\nFixes #11\nResolves #12\nRelated #296')).toEqual([
296,
])
})
it('returns empty for missing/blank bodies', () => {
expect(parseRelatedIssueNumbers(undefined)).toEqual([])
expect(parseRelatedIssueNumbers('')).toEqual([])
expect(parseRelatedIssueNumbers('no refs here')).toEqual([])
})
})