teqo/tests/unit/forgejoApi.unit.spec.ts
Francisco Solla f20305759a OPS50: port CI e scripts de agentes do GitHub para o Forgejo
Workflows nativos em .forgejo/workflows/ (ci, ci-pr, agent-pool,
automerge, issue-transition, plan-issue, vercel-promote, archive),
removendo .github/workflows e .github/actions. Runner Forgejo v13 com
services Postgres por porta + DB_HOST via gateway do bridge. Libs de
agente sobre o novo cliente forgejo-api.mjs (zero dependências,
GITHUB_SERVER_URL/FORGEJO_API_URL), helpers de workflow plain-Node.
pnpm issue corrigido (await em função sync) e validado ao vivo. Testes:
forgejoApi.unit.spec.ts novo, pin do matrix do ci.yml atualizado.

Closes #1 (parte 1 — docs/issues/pool seguem na parte 2)
2026-08-16 23:32:20 -03:00

212 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { createApi } from '../../scripts/lib/forgejo-api.mjs'
type FetchCall = {
url: string
method?: string
init?: { headers?: Record<string, string>; method?: string; body?: string }
body?: unknown
}
const ok = (body: unknown, status = 200) =>
new Response(typeof body === 'string' ? body : JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
describe('forgejo-api', () => {
it('builds the request with token, JSON body and explicit User-Agent', async () => {
const calls: FetchCall[] = []
const api = createApi({
base: 'https://git.solla.dev/api/v1',
token: 'tok',
repository: 'fsolla/teqo',
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init })
return ok({ number: 1, title: 'x', body: '', state: 'open', created_at: '', labels: [] })
},
})
await api.createIssue({ title: 't', body: 'b' })
expect(calls).toHaveLength(1)
expect(calls[0].url).toBe('https://git.solla.dev/api/v1/repos/fsolla/teqo/issues')
expect(calls[0].init!.method).toBe('POST')
expect(calls[0].init!.headers!.Authorization).toBe('token tok')
expect(calls[0].init!.headers!['User-Agent']).toContain('teqo-agent-scripts')
expect(JSON.parse(calls[0].init!.body!)).toEqual({ title: 't', body: 'b' })
})
it('defaults the base URL to git.solla.dev with the default repo path', async () => {
const calls: FetchCall[] = []
const api = createApi({
token: 'tok',
fetchImpl: async (url) => {
calls.push({ url: String(url) })
return ok([{ number: 1, title: '', body: '', state: 'open', created_at: '', labels: [] }])
},
})
await api.listIssues()
expect(calls[0].url).toContain('https://git.solla.dev/api/v1/repos/fsolla/teqo/issues?')
})
it('normalizes state and labels to the gh-flavored contract', async () => {
const api = createApi({
token: 'tok',
fetchImpl: async () =>
ok({
number: 7,
title: 't',
body: 'b',
state: 'closed',
created_at: '2026-01-01T00:00:00Z',
labels: [{ name: 'ready', color: '0E8A16' }],
}),
})
const issue = await api.getIssue(7)
expect(issue.state).toBe('CLOSED')
expect(issue.labels).toEqual([{ name: 'ready', color: '0E8A16' }])
expect(issue.createdAt).toBe('2026-01-01T00:00:00Z')
})
it('throws with the API error body on non-2xx', async () => {
const api = createApi({
token: 'tok',
fetchImpl: async () => ok('{"message":"nope"}', 403),
})
await expect(api.listIssues()).rejects.toThrow(/403/)
await expect(api.listIssues()).rejects.toThrow(/nope/)
})
it('fails closed when no token is available', async () => {
delete process.env.FORGEJO_API_TOKEN
delete process.env.GITHUB_TOKEN
const api = createApi({ fetchImpl: async () => ok('[]') })
await expect(api.listIssues()).rejects.toThrow(/Sem token/)
})
it('setLabels removes by id and adds by name', async () => {
const calls: FetchCall[] = []
const api = createApi({
token: 'tok',
fetchImpl: async (url, init) => {
calls.push({ url: String(url), method: init?.method, body: init?.body })
if (String(url).endsWith('/labels') && init?.method === 'GET') {
return ok([{ name: 'blocked', id: 42 }])
}
return ok(null)
},
})
await api.setLabels(5, { add: ['ready'], remove: ['blocked'] })
expect(calls.map((call) => [call.method, call.url])).toEqual([
['GET', 'https://git.solla.dev/api/v1/repos/fsolla/teqo/issues/5/labels'],
['DELETE', 'https://git.solla.dev/api/v1/repos/fsolla/teqo/issues/5/labels/42'],
['POST', 'https://git.solla.dev/api/v1/repos/fsolla/teqo/issues/5/labels'],
])
expect(JSON.parse(calls[2].body as string)).toEqual({ labels: ['ready'] })
})
it('workflowDispatch posts inputs and ref', async () => {
const calls: FetchCall[] = []
const api = createApi({
token: 'tok',
fetchImpl: async (url, init) => {
calls.push({ url: String(url), body: JSON.parse(String(init?.body ?? '{}')) })
return ok(null)
},
})
await api.workflowDispatch('agent-pool.yml', { ref: 'main', inputs: { action: 'tick' } })
expect(calls[0].url).toContain('/actions/workflows/agent-pool.yml/dispatches')
expect(calls[0].body).toEqual({ ref: 'main', inputs: { action: 'tick' } })
})
it('merges with rebase (repo canonical merge style)', async () => {
const calls: FetchCall[] = []
const api = createApi({
token: 'tok',
fetchImpl: async (url, init) => {
calls.push({ url: String(url), body: JSON.parse(String(init?.body ?? '{}')) })
return ok(null)
},
})
await api.mergePullRequest(4)
expect(calls[0].url).toContain('/pulls/4/merge')
expect(calls[0].body).toEqual({ Do: 'rebase' })
})
it('waitForChecks returns once every status is settled and no failure exists', async () => {
let poll = 0
const api = createApi({
token: 'tok',
fetchImpl: async (url) => {
if (String(url).includes('/pulls/')) {
return ok({
number: 4,
state: 'open',
merged: false,
mergeable: true,
head: { ref: 'x', sha: 'abc' },
})
}
poll += 1
const statuses =
poll < 2
? [{ context: 'CI / static', status: 'pending' }]
: [{ context: 'CI / static', status: 'success' }]
return ok({ statuses })
},
})
const pr = await api.waitForChecks(4, { pollMs: 1 })
expect(pr).toBeTruthy()
expect(poll).toBe(2)
})
it('getFileContents decodes base64 and updateFile sends sha', async () => {
const calls: FetchCall[] = []
const api = createApi({
token: 'tok',
fetchImpl: async (url, init) => {
calls.push({
url: String(url),
method: init?.method,
body: init?.body ? JSON.parse(String(init.body)) : null,
})
if (init?.method === 'GET') {
return ok({ content: Buffer.from('{"a":1}').toString('base64'), sha: 'shasha' })
}
return ok(null)
},
})
const file = await api.getFileContents('pool-state.json', 'pool-state')
expect(file.content).toBe('{"a":1}')
expect(file.sha).toBe('shasha')
await api.updateFile('pool-state.json', {
message: 'm',
content: '{"a":2}',
branch: 'pool-state',
sha: 'shasha',
})
expect(calls.at(-1)!.method).toBe('PUT')
expect(
Buffer.from((calls.at(-1)!.body as { content: string }).content, 'base64').toString(),
).toBe('{"a":2}')
expect((calls.at(-1)!.body as { sha: string }).sha).toBe('shasha')
})
})