tests/build-log-snapshot.unit.test.ts
Rama main · réplica de lectura del repositorio original.
import { Hono } from 'hono'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { onError } from '../supabase/functions/_backend/utils/on_error.ts'
import { streamBuildLogs } from '../supabase/functions/_backend/public/build/logs.ts'
const mocks = vi.hoisted(() => ({ query: vi.fn(), permission: vi.fn(), token: vi.fn(), fetch: vi.fn() }))
vi.mock('../supabase/functions/_backend/utils/pg_rls.ts', () => ({
apikeyPrincipal: vi.fn(), withRlsContext: async (_c: unknown, _principal: unknown, fn: (client: unknown) => unknown) => fn({}), queryJson: (...args: unknown[]) => mocks.query(...args),
}))
vi.mock('../supabase/functions/_backend/utils/rbac.ts', () => ({ checkPermission: (...args: unknown[]) => mocks.permission(...args) }))
vi.mock('../supabase/functions/_backend/utils/discord.ts', () => ({ sendDiscordAlert500: vi.fn() }))
vi.mock('../supabase/functions/_backend/utils/posthog.ts', () => ({ capturePosthogException: vi.fn() }))
vi.mock('../supabase/functions/_backend/utils/utils.ts', () => ({ backgroundTask: vi.fn(), getEnv: (_c: unknown, key: string) => ({ BUILDER_URL: 'https://builder.example.com', BUILDER_API_KEY: 'fake-key', JWT_SECRET: 'fake-secret' })[key] }))
vi.mock('../supabase/functions/_backend/public/build/start.ts', () => ({ generateLogStreamToken: (...args: unknown[]) => mocks.token(...args) }))
const app = new Hono()
app.onError(onError('build'))
app.get('/', c => streamBuildLogs(c, 'example-cloud-job', 'com.example.app', { key: 'example-test-key', user_id: 'example-user' }, true))
beforeEach(() => {
vi.resetAllMocks()
mocks.query.mockResolvedValue([{ app_id: 'com.example.app' }])
mocks.permission.mockResolvedValue(true)
mocks.token.mockResolvedValue('job-scoped-token')
mocks.fetch.mockResolvedValue(Response.json({ logs: 'Code signing failed', cursor: 12, first_available_id: 1, truncated: false, retention: 'Recent buffered logs only' }))
vi.stubGlobal('fetch', mocks.fetch)
})
afterEach(() => vi.unstubAllGlobals())
describe('native build log monitoring', () => {
it('uses the actual builder snapshot route, returns explanations, and never cancels on disconnect', async () => {
const controller = new AbortController()
const response = await app.request('http://local/?cursor=7', { signal: controller.signal })
expect(response.status).toBe(200)
expect(response.headers.get('cache-control')).toBe('no-store')
expect(await response.json()).toMatchObject({ cursor: 12, format: 'text', diagnostics: [expect.objectContaining({ classification: 'error', evidence: 'Code signing failed' })] })
const [url, init] = mocks.fetch.mock.calls[0]
expect(String(url)).toBe('https://builder.example.com/build_logs_direct/example-cloud-job/snapshot?cursor=7')
expect(init.headers).toEqual({ 'x-capgo-log-token': 'job-scoped-token' })
controller.abort()
expect(mocks.fetch).toHaveBeenCalledTimes(1)
expect(mocks.permission).toHaveBeenCalledTimes(1)
})
it('rejects a job bound to a different app before minting a token or contacting the builder', async () => {
mocks.query.mockResolvedValue([{ app_id: 'com.example.other' }])
expect((await app.request('http://local/')).status).toBeGreaterThanOrEqual(400)
expect(mocks.token).not.toHaveBeenCalled()
expect(mocks.fetch).not.toHaveBeenCalled()
})
it('requires log permission', async () => {
mocks.permission.mockResolvedValue(false)
expect((await app.request('http://local/')).status).toBeGreaterThanOrEqual(400)
expect(mocks.fetch).not.toHaveBeenCalled()
})
it('rejects invalid cursors', async () => {
expect((await app.request('http://local/?cursor=-1')).status).toBeGreaterThanOrEqual(400)
expect(mocks.fetch).not.toHaveBeenCalled()
})
it('reports unavailable builder snapshots as an error, not empty successful logs', async () => {
mocks.fetch.mockResolvedValue(new Response('not found', { status: 404 }))
expect((await app.request('http://local/')).status).toBeGreaterThanOrEqual(400)
})
})
it.each([
() => new Response('{broken', { headers: { 'content-type': 'application/json' } }),
() => Response.json({ logs: null }),
])('identifies incompatible builder responses as snapshot dependency errors', async (response) => {
mocks.fetch.mockResolvedValue(response())
const result = await app.request('http://local/')
expect(result.status).toBe(502)
expect(await result.text()).toContain('builder_error')
})