feat(documents): suggest inbox folder and type with AI
This commit is contained in:
@@ -17,6 +17,7 @@ import gateway from "../src/routes/scannerInboxGateway"
|
||||
import inboxRoutes from "../src/routes/documentInbox"
|
||||
import corsPlugin from "../src/plugins/cors"
|
||||
import { createScannerInboxService, SCANNER_UPLOAD_LIMIT, validateScannerFile, type ScannerInboxService } from "../src/modules/scanner-inbox.service"
|
||||
import type { FilingSuggestionResult } from "../src/modules/scanner-inbox-suggestions"
|
||||
|
||||
const execute = promisify(execFile)
|
||||
const basic = (username = "scanner", password = "secret") => ({ authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` })
|
||||
@@ -208,9 +209,18 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
|
||||
await migrate(db, { migrationsFolder: tempMigrations })
|
||||
const stored = new Map<string, Buffer>()
|
||||
let storageFailure = false
|
||||
let aiResult: FilingSuggestionResult = { suggestion: null }
|
||||
const analyses: any[] = []
|
||||
let analysisGate: Promise<void> | undefined
|
||||
let analysisStarted: (() => void) | undefined
|
||||
const service = createScannerInboxService(db, {
|
||||
async put(key, body) { stored.set(key, Buffer.from(body)); if (storageFailure) throw new Error("Simulated object store failure") },
|
||||
async remove(key) { stored.delete(key) },
|
||||
}, async (document, candidates) => {
|
||||
analyses.push({ document, candidates })
|
||||
analysisStarted?.()
|
||||
await analysisGate
|
||||
return aiResult
|
||||
})
|
||||
const first = await service.credentials(1)
|
||||
const second = await service.credentials(2)
|
||||
@@ -322,6 +332,43 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
|
||||
[targetFolder, targetType, foreignFolder, archivedFolder])
|
||||
const fileInto = (id: string, folder: string | null = targetFolder, type: string = targetType, headers = {}) =>
|
||||
app.inject({ method: "POST", url: `/api/document-inbox/${id}/file`, payload: { folder, type }, headers })
|
||||
const suggest = (id: string, headers = {}) => app.inject({ method: "POST", url: `/api/document-inbox/${id}/suggestion`, headers })
|
||||
await t.test("AI analysis is tenant scoped, validates destinations and never files a document", async () => {
|
||||
const id = (await service.list(1, 50, 0, "scan.pdf")).documents[0].id
|
||||
const before = (await pool.query("SELECT folder, type, extracted_text FROM files WHERE id=$1", [id])).rows[0]
|
||||
aiResult = { suggestion: { folderId: targetFolder, typeId: targetType, confidence: "high", reason: "Rechnung erkannt." } }
|
||||
assert.equal((await suggest(id, { "x-test-tenant": "2" })).statusCode, 404)
|
||||
assert.equal((await suggest("invalid")).statusCode, 400)
|
||||
await pool.query("INSERT INTO auth_profiles VALUES (1, $1, 123)", [userId])
|
||||
assert.equal((await suggest(id)).statusCode, 403)
|
||||
await pool.query("DELETE FROM auth_profiles")
|
||||
assert.equal(analyses.length, 0)
|
||||
const result = await suggest(id, { "x-test-admin": "false" })
|
||||
assert.equal(result.statusCode, 200, result.body)
|
||||
assert.deepEqual(result.json().suggestion, aiResult.suggestion)
|
||||
assert.equal(analyses[0].document.name, "scan.pdf")
|
||||
assert.deepEqual(analyses[0].candidates.folders.map((folder: any) => folder.id), [targetFolder])
|
||||
assert.ok(!analyses[0].candidates.types.some((type: any) => [foreignType, archivedType].includes(type.id)))
|
||||
assert.deepEqual((await pool.query("SELECT folder, type, extracted_text FROM files WHERE id=$1", [id])).rows[0], before)
|
||||
assert.equal((await service.list(1, 50, 0, "scan.pdf")).total, 1)
|
||||
aiResult = { suggestion: { ...aiResult.suggestion!, folderId: foreignFolder } }
|
||||
assert.equal((await suggest(id)).json().suggestion, null)
|
||||
})
|
||||
await t.test("AI results are checked again if destination availability changes during analysis", async () => {
|
||||
const id = (await service.list(1, 50, 0, "scan.pdf")).documents[0].id
|
||||
aiResult = { suggestion: { folderId: targetFolder, typeId: targetType, confidence: "high", reason: "Rechnung erkannt." } }
|
||||
let finish!: () => void
|
||||
analysisGate = new Promise(resolve => { finish = resolve })
|
||||
const started = new Promise<void>(resolve => { analysisStarted = resolve })
|
||||
const pending = suggest(id).then(response => response)
|
||||
await started
|
||||
await pool.query("UPDATE folders SET archived=true WHERE id=$1", [targetFolder])
|
||||
finish()
|
||||
assert.equal((await pending).json().suggestion, null)
|
||||
await pool.query("UPDATE folders SET archived=false WHERE id=$1", [targetFolder])
|
||||
analysisGate = undefined
|
||||
analysisStarted = undefined
|
||||
})
|
||||
await t.test("filing options and destinations exclude other tenants and archived entries", async () => {
|
||||
const options = await app.inject({ method: "GET", url: "/api/document-inbox/filing-options" })
|
||||
assert.equal(options.statusCode, 200)
|
||||
@@ -364,6 +411,9 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
|
||||
assert.equal(row.name, before.name)
|
||||
assert.deepEqual(stored.get(row.path), pdf)
|
||||
assert.equal((await service.list(1, 50, 0, "scan.pdf")).total, 0)
|
||||
const analysisCount = analyses.length
|
||||
assert.equal((await suggest(id)).statusCode, 404)
|
||||
assert.equal(analyses.length, analysisCount)
|
||||
assert.equal((await send("scan.pdf")).statusCode, 204)
|
||||
assert.equal((await service.list(1, 50, 0, "scan.pdf")).total, 0)
|
||||
assert.equal((await service.list(2, 50, 0, "scan.pdf")).total, 1)
|
||||
|
||||
Reference in New Issue
Block a user