Files
accounted/lib/documents/__tests__/unlinked-documents.test.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

114 lines
4.8 KiB
TypeScript

/**
* The unlinked-document predicate, and above all the mime allow-list, which is
* the part that decides whether this surface is useful or actively harmful.
*/
import { describe, it, expect } from 'vitest'
import { createQueuedMockSupabase } from '@/tests/helpers'
import {
fetchUnlinkedDocuments,
UNDERLAG_MIME_TYPES,
UNLINKED_DOCUMENT_SCAN_CAP,
} from '../unlinked-documents'
type Enqueue = (r: { data?: unknown; error?: unknown; count?: number | null }) => void
/** One empty result per referencing table, so nothing claims any candidate. */
function enqueueNoReferences(enqueue: Enqueue) {
for (let i = 0; i < 8; i += 1) enqueue({ data: [] })
}
const doc = (id: string, overrides: Record<string, unknown> = {}) => ({
id,
file_name: `${id}.pdf`,
mime_type: 'application/pdf',
file_size_bytes: 1024,
upload_source: 'api',
created_at: '2026-08-01T00:00:00.000Z',
...overrides,
})
describe('UNDERLAG_MIME_TYPES', () => {
it('excludes application/json, which on this surface is only the PSD2 archive', () => {
// Measured on production 2026-08-27: of the document rows referenced by
// nothing, application/json was 11 309 of 11 309 archived PSD2 bank-API
// responses, and 0 of 4 495 pdf/png/jpeg/heic rows were. Those archives are
// unlinked BY DESIGN. Admitting them here would hand an agent 11 309 items
// of work it must not do, which is worse than showing nothing at all.
expect(UNDERLAG_MIME_TYPES).not.toContain('application/json')
})
it('is an allow-list, so a future machine payload format stays out by default', () => {
// The alternative, excluding known-bad filenames, leaks every new archive
// format until someone notices and adds another exclusion.
for (const mime of UNDERLAG_MIME_TYPES) {
expect(mime === 'application/pdf' || mime.startsWith('image/')).toBe(true)
}
})
})
describe('fetchUnlinkedDocuments', () => {
it('costs exactly one query when the company has no candidates', async () => {
const { supabase, enqueue } = createQueuedMockSupabase()
enqueue({ data: [] })
// Deliberately NOT enqueueing the eight reference lookups: if the early
// return regressed, the mock would starve and this test would fail.
const result = await fetchUnlinkedDocuments(supabase as never, 'company-1')
expect(result).toEqual({ documents: [], count: 0, capped: false })
})
it('returns candidates that no table references', async () => {
const { supabase, enqueue } = createQueuedMockSupabase()
enqueue({ data: [doc('d-1'), doc('d-2')] })
enqueueNoReferences(enqueue)
const result = await fetchUnlinkedDocuments(supabase as never, 'company-1')
expect(result.count).toBe(2)
expect(result.documents.map((d) => d.id)).toEqual(['d-1', 'd-2'])
expect(result.capped).toBe(false)
})
it('drops a candidate claimed by any one of the eight referencing tables', async () => {
// Walk the tables one at a time: a document claimed only by the Nth table
// must still be excluded, which is what catches a missing entry in the list.
for (let table = 0; table < 8; table += 1) {
const { supabase, enqueue } = createQueuedMockSupabase()
enqueue({ data: [doc('d-claimed'), doc('d-free')] })
for (let i = 0; i < 8; i += 1) {
// Every referencing query selects its own column name, and the
// implementation reads whichever key the row carries.
enqueue({ data: i === table ? [{ document_id: 'd-claimed', document_attachment_id: 'd-claimed', xml_document_id: 'd-claimed', dokument_id: 'd-claimed', file_document_id: 'd-claimed' }] : [] })
}
const result = await fetchUnlinkedDocuments(supabase as never, 'company-1')
expect(result.documents.map((d) => d.id), `table index ${table}`).toEqual(['d-free'])
}
})
it('reports capped when the candidate scan hits the limit', async () => {
const { supabase, enqueue } = createQueuedMockSupabase()
const many = Array.from({ length: 3 }, (_, i) => doc(`d-${i}`))
enqueue({ data: many })
enqueueNoReferences(enqueue)
const result = await fetchUnlinkedDocuments(supabase as never, 'company-1', { limit: 3 })
// count is a floor, not a total, and the caller says so in its prose.
expect(result.capped).toBe(true)
expect(result.count).toBe(3)
})
it('keeps the scan cap small enough that the eight .in() lookups stay valid URLs', () => {
// Each candidate id is echoed back through eight .in(column, ids) queries,
// and a UUID costs ~38 bytes in a PostgREST query string. Past a few
// hundred the URL exceeds what the gateway accepts, the lookups fail, and
// the "claims nothing" fallback turns every candidate into a false
// positive. This is the constraint that sets the cap, not table size.
const bytesPerLookup = UNLINKED_DOCUMENT_SCAN_CAP * 38
expect(bytesPerLookup).toBeLessThan(16_000)
})
})