feat(M13): cron /api/cron/cleanup-agent-artifacts — hard-delete FAILED/CANCELLED jobs >7 days

This commit is contained in:
Janpeter Visser 2026-05-01 13:20:38 +02:00
parent 2f746290a4
commit 9f31816455
4 changed files with 117 additions and 0 deletions

View file

@ -0,0 +1,63 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('@/lib/prisma', () => ({
prisma: {
claudeJob: { deleteMany: vi.fn() },
},
}))
import { prisma } from '@/lib/prisma'
import { POST } from '@/app/api/cron/cleanup-agent-artifacts/route'
const mockPrisma = prisma as unknown as {
claudeJob: { deleteMany: ReturnType<typeof vi.fn> }
}
const SECRET = 'test-cron-secret-abc123'
function makeReq(headers: Record<string, string> = {}): Request {
return new Request('http://localhost:3000/api/cron/cleanup-agent-artifacts', {
method: 'POST',
headers,
})
}
beforeEach(() => {
vi.clearAllMocks()
process.env.CRON_SECRET = SECRET
mockPrisma.claudeJob.deleteMany.mockResolvedValue({ count: 0 })
})
describe('POST /api/cron/cleanup-agent-artifacts', () => {
it('401 zonder Authorization-header', async () => {
const res = await POST(makeReq())
expect(res.status).toBe(401)
expect(mockPrisma.claudeJob.deleteMany).not.toHaveBeenCalled()
})
it('401 met verkeerde secret', async () => {
const res = await POST(makeReq({ authorization: 'Bearer wrong-secret' }))
expect(res.status).toBe(401)
expect(mockPrisma.claudeJob.deleteMany).not.toHaveBeenCalled()
})
it('200 met juiste secret + deleteMany aangeroepen voor FAILED/CANCELLED ouder dan 7 dagen', async () => {
mockPrisma.claudeJob.deleteMany.mockResolvedValue({ count: 5 })
const res = await POST(makeReq({ authorization: 'Bearer ' + SECRET }))
expect(res.status).toBe(200)
const body = await res.json()
expect(body.deleted).toBe(5)
expect(body.ran_at).toMatch(/^\d{4}-\d{2}-\d{2}T/)
const arg = mockPrisma.claudeJob.deleteMany.mock.calls[0][0]
expect(arg.where.status).toEqual({ in: ['FAILED', 'CANCELLED'] })
expect(arg.where.finished_at.lt).toBeInstanceOf(Date)
// cutoff should be approximately 7 days ago
const cutoff = arg.where.finished_at.lt as Date
const diffMs = Date.now() - cutoff.getTime()
const diffDays = diffMs / (1000 * 60 * 60 * 24)
expect(diffDays).toBeCloseTo(7, 0)
})
})

View file

@ -0,0 +1,24 @@
import { prisma } from '@/lib/prisma'
export const runtime = 'nodejs'
const CUTOFF_DAYS = 7
export async function POST(request: Request) {
const auth = request.headers.get('authorization')
const expected = process.env.CRON_SECRET
if (!expected || auth !== `Bearer ${expected}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const cutoff = new Date(Date.now() - CUTOFF_DAYS * 24 * 60 * 60 * 1000)
const { count: deleted } = await prisma.claudeJob.deleteMany({
where: {
status: { in: ['FAILED', 'CANCELLED'] },
finished_at: { lt: cutoff },
},
})
return Response.json({ deleted, ran_at: new Date().toISOString() })
}

View file

@ -484,6 +484,32 @@ curl -X POST -H "Authorization: Bearer $CRON_SECRET" \
---
## Cron — Cleanup agent artifacts
### `POST /api/cron/cleanup-agent-artifacts`
Vercel cron handler die dagelijks draait. Verwijdert `FAILED` en `CANCELLED` claude_jobs waarvan `finished_at` ouder is dan 7 dagen. Hard-delete — geen historische waarde; audit-trail zit in git-commits.
**Auth:** `Authorization: Bearer ${CRON_SECRET}` — zelfde mechanisme als `/api/cron/expire-questions`. Zonder secret of bij mismatch: 401.
**Schedule:** `0 3 * * *` (dagelijks om 03:00 UTC).
**Response 200:**
```json
{
"deleted": 3,
"ran_at": "2026-05-01T03:00:00.000Z"
}
```
**Voorbeeld (handmatige trigger):**
```bash
curl -X POST -H "Authorization: Bearer $CRON_SECRET" \
https://your-app.vercel.app/api/cron/cleanup-agent-artifacts
```
---
## Voorbeeldworkflow voor Claude Code
1. **Probe:** `GET /api/health?db=1` — bevestig dat de service en DB bereikbaar zijn.

View file

@ -4,6 +4,10 @@
{
"path": "/api/cron/expire-questions",
"schedule": "0 4 * * *"
},
{
"path": "/api/cron/cleanup-agent-artifacts",
"schedule": "0 3 * * *"
}
]
}