teqo-backup/tests/unit/municipalitiesWithoutUpdateTool.unit.spec.ts
Francisco Solla 39fb3b47f0 fix(OPS62-followup): âncora de relógio no fixture do municipalitiesWithoutUpdateTool — elimina o flake de ordenação por raiz
Causa raiz (reproduzida, refuta a hipótese da Issue): daysAgo() chamava
Date.now() por chamada; as duas daysAgo(40) de Beta/Alpha podiam cair em ms
diferentes sob carga, deixando Alpha estritamente mais novo — sortStale
ordenava por timestamp e o tie-break por nome nunca disparava (0.08% das
construções sem pausa; 100% com pausa de 1 ms; fixture ancorado: 0 em 2000).
Fix test-only: âncora fixtureNowMs no load do módulo (padrão prioritiesTool)
+ hoist do timestamp empatado (igualdade estrutural) + comentário.
Nada muda em produção — o sort real é determinístico sobre timestamps fixos.
Verificação: spec 30/30, unit 2405, int 791, tsc/lint/format/cycles verdes.
2026-08-19 13:07:30 -03:00

296 lines
11 KiB
TypeScript

import type { Payload } from 'payload'
import { describe, expect, it, vi } from 'vitest'
import type { AIToolContext } from '@/lib/ai/types'
import { DAY_MS } from '@/lib/text'
import type { CampaignUser } from '@/payload-types'
import { getMunicipalitiesWithoutUpdate } from '@/utilities/ai/tools/getMunicipalitiesWithoutUpdate'
import { stub } from '../helpers/stub'
const denied = { error: 'Leitura de municípios negada.' }
type FindMock = ReturnType<typeof vi.fn>
const findResult = (docs: unknown[]) => ({ docs })
const ctxFor = (user: CampaignUser, payload: Payload): AIToolContext => ({ user, payload })
const leader = stub<CampaignUser>({ collection: 'campaignUser', role: 'leader' })
const coordinator = stub<CampaignUser>({ collection: 'campaignUser', role: 'coordinator' })
const advisor = stub<CampaignUser>({ collection: 'campaignUser', role: 'advisor' })
const scriptedPayload = (find: FindMock): Payload =>
stub<Payload>({ find: find as Payload['find'] })
const untouchablePayload = scriptedPayload(
vi.fn(() => {
throw new Error('gate must fail closed before any payload query')
}),
)
const municipalityDoc = (overrides: Record<string, unknown>) => ({
id: 1,
name: 'Amargosa',
slug: 'amargosa',
city: 'Amargosa',
region: 'Vale do Jiquiriçá',
kind: 'municipio',
lastUpdateAt: null,
advisors: [],
...overrides,
})
// Fixture clock anchored once at module load (same pattern as the prioritiesTool
// spec): per-call Date.now() drifting ≥1 ms between two daysAgo(40) calls made
// the tie-break pair unequal and flipped the name ordering (OPS62-followup CI
// flake 2026-08-18). The ordering spec also hoists its tied timestamp so the
// equality is structural, not just probabilistic.
const fixtureNowMs = Date.now()
const daysAgo = (days: number): string => new Date(fixtureNowMs - days * DAY_MS).toISOString()
type ExecutableTool = {
execute: (args: unknown, options?: unknown) => Promise<unknown>
}
const execute =
(payload: Payload, user: CampaignUser = coordinator) =>
(args: unknown) =>
(getMunicipalitiesWithoutUpdate(ctxFor(user, payload)) as unknown as ExecutableTool).execute!(
args,
)
const run = async (
find: FindMock,
args: unknown,
user: CampaignUser = coordinator,
): Promise<Record<string, unknown>> =>
(await execute(scriptedPayload(find), user)(args)) as Record<string, unknown>
describe('getMunicipalitiesWithoutUpdate gate (B189)', () => {
it('denies a leader with the chat-shaped error before any payload query', async () => {
const payload = untouchablePayload
await expect(execute(payload, leader)({})).resolves.toEqual(denied)
expect(payload.find).not.toHaveBeenCalled()
})
it('lets a coordinator pass and returns the empty staff shape', async () => {
const find = vi.fn().mockResolvedValue(findResult([]))
const result = await run(find, {})
expect(result.total).toBe(0)
expect(result.municipios).toEqual([])
expect(result.escopoRestrito).toBe(false)
expect(result.limiarDias).toBe(30)
expect(result.criterio).toContain('há mais de 30 dias')
expect(find).toHaveBeenCalledWith(
expect.objectContaining({
collection: 'municipality',
where: {},
overrideAccess: false,
}),
)
})
it('lets an advisor pass (RBAC atual via access control, não pelo gate)', async () => {
const find = vi.fn().mockResolvedValue(findResult([]))
const result = await run(find, {}, advisor)
expect(result.total).toBe(0)
expect(result.escopoRestrito).toBe(true)
})
})
describe('getMunicipalitiesWithoutUpdate coverage criterion (B189)', () => {
it('includes never-updated (top) and municipalities older than the threshold; excludes fresher ones', async () => {
const find = vi
.fn()
.mockResolvedValue(
findResult([
municipalityDoc({ id: 1, lastUpdateAt: null, name: 'Nunca' }),
municipalityDoc({ id: 2, lastUpdateAt: daysAgo(40), name: 'Velho' }),
municipalityDoc({ id: 3, lastUpdateAt: daysAgo(31), name: 'Borda incluída' }),
municipalityDoc({ id: 4, lastUpdateAt: daysAgo(30), name: 'Borda excluída' }),
municipalityDoc({ id: 5, lastUpdateAt: daysAgo(5), name: 'Fresco' }),
]),
)
const result = await run(find, {})
const ids = (result.municipios as Array<{ id: number }>).map((m) => m.id)
expect(result.total).toBe(3)
expect(result.nuncaAtualizados).toBe(1)
expect(ids).toEqual([1, 2, 3])
})
it('respects a custom threshold from the question (strictly greater)', async () => {
const find = vi
.fn()
.mockResolvedValue(
findResult([
municipalityDoc({ id: 1, lastUpdateAt: daysAgo(20) }),
municipalityDoc({ id: 2, lastUpdateAt: daysAgo(16) }),
municipalityDoc({ id: 3, lastUpdateAt: daysAgo(15) }),
municipalityDoc({ id: 4, lastUpdateAt: daysAgo(14) }),
]),
)
const result = await run(find, { days: 15 })
const ids = (result.municipios as Array<{ id: number }>).map((m) => m.id)
expect(ids).toEqual([1, 2])
expect(result.limiarDias).toBe(15)
expect(result.criterio).toContain('há mais de 15 dias')
})
it('orders never-updated first (by name), then oldest to newest (by name as tie-break)', async () => {
const stale40 = daysAgo(40)
const find = vi
.fn()
.mockResolvedValue(
findResult([
municipalityDoc({ id: 1, lastUpdateAt: stale40, name: 'Beta' }),
municipalityDoc({ id: 2, lastUpdateAt: null, name: 'Zulu' }),
municipalityDoc({ id: 3, lastUpdateAt: stale40, name: 'Alpha' }),
municipalityDoc({ id: 4, lastUpdateAt: null, name: 'Mike' }),
municipalityDoc({ id: 5, lastUpdateAt: daysAgo(32), name: 'Gama' }),
]),
)
const result = await run(find, {})
const names = (result.municipios as Array<{ nome: string }>).map((m) => m.nome)
expect(names).toEqual(['Mike', 'Zulu', 'Alpha', 'Beta', 'Gama'])
})
it('labels never-updated items with null days and the stagnation flag', async () => {
const find = vi
.fn()
.mockResolvedValue(
findResult([municipalityDoc({ id: 1, lastUpdateAt: null, name: 'Nunca' })]),
)
const result = await run(find, {})
const item = (result.municipios as Array<Record<string, unknown>>)[0]!
expect(item.diasSemAtualizacao).toBeNull()
expect(item.nuncaAtualizado).toBe(true)
expect(item.ultimaAtualizacao).toBeNull()
expect(item.tipo).toBe('Município inteiro')
})
})
describe('getMunicipalitiesWithoutUpdate scope resolution (B189)', () => {
it('resolves a region scope without accents into the region where', async () => {
const find = vi
.fn()
.mockResolvedValue(findResult([municipalityDoc({ id: 1, region: 'Vale do Jiquiriçá' })]))
const result = await run(find, { scope: 'Vale do Jiquiriça' })
expect(result.escopo).toEqual({ tipo: 'regiao', nome: 'Vale do Jiquiriçá' })
expect(find).toHaveBeenCalledWith(
expect.objectContaining({
where: { region: { equals: 'Vale do Jiquiriçá' } },
}),
)
expect(find).toHaveBeenCalledWith(expect.objectContaining({ where: { id: { in: [1] } } }))
})
it('resolves "Salvador" into the 19 zones as one city scope', async () => {
const find = vi.fn()
find.mockResolvedValueOnce(
findResult([municipalityDoc({ id: 1 }), municipalityDoc({ id: 2, name: 'Salvador ZE 2' })]),
)
find.mockResolvedValueOnce(findResult([]))
const result = await run(find, { scope: 'Salvador' })
expect(result.escopo).toEqual({ tipo: 'cidade', nome: 'Salvador (cidade)' })
expect(find).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ where: { city: { equals: 'Salvador' } } }),
)
})
it('resolves a municipality by name', async () => {
const find = vi.fn()
find.mockResolvedValueOnce(findResult([municipalityDoc({ id: 1 })]))
find.mockResolvedValueOnce(findResult([]))
const result = await run(find, { scope: 'amargosa' })
expect(result.escopo).toEqual({ tipo: 'municipio', nome: 'Amargosa' })
})
it('rejects an unknown scope with the family error message', async () => {
const find = vi.fn()
const result = await run(find, { scope: 'Atlântida' })
expect(result).toMatchObject({ error: expect.stringContaining('Escopo não reconhecido') })
expect(find).not.toHaveBeenCalled()
})
it('fails closed with the empty scoped set for an advisor asking outside the portfolio (no municipality read)', async () => {
const find = vi.fn().mockResolvedValue(findResult([]))
const result = await run(find, { scope: 'Vale do Jiquiriça' }, advisor)
expect(result.total).toBe(0)
expect(result.nuncaAtualizados).toBe(0)
expect(result.municipios).toEqual([])
expect(result.escopoRestrito).toBe(true)
expect(find).toHaveBeenCalledTimes(1)
expect(find).toHaveBeenCalledWith(
expect.objectContaining({ where: { region: { equals: 'Vale do Jiquiriçá' } } }),
)
})
it('filters the scope query by municipality ids when a scope resolved them', async () => {
const find = vi.fn()
find.mockResolvedValueOnce(findResult([municipalityDoc({ id: 1 }), municipalityDoc({ id: 2 })]))
find.mockResolvedValueOnce(findResult([]))
await run(find, { scope: 'Salvador' }, advisor)
expect(find).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
where: { id: { in: [1, 2] } },
overrideAccess: false,
user: advisor,
}),
)
})
})
describe('getMunicipalitiesWithoutUpdate advisors (B189)', () => {
it('resolves advisor names via a second campaignUser query and attaches them to items', async () => {
const find = vi.fn()
find.mockResolvedValueOnce(
findResult([municipalityDoc({ id: 1, advisors: [41, 42] }), municipalityDoc({ id: 2 })]),
)
find.mockResolvedValueOnce(findResult([{ id: 41, name: 'Ana Assessora' }]))
const result = await run(find, {})
const items = result.municipios as Array<{
assessores: Array<{ id: number; nome: string | null }>
}>
expect(items[0]!.assessores).toEqual([
{ id: 41, nome: 'Ana Assessora' },
{ id: 42, nome: null },
])
expect(items[1]!.assessores).toEqual([])
expect(find).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
collection: 'campaignUser',
where: { id: { in: [41, 42] } },
select: { name: true },
overrideAccess: false,
}),
)
})
it('keeps zone items typed as electoral zones', async () => {
const find = vi.fn().mockResolvedValue(
findResult([
municipalityDoc({
id: 1,
kind: 'zona',
name: 'Salvador ZE 3',
city: 'Salvador',
region: 'Metropolitano de Salvador',
}),
]),
)
const result = await run(find, {})
const item = (result.municipios as Array<Record<string, unknown>>)[0]!
expect(item.tipo).toBe('Zona eleitoral (Salvador)')
expect(item.cidade).toBe('Salvador')
})
})