feat(web): 票据夹资产缓存接入与 AI 工作台附件流程完善
- ReceiptFolderView 删除票据后提示已关联附件副本保留,接入 useToast;fetchReceiptFolderAsset 加 no-store 避免预览缓存 - PersonalWorkbenchAiMode 附件区/对话气泡适配资产缓存,personal-workbench-ai-mode.css 调整布局 - usePersonalWorkbenchAiMode/useWorkbenchAiApplicationPreviewFlow/useWorkbenchAiAttachmentAssociationFlow/useWorkbenchAiStewardFlow 完善附件草稿选择与关联流程 - travelRequestDetailSmartEntryRecognition 智能识别增强,AppShellRouteView/PersonalWorkbenchView/useApplicationPreviewEditor/useTravelReimbursementSubmitComposer 等配套适配 - 新增 expense-attachment-draft-selection、receipt-folder-asset-cache、travel-request-detail-smart-entry-recognition 测试,更新 attachment-association-confirmation、expense-application-fast-preview、workbench-ai-mode-switch 测试
This commit is contained in:
@@ -263,6 +263,36 @@ test('OCR documents keep full recognized text for backend context', () => {
|
||||
assert.match(documents[0].text, /电子客票号:E1234567890/)
|
||||
})
|
||||
|
||||
test('OCR documents normalize receipt-folder field shapes for AI cards', () => {
|
||||
const documents = normalizeOcrDocuments({
|
||||
documents: [
|
||||
{
|
||||
filename: 'train-ticket.png',
|
||||
document_info: {
|
||||
fields: [
|
||||
{ label: '身份证号', value: '4201061987****1615' },
|
||||
{ key: 'seat_no', label: '座位号', value: '01B' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
filename: 'hotel.png',
|
||||
fields: [
|
||||
{ name: 'amount', label: '金额', value: '450元' }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.deepEqual(documents[0].document_fields, [
|
||||
{ key: '身份证号', label: '身份证号', value: '4201061987****1615' },
|
||||
{ key: 'seat_no', label: '座位号', value: '01B' }
|
||||
])
|
||||
assert.deepEqual(documents[1].document_fields, [
|
||||
{ key: 'amount', label: '金额', value: '450元' }
|
||||
])
|
||||
})
|
||||
|
||||
test('receipt files are collected through a single OCR persistence entry before draft association', async () => {
|
||||
const files = [
|
||||
{ name: 'invoice.png' }
|
||||
|
||||
@@ -1902,6 +1902,80 @@ test('application preview editor can edit return date from inline table input',
|
||||
assert.equal(message.applicationPreview.fields.days, '5\u5929')
|
||||
})
|
||||
|
||||
test('application preview editor opens date fields with native date input values', () => {
|
||||
const preview = normalizeApplicationPreview({
|
||||
fields: {
|
||||
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
||||
time: '2026-02-20 \u81f3 2026-02-23',
|
||||
location: '\u4e0a\u6d77',
|
||||
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
||||
days: '4\u5929'
|
||||
}
|
||||
})
|
||||
const message = {
|
||||
id: 'application-preview-editor-native-date-message',
|
||||
applicationPreview: preview,
|
||||
text: ''
|
||||
}
|
||||
const editor = useApplicationPreviewEditor({
|
||||
persistSessionState: () => {},
|
||||
toast: () => {}
|
||||
})
|
||||
|
||||
editor.openApplicationPreviewEditor(message, 'time', message.applicationPreview.fields.time)
|
||||
assert.equal(editor.resolveApplicationPreviewEditorControl('time'), 'date')
|
||||
assert.equal(editor.applicationPreviewEditor.value.draftValue, '2026-02-20')
|
||||
assert.equal(editor.resolveApplicationPreviewEditorDateMax(message, 'time'), '2026-02-23')
|
||||
|
||||
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
||||
assert.equal(editor.resolveApplicationPreviewEditorControl('time_return'), 'date')
|
||||
assert.equal(editor.applicationPreviewEditor.value.draftValue, '2026-02-23')
|
||||
assert.equal(editor.resolveApplicationPreviewEditorDateMin(message, 'time_return'), '2026-02-20')
|
||||
})
|
||||
|
||||
test('application preview editor blocks invalid date ranges', async () => {
|
||||
const preview = normalizeApplicationPreview({
|
||||
fields: {
|
||||
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
||||
time: '2026-02-20 \u81f3 2026-02-23',
|
||||
location: '\u4e0a\u6d77',
|
||||
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
||||
days: '4\u5929',
|
||||
transportMode: '\u706b\u8f66',
|
||||
amount: ''
|
||||
}
|
||||
})
|
||||
const message = {
|
||||
id: 'application-preview-editor-invalid-date-message',
|
||||
applicationPreview: preview,
|
||||
text: ''
|
||||
}
|
||||
const toastMessages = []
|
||||
const editor = useApplicationPreviewEditor({
|
||||
persistSessionState: () => {},
|
||||
toast: (messageText) => {
|
||||
toastMessages.push(messageText)
|
||||
}
|
||||
})
|
||||
|
||||
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
||||
editor.applicationPreviewEditor.value.draftValue = '2026-02-19'
|
||||
const returnCommitted = await editor.commitApplicationPreviewEditor(message)
|
||||
|
||||
assert.equal(returnCommitted, false)
|
||||
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-23')
|
||||
assert.equal(message.applicationPreview.fields.days, '4\u5929')
|
||||
assert.equal(toastMessages.at(-1), '\u51fa\u53d1\u65f6\u95f4\u4e0d\u80fd\u665a\u4e8e\u8fd4\u56de\u65f6\u95f4\uff0c\u8bf7\u91cd\u65b0\u9009\u62e9\u3002')
|
||||
|
||||
editor.openApplicationPreviewEditor(message, 'time', message.applicationPreview.fields.time)
|
||||
editor.applicationPreviewEditor.value.draftValue = '2026-02-24'
|
||||
const startCommitted = await editor.commitApplicationPreviewEditor(message)
|
||||
|
||||
assert.equal(startCommitted, false)
|
||||
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-23')
|
||||
assert.equal(toastMessages.at(-1), '\u51fa\u53d1\u65f6\u95f4\u4e0d\u80fd\u665a\u4e8e\u8fd4\u56de\u65f6\u95f4\uff0c\u8bf7\u91cd\u65b0\u9009\u62e9\u3002')
|
||||
})
|
||||
|
||||
test('application preview editor estimates after shorthand return date input', async () => {
|
||||
const preview = normalizeApplicationPreview({
|
||||
fields: {
|
||||
|
||||
90
web/tests/expense-attachment-draft-selection.test.mjs
Normal file
90
web/tests/expense-attachment-draft-selection.test.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { useTravelReimbursementCreateViewControls } from '../src/views/scripts/useTravelReimbursementCreateViewControls.js'
|
||||
|
||||
function ref(value) {
|
||||
return { value }
|
||||
}
|
||||
|
||||
const submitComposerScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('选择候选草稿时直接确认归集并带入附件原件', async () => {
|
||||
const submitCalls = []
|
||||
const attachedFiles = [{ name: '2月20 武汉-上海.pdf' }]
|
||||
const message = {
|
||||
queryPayload: {
|
||||
selectionMode: 'draft_association'
|
||||
}
|
||||
}
|
||||
|
||||
const controls = useTravelReimbursementCreateViewControls({
|
||||
activeSessionType: ref('expense'),
|
||||
attachedFiles: ref(attachedFiles),
|
||||
clearAssistantSessionSnapshot: () => {},
|
||||
closeAfterBusy: ref(false),
|
||||
conversationId: ref('conversation-1'),
|
||||
deleteConversation: async () => {},
|
||||
deleteSessionBusy: ref(false),
|
||||
deleteSessionDialogOpen: ref(false),
|
||||
draftClaimId: ref(''),
|
||||
emitClose: () => {},
|
||||
getExpenseQueryActivePage: () => 1,
|
||||
getExpenseQueryTotalPages: () => 1,
|
||||
persistSessionState: () => {},
|
||||
resetCurrentSessionState: () => {},
|
||||
reviewActionBusy: ref(false),
|
||||
router: { push: () => {} },
|
||||
resolveCurrentUserId: () => 'user-1',
|
||||
sessionSwitchBusy: ref(false),
|
||||
submitComposer: async (options) => {
|
||||
submitCalls.push(options)
|
||||
return { ok: true }
|
||||
},
|
||||
submitting: ref(false),
|
||||
toast: () => {},
|
||||
workbenchVisible: ref(true)
|
||||
})
|
||||
|
||||
await controls.handleExpenseQueryRecordClick(message, {
|
||||
claimId: 'claim-1',
|
||||
claimNo: 'R74CB7C2R'
|
||||
})
|
||||
|
||||
assert.equal(submitCalls.length, 1)
|
||||
assert.equal(submitCalls[0].associationConfirmed, true)
|
||||
assert.equal(submitCalls[0].skipDraftAssociationPrompt, true)
|
||||
assert.equal(submitCalls[0].uploadDisposition, 'continue_existing')
|
||||
assert.deepEqual(submitCalls[0].files, attachedFiles)
|
||||
assert.equal(submitCalls[0].files[0], attachedFiles[0])
|
||||
assert.equal(submitCalls[0].extraContext.review_action, 'link_to_existing_draft')
|
||||
assert.equal(submitCalls[0].extraContext.attachment_association_confirmed, true)
|
||||
assert.equal(submitCalls[0].extraContext.draft_claim_id, 'claim-1')
|
||||
assert.equal(message.queryPayload.selectionLocked, true)
|
||||
assert.equal(message.queryPayload.selectedClaimId, 'claim-1')
|
||||
})
|
||||
|
||||
test('确认归集到现有草稿时先同步附件再渲染最终结果', () => {
|
||||
assert.match(
|
||||
submitComposerScript,
|
||||
/let attachmentSyncCompleted = false/
|
||||
)
|
||||
assert.match(
|
||||
submitComposerScript,
|
||||
/if \(\s*reviewActionResult === 'link_to_existing_draft'[\s\S]*await persistComposerFilesToDraft\(\)[\s\S]*attachmentSyncCompleted = true[\s\S]*\}/
|
||||
)
|
||||
assert.ok(
|
||||
submitComposerScript.indexOf('await persistComposerFilesToDraft()') <
|
||||
submitComposerScript.indexOf('const assistantMessage = createMessage('),
|
||||
'附件同步应先于最终助手消息,避免详情页先展示空明细和旧风险'
|
||||
)
|
||||
assert.match(
|
||||
submitComposerScript,
|
||||
/if \(!attachmentSyncCompleted\) \{\s*const persistTask = persistComposerFilesToDraft\(\)/
|
||||
)
|
||||
})
|
||||
14
web/tests/receipt-folder-asset-cache.test.mjs
Normal file
14
web/tests/receipt-folder-asset-cache.test.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
test('receipt folder asset fetch bypasses stale preview cache', () => {
|
||||
const service = readFileSync(join(root, 'web/src/services/receiptFolder.js'), 'utf8')
|
||||
|
||||
assert.match(service, /export function fetchReceiptFolderAsset/)
|
||||
assert.match(service, /cache: 'no-store'/)
|
||||
assert.match(service, /responseType: 'blob'/)
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
resolveCreatedSmartEntryRecognitionItem
|
||||
} from '../src/views/scripts/travelRequestDetailSmartEntryRecognition.js'
|
||||
|
||||
test('智能录入创建明细后跳过系统补贴行', () => {
|
||||
const createdItem = resolveCreatedSmartEntryRecognitionItem([
|
||||
{
|
||||
id: 'allowance-item',
|
||||
item_type: 'travel_allowance',
|
||||
invoice_id: ''
|
||||
},
|
||||
{
|
||||
id: 'business-item',
|
||||
item_type: 'travel',
|
||||
invoice_id: ''
|
||||
}
|
||||
], new Set())
|
||||
|
||||
assert.equal(createdItem?.id, 'business-item')
|
||||
})
|
||||
|
||||
test('智能录入创建明细后没有可上传业务行时返回空', () => {
|
||||
const createdItem = resolveCreatedSmartEntryRecognitionItem([
|
||||
{
|
||||
id: 'allowance-item',
|
||||
item_type: 'travel_allowance',
|
||||
invoice_id: ''
|
||||
}
|
||||
], new Set())
|
||||
|
||||
assert.equal(createdItem, null)
|
||||
})
|
||||
@@ -229,12 +229,17 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiModeSurface, /rows="3"/)
|
||||
assert.match(aiModeSurface, /workbench-ai-composer-toolbar/)
|
||||
assert.match(aiModeSurface, /<article v-for="file in selectedFileCards"[\s\S]*class="workbench-ai-file-card"/)
|
||||
assert.match(aiModeSurface, /class="workbench-ai-file-card__ocr"/)
|
||||
assert.match(aiModeSurface, /file\.ocrState\?\.label/)
|
||||
assert.match(aiModeSurface, /mdi mdi-text-recognition/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-file-card__ocr/)
|
||||
assert.match(aiModeStyles, /workbenchAiOcrSpin/)
|
||||
assert.match(aiModeSurface, /:aria-label="`移除附件 \$\{file\.name\}`"/)
|
||||
assert.match(aiModeSurface, /function removeAiModeFile\(fileKey\)/)
|
||||
assert.match(aiModeSurface, /const selectedFileCards = computed/)
|
||||
assert.match(aiModeSurface, /resolveAiComposerFileType\(file\)/)
|
||||
assert.match(aiModeSurface, /AI_COMPOSER_FILE_TYPE_META = \{[\s\S]*pdf:\s*\{ label:\s*'PDF'/)
|
||||
assert.match(aiModeSurface, /import \{ collectReceiptFiles \} from '\.\.\/\.\.\/views\/scripts\/travelReimbursementAttachmentModel\.js'/)
|
||||
assert.match(aiModeSurface, /buildFileIdentity,[\s\S]*collectReceiptFiles[\s\S]*travelReimbursementAttachmentModel\.js/)
|
||||
assert.match(aiModeSurface, /MAX_ATTACHMENTS,[\s\S]*mergeFilesWithLimit[\s\S]*travelReimbursementAttachmentModel\.js/)
|
||||
assert.match(aiModeSurface, /import \* as aiAttachmentAssociationModel from '\.\.\/\.\.\/utils\/aiAttachmentAssociationModel\.js'/)
|
||||
assert.match(aiModeSurface, /aiAttachmentAssociationModel\.resolveAiAttachmentAssociationMatch/)
|
||||
@@ -261,7 +266,7 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiModeSurface, /function findAiAttachmentAssociationRuntime\(options = \{\}\)/)
|
||||
assert.match(aiModeSurface, /resolveAiAttachmentAssociationClaimNo\(actionPayload\)/)
|
||||
assert.match(aiModeSurface, /if \(actionType === AI_ATTACHMENT_OCR_DETAIL_ACTION\)/)
|
||||
assert.match(aiModeSurface, /collectReceiptFiles\(\{[\s\S]*files,[\s\S]*recognizeOcrFiles[\s\S]*\}\)/)
|
||||
assert.match(aiModeSurface, /const collected = await collectAiModeReceiptContext\(files\)/)
|
||||
assert.match(aiModeSurface, /const claims = extractExpenseClaimItems\(claimsPayload\)/)
|
||||
assert.match(aiModeSurface, /aiAttachmentAssociationModel\.resolveAiAttachmentAssociationMatch\(claims, collected\.ocrDocuments\)/)
|
||||
assert.match(aiModeSurface, /aiAttachmentAssociationRuntime\.set\(associationId/)
|
||||
@@ -278,6 +283,14 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiModeSurface, /mdi mdi-calendar-range/)
|
||||
assert.match(aiModeSurface, /workbench-ai-date-popover/)
|
||||
assert.match(aiModeSurface, /type="date"/)
|
||||
assert.match(aiModeSurface, /:min="resolveInlineApplicationPreviewEditorDateMin\(message, row\.key\)"/)
|
||||
assert.match(aiModeSurface, /:max="resolveInlineApplicationPreviewEditorDateMax\(message, row\.key\)"/)
|
||||
assert.match(aiModeSurface, /resolveInlineApplicationPreviewEditorControl\(row\.key\) === 'date'/)
|
||||
assert.match(aiModeSurface, /class="\['application-preview-input', 'application-preview-date-input', `application-preview-input--\$\{row\.key\}`\]"/)
|
||||
assert.match(aiModeSurface, /function resolveInlineApplicationPreviewEditorControl\(fieldKey\) \{[\s\S]*return resolveApplicationPreviewEditorControl\(fieldKey\)/)
|
||||
assert.match(aiModeSurface, /function resolveInlineApplicationPreviewEditorDateMin\(message, fieldKey\) \{[\s\S]*return resolveApplicationPreviewEditorDateMin\?\.\(message, fieldKey\) \|\| ''/)
|
||||
assert.match(aiModeSurface, /function resolveInlineApplicationPreviewEditorDateMax\(message, fieldKey\) \{[\s\S]*return resolveApplicationPreviewEditorDateMax\?\.\(message, fieldKey\) \|\| ''/)
|
||||
assert.doesNotMatch(aiModeSurface, /return control === 'date' \? 'text' : control/)
|
||||
assert.doesNotMatch(aiModeSurface, /mdi mdi-web/)
|
||||
assert.match(aiModeSurface, /mdi mdi-microphone-outline/)
|
||||
assert.match(aiModeSurface, /mdi mdi-arrow-up/)
|
||||
@@ -342,6 +355,9 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-action-link\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-image-frame\)/)
|
||||
assert.match(aiModeStyles, /\.application-preview-date-input\s*\{[\s\S]*width:\s*min\(100%,\s*188px\);/)
|
||||
assert.match(aiModeStyles, /\.application-preview-input--location\s*\{[\s\S]*width:\s*min\(100%,\s*220px\);/)
|
||||
assert.match(aiModeStyles, /\.application-preview-input--reason\s*\{[\s\S]*width:\s*min\(100%,\s*680px\);/)
|
||||
assert.match(aiModeSurface, /import \{ fetchSettings \} from '\.\.\/\.\.\/services\/settings\.js'/)
|
||||
assert.match(aiModeSurface, /fetchStewardPlan,[\s\S]*fetchStewardPlanStream[\s\S]*services\/steward\.js'/)
|
||||
assert.match(aiModeSurface, /import \{ useWorkbenchComposerDate \} from '\.\.\/useWorkbenchComposerDate\.js'/)
|
||||
@@ -354,7 +370,7 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiModeSurface, /buildStewardPlanRequest/)
|
||||
assert.match(aiModeSurface, /buildStewardPlanMessageText/)
|
||||
assert.match(aiModeSurface, /buildStewardSuggestedActions/)
|
||||
assert.match(aiModeSurface, /const emit = defineEmits\(\['conversation-change', 'conversation-history-change', 'open-document'\]\)/)
|
||||
assert.match(aiModeSurface, /const emit = defineEmits\(\['conversation-change', 'conversation-history-change', 'open-document', 'request-updated'\]\)/)
|
||||
assert.match(aiModeSurface, /function startInlineConversation\(prompt, entry = \{\}, files = \[\]\)/)
|
||||
assert.match(aiModeSurface, /activateInlineConversation\(\{[\s\S]*title:[\s\S]*\}\)[\s\S]*conversationMessages\.value\.push\(createInlineMessage\('user'/)
|
||||
assert.match(aiModeSurface, /persistCurrentConversation\(\)/)
|
||||
@@ -370,6 +386,13 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.match(aiModeSurface, /if \(!hasServerStreamedContent\) \{[\s\S]*await streamInlineAssistantContent\(pendingMessage\.id, finalMessageText\)[\s\S]*\}/)
|
||||
assert.match(aiModeSurface, /if \(actionType === AI_APPLICATION_ACTION_SUBMIT\) \{[\s\S]*buildInlineApplicationResultTable\(draftPayload/)
|
||||
assert.match(aiModeSurface, /需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。/)
|
||||
assert.match(aiModeSurface, /function buildInlineApplicationActionFailureText\(error, isSubmit\)/)
|
||||
assert.match(aiModeSurface, /我已保留当前申请核对表/)
|
||||
assert.match(aiModeSurface, /applicationPreview:\s*targetMessage\.applicationPreview/)
|
||||
assert.match(
|
||||
aiModeSurface,
|
||||
/suggestedActions:\s*buildInlineApplicationPreviewSuggestedActions\([\s\S]*targetMessage\.applicationPreview/
|
||||
)
|
||||
assert.doesNotMatch(aiModeSurface, /\*\*申请单号:\*\*/)
|
||||
assert.doesNotMatch(aiModeSurface, /createInlineMessage\('assistant', buildStewardPlanMessageText\(plan\)/)
|
||||
assert.doesNotMatch(aiModeSurface, /runOrchestrator\(/)
|
||||
@@ -489,14 +512,51 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.ok(pngPresentation.minimumForegroundHeightRatio > 0.9)
|
||||
})
|
||||
|
||||
test('AI attachment association notifies shell to refresh the target detail page', () => {
|
||||
const aiModeComponent = readSource('../src/components/business/PersonalWorkbenchAiMode.vue')
|
||||
const workbenchView = readSource('../src/views/PersonalWorkbenchView.vue')
|
||||
const appShellRouteView = readSource('../src/views/AppShellRouteView.vue')
|
||||
const aiModeComposable = readSource('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js')
|
||||
const attachmentFlow = readSource('../src/composables/workbenchAiMode/useWorkbenchAiAttachmentAssociationFlow.js')
|
||||
|
||||
assert.match(aiModeComponent, /defineEmits\(\[[^\]]*'request-updated'/)
|
||||
assert.match(workbenchView, /@request-updated="emit\('request-updated', \$event\)"/)
|
||||
assert.match(workbenchView, /defineEmits\(\[[^\]]*'request-updated'/)
|
||||
assert.match(appShellRouteView, /<PersonalWorkbenchView[\s\S]*@request-updated="handleRequestUpdated"/)
|
||||
assert.match(
|
||||
aiModeComposable,
|
||||
/notifyRequestUpdated:\s*\(payload\)\s*=>\s*emit\('request-updated', payload\)/
|
||||
)
|
||||
assert.match(
|
||||
attachmentFlow,
|
||||
/notifyRequestUpdated\?\.\(\{[\s\S]*claimId:[\s\S]*runtime\.claimId[\s\S]*uploadedCount:[\s\S]*syncResult\?\.uploadedCount/
|
||||
)
|
||||
})
|
||||
|
||||
test('AI mode normal assistant requests include OCR context for uploaded receipts', () => {
|
||||
assert.match(aiModeSurface, /function isLikelyAiModeOcrFile\(file = \{\}\)/)
|
||||
assert.match(aiModeSurface, /const aiModeReceiptContextCache = new Map\(\)/)
|
||||
assert.match(aiModeSurface, /const aiModeReceiptRecognitionState = reactive\(\{\}\)/)
|
||||
assert.match(aiModeSurface, /function resolveAiModeReceiptRecognitionState\(file\)/)
|
||||
assert.match(aiModeSurface, /resolveAiModeReceiptRecognitionState\(selectedFiles\.value\[index\]\)/)
|
||||
assert.match(aiModeSurface, /status:\s*'recognizing'[\s\S]*label:\s*'智能录入识别中'/)
|
||||
assert.match(aiModeSurface, /status:\s*'recognized'[\s\S]*label:\s*detail \? `已识别票据/)
|
||||
assert.match(aiModeSurface, /status:\s*'failed'[\s\S]*label:\s*'识别失败'/)
|
||||
assert.match(aiModeSurface, /function primeAiModeReceiptContext\(files = \[\]\)/)
|
||||
assert.match(aiModeSurface, /function startAiModeReceiptRecognition\(files = \[\]\)/)
|
||||
assert.match(aiModeSurface, /function buildAiModeReceiptContextCacheKey\(ocrFiles = \[\]\)/)
|
||||
assert.match(aiModeSurface, /applyAiModeReceiptRecognitionResult\(ocrFiles, context\)/)
|
||||
assert.match(aiModeSurface, /buildFileIdentity\(file\)/)
|
||||
assert.match(aiModeSurface, /watch\(selectedFiles, \(files\) => \{[\s\S]*attachmentFlow\.primeAiModeReceiptContext\(files\)/)
|
||||
assert.match(aiModeSurface, /async function collectAiModeReceiptContext\(files = \[\]\)/)
|
||||
assert.match(aiModeSurface, /cached\?\.status === 'pending'[\s\S]*await cached\.promise/)
|
||||
assert.match(aiModeSurface, /collectReceiptFiles\(\{[\s\S]*files:\s*ocrFiles,[\s\S]*recognizeOcrFiles[\s\S]*\}\)/)
|
||||
assert.match(aiModeSurface, /const receiptContext = await collectAiModeReceiptContext\(files\)/)
|
||||
assert.match(aiModeSurface, /const attachmentOcrDetails = buildInlineAttachmentOcrDetails\(receiptContext, files\)/)
|
||||
assert.match(aiModeSurface, /ocr_summary:\s*receiptContext\.ocrSummary/)
|
||||
assert.match(aiModeSurface, /ocr_documents:\s*receiptContext\.ocrDocuments/)
|
||||
assert.match(aiModeSurface, /attachment_names:\s*receiptContext\.attachmentNames/)
|
||||
assert.match(aiModeSurface, /attachment_count:\s*receiptContext\.attachmentCount/)
|
||||
assert.match(aiModeSurface, /ocr_source_file_names:\s*receiptContext\.ocrSourceFileNames/)
|
||||
assert.match(aiModeSurface, /attachmentOcrDetails/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user