feat(M13): cron /api/cron/cleanup-agent-artifacts — hard-delete FAILED/CANCELLED jobs >7 days
This commit is contained in:
parent
2f746290a4
commit
9f31816455
4 changed files with 117 additions and 0 deletions
63
__tests__/api/cron-cleanup-agent-artifacts.test.ts
Normal file
63
__tests__/api/cron-cleanup-agent-artifacts.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
24
app/api/cron/cleanup-agent-artifacts/route.ts
Normal file
24
app/api/cron/cleanup-agent-artifacts/route.ts
Normal 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() })
|
||||
}
|
||||
26
docs/API.md
26
docs/API.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
{
|
||||
"path": "/api/cron/expire-questions",
|
||||
"schedule": "0 4 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/cron/cleanup-agent-artifacts",
|
||||
"schedule": "0 3 * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue