feat(documents): suggest inbox folder and type with AI
This commit is contained in:
@@ -19,7 +19,7 @@ function setup(api, apiBase = 'https://fedeo.example/backend') {
|
||||
let mount
|
||||
let unmount
|
||||
const context = vm.createContext({
|
||||
require, exports: {}, URL,
|
||||
require, exports: {}, URL, AbortController,
|
||||
ref: vue.ref, computed: vue.computed, watch: vue.watch,
|
||||
useAuthStore: () => auth,
|
||||
useRuntimeConfig: () => ({ public: { apiBase } }),
|
||||
@@ -141,7 +141,7 @@ test('successful filing sends folder and type, removes the entry and returns fro
|
||||
assert.equal(post.url, '/api/document-inbox/scan/file')
|
||||
assert.equal(post.options.body.folder, null)
|
||||
assert.equal(post.options.body.type, 'invoice')
|
||||
assert.equal(ui.run('filingOpen.value'), false)
|
||||
assert.equal(ui.run('previewOpen.value'), false)
|
||||
assert.equal(ui.run('page.value'), 1)
|
||||
assert.equal(requests.at(-1).options.query.offset, 0)
|
||||
assert.equal(ui.run('documents.value[0].id'), 'remaining')
|
||||
@@ -159,7 +159,7 @@ test('filing failure preserves the document and selected values for retry', asyn
|
||||
await settle()
|
||||
await ui.run('fileDocument()')
|
||||
assert.equal(ui.run('filingError.value'), 'Ordner nicht verfügbar')
|
||||
assert.equal(ui.run('filingOpen.value'), true)
|
||||
assert.equal(ui.run('previewOpen.value'), true)
|
||||
assert.equal(ui.run('filingType.value'), 'invoice')
|
||||
assert.equal(ui.run('documents.value[0].id'), 'scan')
|
||||
assert.equal(ui.run('canFile.value'), true)
|
||||
@@ -184,9 +184,98 @@ test('tenant switching discards pending folder options and filing responses', as
|
||||
await settle()
|
||||
pending.resolve(stage === 'options' ? filingOptions : { id: 'scan' })
|
||||
await operation
|
||||
assert.equal(ui.run('filingOpen.value'), false)
|
||||
assert.equal(ui.run('previewOpen.value'), false)
|
||||
assert.equal(ui.run('filingFolders.value.length'), 0)
|
||||
assert.equal(ui.run('filingTypes.value.length'), 0)
|
||||
assert.equal(ui.run('documents.value[0].id'), 'tenant-2')
|
||||
}
|
||||
})
|
||||
|
||||
const aiSuggestion = { folderId: 'folder', typeId: 'invoice', confidence: 'high', reason: 'Rechnung erkannt.' }
|
||||
|
||||
test('AI preselection only files the document after confirmation and reuses its preview result', async t => {
|
||||
const requests = []
|
||||
const ui = setup(async (url, options) => {
|
||||
requests.push({ url, options })
|
||||
if (url.endsWith('/filing-options')) return filingOptions
|
||||
if (url.endsWith('/suggestion')) return { suggestion: aiSuggestion }
|
||||
if (url.endsWith('/file')) return { id: 'scan' }
|
||||
return { documents: [], total: 0 }
|
||||
})
|
||||
t.after(ui.close)
|
||||
await ui.run('openPreview({ id: "scan", name: "scan.pdf" })')
|
||||
await settle()
|
||||
assert.equal(ui.run('filingFolder.value'), 'folder')
|
||||
assert.equal(ui.run('filingType.value'), 'invoice')
|
||||
assert.equal(ui.run('canFile.value'), true)
|
||||
assert.equal(requests.some(request => request.url.endsWith('/file')), false)
|
||||
ui.run('previewOpen.value = false')
|
||||
await settle()
|
||||
await ui.run('openPreview({ id: "scan", name: "scan.pdf" })')
|
||||
await settle()
|
||||
assert.equal(requests.filter(request => request.url.endsWith('/suggestion')).length, 1)
|
||||
await ui.run('fileDocument()')
|
||||
const save = requests.find(request => request.url.endsWith('/file'))
|
||||
assert.equal(save.options.body.folder, 'folder')
|
||||
assert.equal(save.options.body.type, 'invoice')
|
||||
assert.equal(ui.run('previewOpen.value'), false)
|
||||
})
|
||||
|
||||
test('late AI results preserve manual choices, and can still be explicitly applied', async t => {
|
||||
const pending = deferred()
|
||||
const ui = setup(url => url.endsWith('/suggestion') ? pending.promise : Promise.resolve(filingOptions))
|
||||
t.after(ui.close)
|
||||
await ui.run('openPreview({ id: "scan", name: "scan.pdf" })')
|
||||
ui.run('filingType.value = "other"; selectionTouched.value = true')
|
||||
pending.resolve({ suggestion: aiSuggestion })
|
||||
await settle()
|
||||
assert.equal(ui.run('filingFolder.value'), 'root')
|
||||
assert.equal(ui.run('filingType.value'), 'other')
|
||||
assert.equal(ui.run('suggestion.value.typeId'), 'invoice')
|
||||
ui.run('applySuggestion()')
|
||||
assert.equal(ui.run('filingFolder.value'), 'folder')
|
||||
assert.equal(ui.run('filingType.value'), 'invoice')
|
||||
})
|
||||
|
||||
test('closing, changing documents or switching tenant cancels and discards pending AI responses', async t => {
|
||||
for (const action of ['close', 'document', 'tenant', 'unmount']) {
|
||||
const pending = deferred()
|
||||
const signals = []
|
||||
const ui = setup((url, options) => {
|
||||
if (url.endsWith('/suggestion')) { signals.push(options.signal); return pending.promise }
|
||||
if (url.endsWith('/filing-options')) return Promise.resolve(filingOptions)
|
||||
return Promise.resolve({ documents: [], total: 0 })
|
||||
})
|
||||
t.after(ui.close)
|
||||
await ui.run('openPreview({ id: "scan", name: "scan.pdf" })')
|
||||
if (action === 'close') ui.run('previewOpen.value = false')
|
||||
if (action === 'tenant') ui.auth.activeTenant = '2'
|
||||
if (action === 'document') await ui.run('openPreview({ id: "next", name: "next.pdf" })')
|
||||
if (action === 'unmount') ui.close()
|
||||
await settle()
|
||||
assert.equal(signals[0].aborted, true)
|
||||
// Keep the replacement document's request separate from the stale first result.
|
||||
if (action === 'document') ui.run('previewOpen.value = false')
|
||||
await settle()
|
||||
pending.resolve({ suggestion: aiSuggestion })
|
||||
await settle()
|
||||
assert.equal(ui.run('suggestion.value'), null)
|
||||
assert.equal(ui.run('suggestionCache.size'), 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('AI failures leave manual filing available and optional folder defaults do not overwrite the AI type', async t => {
|
||||
const ui = setup(async url => {
|
||||
if (url.endsWith('/suggestion')) throw { data: { message: 'KI nicht verfügbar' } }
|
||||
return filingOptions
|
||||
})
|
||||
t.after(ui.close)
|
||||
await ui.run('openPreview({ id: "scan", name: "scan.pdf" })')
|
||||
await settle()
|
||||
assert.equal(ui.run('suggestionError.value'), 'KI nicht verfügbar')
|
||||
ui.run('filingType.value = "other"')
|
||||
assert.equal(ui.run('canFile.value'), true)
|
||||
ui.run('filingFolders.value[1].standardFiletypeIsOptional = true; suggestion.value = { folderId: "folder", typeId: "other" }; applySuggestion()')
|
||||
await settle()
|
||||
assert.equal(ui.run('filingType.value'), 'other')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user