Files
accounted/app/api/agent/conversations/[id]/__tests__/reject-pending.test.ts
T
1f7acbf144 feat(agent): one-tap "clear the proposals I didn't approve" (BoXon feedback) (#1798)
When the assistant stages several verifikat and the user approves only some,
the rest lingered as pending_operations in Granskning until the 30-day expiry —
manual per-item cleanup. Now:

- lib/agent/pending/reject-conversation-pending.ts rejects a conversation's
  still-pending proposals in one update (guarded on status='pending' so it never
  stamps over a committed verifikat; company-scoped; keyed on the conversation).
- POST /api/agent/conversations/[id]/reject-pending — the chat's "Rensa förslag
  som inte godkänts" button (appears when the thread has staged proposals; drops
  the cards from view).
- Auto-clear on archive: archiving a thread ("I'm done") clears its leftover
  proposals in the PATCH, best-effort.

Kept durable-by-default (proposals still come back on resume) — the button is
explicit user intent, not an auto-reject on every panel close, so resume still
works. 10 tests (helper + endpoint 401/404/404/happy); lint + guards + scoped
typecheck clean. UI button awaits founder visual sign-off.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 10:42:22 +02:00

62 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { NextResponse } from 'next/server'
import { createMockRequest, createMockRouteParams, parseJsonResponse, createQueuedMockSupabase } from '@/tests/helpers'
const { supabase: mockSupabase, enqueue, reset } = createQueuedMockSupabase()
const requireAuthMock = vi.fn()
vi.mock('@/lib/auth/require-auth', () => ({ requireAuth: (...a: unknown[]) => requireAuthMock(...a) }))
vi.mock('@/lib/company/context', () => ({
getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
requireCompanyId: vi.fn().mockResolvedValue('company-1'),
}))
vi.mock('@/lib/auth/require-write', () => ({ requireWritePermission: vi.fn().mockResolvedValue({ ok: true }) }))
const rejectPending = vi.fn()
vi.mock('@/lib/agent/pending/reject-conversation-pending', () => ({
rejectPendingForConversation: (...a: unknown[]) => rejectPending(...a),
}))
import { POST } from '../reject-pending/route'
const params = () => createMockRouteParams({ id: 'conv-1' })
beforeEach(() => {
vi.clearAllMocks()
reset()
requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase: mockSupabase, error: null })
rejectPending.mockResolvedValue(3)
})
describe('POST /api/agent/conversations/[id]/reject-pending', () => {
it('401 when unauthenticated', async () => {
requireAuthMock.mockResolvedValue({ user: null, supabase: mockSupabase, error: NextResponse.json({ error: 'x' }, { status: 401 }) })
const res = await POST(createMockRequest('/x', { method: 'POST' }), params())
expect((await parseJsonResponse(res)).status).toBe(401)
})
it('404 when the conversation is not the caller’s', async () => {
enqueue({ data: null }) // conversation ownership lookup misses
const res = await POST(createMockRequest('/x', { method: 'POST' }), params())
expect(res.status).toBe(404)
expect(rejectPending).not.toHaveBeenCalled()
})
it('404 when the caller is no longer a member of the company', async () => {
enqueue({ data: { id: 'conv-1', company_id: 'company-1', user_id: 'user-1' } }) // owns it
enqueue({ data: null }) // membership lookup misses
const res = await POST(createMockRequest('/x', { method: 'POST' }), params())
expect(res.status).toBe(404)
expect(rejectPending).not.toHaveBeenCalled()
})
it('clears the conversation’s pending proposals and returns the count', async () => {
enqueue({ data: { id: 'conv-1', company_id: 'company-1', user_id: 'user-1' } })
enqueue({ data: { role: 'owner' } })
const res = await POST(createMockRequest('/x', { method: 'POST' }), params())
const { status, body } = await parseJsonResponse<{ data: { cleared: number } }>(res)
expect(status).toBe(200)
expect(body.data.cleared).toBe(3)
expect(rejectPending).toHaveBeenCalledWith(mockSupabase, 'company-1', 'conv-1')
})
})