teqo-backup/tests/unit/vercelDeployCooldown.unit.spec.ts
Cursor Agent cbddd8c796
OPS11 — cooldown de 30 min entre deploys de produção (Vercel)
- Script vercel-deploy-cooldown consulta último deploy READY via API Vercel
- Job deploy em ci.yml adia upload quando idade < 30 min (sucesso, não falha)
- Job requeue espera o residual e re-dispara ci.yml
- vercel deploy passa --archive=tgz para reduzir pressão de upload
- Testes unitários para computeCooldownWait e fetch da API

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 10:54:55 +00:00

107 lines
3.2 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import {
computeCooldownWait,
DEPLOY_COOLDOWN_MS,
evaluateDeployCooldown,
fetchLastProductionReadyAt,
lastProductionReadyAtFromDeployment,
} from '../../scripts/lib/vercel-deploy-cooldown.mjs'
const fakeResponse = (ok: boolean, status: number, payload: unknown) =>
({
ok,
status,
text: async () => (payload === null ? '' : JSON.stringify(payload)),
}) as Response
const captureFetch = (response: Response) => {
const calls: Array<{ url: string; init: RequestInit }> = []
const fetchImpl = async (url: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(url), init: init ?? {} })
return response
}
return { calls, fetchImpl: fetchImpl as typeof fetch }
}
describe('vercelDeployCooldown', () => {
it('uses a 30-minute cooldown constant', () => {
expect(DEPLOY_COOLDOWN_MS).toBe(30 * 60 * 1000)
})
it('lastProductionReadyAtFromDeployment prefers ready over createdAt', () => {
expect(lastProductionReadyAtFromDeployment({ ready: 100, createdAt: 50 })).toBe(100)
expect(lastProductionReadyAtFromDeployment({ createdAt: 50 })).toBe(50)
expect(lastProductionReadyAtFromDeployment(null)).toBeNull()
})
it('computeCooldownWait is inactive when no prior deploy exists', () => {
expect(computeCooldownWait(null, 1_000_000)).toEqual({
active: false,
waitSeconds: 0,
ageMs: null,
})
})
it('computeCooldownWait defers when the last deploy is younger than 30 minutes', () => {
const now = 1_700_000_000_000
const last = now - 10 * 60 * 1000
expect(computeCooldownWait(last, now)).toEqual({
active: true,
waitSeconds: 20 * 60,
ageMs: 10 * 60 * 1000,
})
})
it('computeCooldownWait clears when the last deploy is older than 30 minutes', () => {
const now = 1_700_000_000_000
const last = now - 31 * 60 * 1000
expect(computeCooldownWait(last, now)).toEqual({
active: false,
waitSeconds: 0,
ageMs: 31 * 60 * 1000,
})
})
it('fetchLastProductionReadyAt reads the newest READY production deployment', async () => {
const { calls, fetchImpl } = captureFetch(
fakeResponse(true, 200, {
deployments: [{ ready: 1_700_000_000_000, createdAt: 1_699_999_000_000 }],
}),
)
await expect(
fetchLastProductionReadyAt({
token: 'token',
projectId: 'prj_test',
teamId: 'team_test',
fetchImpl,
}),
).resolves.toBe(1_700_000_000_000)
expect(calls[0]!.url).toContain('/v6/deployments?')
expect(calls[0]!.url).toContain('projectId=prj_test')
expect(calls[0]!.url).toContain('teamId=team_test')
expect(calls[0]!.url).toContain('target=production')
expect(calls[0]!.url).toContain('state=READY')
})
it('evaluateDeployCooldown surfaces API failures', async () => {
const { fetchImpl } = captureFetch(
fakeResponse(false, 403, { error: { message: 'forbidden' } }),
)
await expect(
evaluateDeployCooldown({
token: 'token',
projectId: 'prj_test',
teamId: 'team_test',
fetchImpl,
}),
).rejects.toThrow(/403/)
})
})