import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { buildSelectedFileCards } from '../src/composables/workbenchAiMode/workbenchAiComposerModel.js' function readSource(path) { return readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8') } const aiModeComponent = readSource('../src/components/business/PersonalWorkbenchAiMode.vue') const aiModeTemplate = readSource('../src/components/business/PersonalWorkbenchAiMode.template.html') const aiModeStyles = readSource('../src/assets/styles/components/personal-workbench-ai-mode.css') const composerComponent = readSource('../src/components/business/workbench-ai/WorkbenchAiComposer.vue') const fileStripComponent = readSource('../src/components/business/workbench-ai/WorkbenchAiFileStrip.vue') const filePreviewComponent = readSource('../src/components/business/workbench-ai/WorkbenchAiFilePreviewDialog.vue') const filePreviewStyles = readSource('../src/assets/styles/components/workbench-ai-file-preview-dialog.css') const aiModeRuntime = readSource('../src/composables/workbenchAiMode/usePersonalWorkbenchAiMode.js') const filePreviewRuntime = readSource('../src/composables/workbenchAiMode/useWorkbenchAiFilePreview.js') function countOccurrences(source, pattern) { return source.match(pattern)?.length || 0 } test('personal workbench AI mode reuses shared composer and file strip components', () => { assert.match(aiModeComponent, /import \{ proxyRefs \} from 'vue'/) assert.match(aiModeComponent, /import WorkbenchAiComposer from '\.\/workbench-ai\/WorkbenchAiComposer\.vue'/) assert.match(aiModeComponent, /import WorkbenchAiFileStrip from '\.\/workbench-ai\/WorkbenchAiFileStrip\.vue'/) assert.match(aiModeComponent, /const aiModeRuntime = usePersonalWorkbenchAiMode\(props, emit\)/) assert.match(aiModeComponent, /const workbenchAiRuntime = proxyRefs\(aiModeRuntime\)/) assert.equal(countOccurrences(aiModeTemplate, / { assert.match(composerComponent, /:ref="runtime\.setAssistantInputRef"/) assert.match(aiModeRuntime, /function setAssistantInputRef\(element\)/) assert.match(aiModeRuntime, /assistantInputRef\.value = element/) assert.match(aiModeRuntime, /setAssistantInputRef,/) }) test('shared workbench file strip preserves OCR status badges', () => { assert.match(fileStripComponent, /file\.ocrState\?\.label/) assert.match(fileStripComponent, /class="workbench-ai-file-card__ocr"/) assert.match(fileStripComponent, /file\.ocrState\.status === 'recognizing'/) assert.match(fileStripComponent, /mdi mdi-text-recognition/) assert.match(fileStripComponent, /:title="file\.ocrState\.title \|\| file\.ocrState\.label"/) }) test('shared workbench file strip uses recognized image preview metadata over raw PDF type', () => { const [card] = buildSelectedFileCards([ { name: '2月23 上海-武汉.pdf', type: 'application/pdf', size: 24940, lastModified: 1760000000000 } ], () => ({ status: 'recognized', document: { preview_kind: 'image', receipt_preview_url: '/receipt-folder/receipt-train-1/preview' } })) assert.equal(card.tone, 'image') assert.equal(card.icon, 'mdi mdi-file-image-outline') assert.deepEqual(card.previewAsset, { kind: 'image', url: '/receipt-folder/receipt-train-1/preview', source: 'receipt' }) }) test('AI mode primes attachment OCR synchronously after file selection', () => { assert.match( filePreviewRuntime, /watch\(\s*selectedFiles,\s*\(files(?:,\s*previousFiles\s*=\s*\[\])?\)\s*=>\s*\{[\s\S]*attachmentFlow\.primeAiModeReceiptContext\(files\)[\s\S]*\},\s*\{\s*flush:\s*'sync'\s*\}\s*\)/ ) }) test('AI mode keeps conversation anchored above selected attachments', () => { assert.match( filePreviewRuntime, /watch\(\s*selectedFiles,\s*\(files,\s*previousFiles\s*=\s*\[\]\)\s*=>\s*\{[\s\S]*const fileCountChanged = files\.length !== previousFiles\.length[\s\S]*scrollInlineConversationToBottom\(\{\s*force:\s*true\s*\}\)[\s\S]*\},\s*\{\s*flush:\s*'sync'\s*\}\s*\)/ ) assert.match(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*gap:\s*14px;/) assert.match(aiModeStyles, /\.workbench-ai-thread\s*\{[\s\S]*scroll-padding-bottom:\s*42px;/) }) test('AI mode lays selected attachments in a horizontal scroll strip', () => { assert.match(aiModeStyles, /\.workbench-ai-file-strip\s*\{[\s\S]*flex-wrap:\s*nowrap;[\s\S]*overflow-x:\s*auto;[\s\S]*overflow-y:\s*hidden;/) assert.match(aiModeStyles, /\.workbench-ai-file-card\s*\{[\s\S]*flex:\s*0 0 312px;/) assert.match(fileStripComponent, /role="button"/) assert.match(fileStripComponent, /@click="runtime\.openAiModeFilePreview\(file\.key\)"/) assert.match(fileStripComponent, /@click\.stop="runtime\.removeAiModeFile\(file\.key\)"/) }) test('AI mode attachment preview opens a split source and recognition dialog', () => { assert.match(aiModeComponent, /import WorkbenchAiFilePreviewDialog from '\.\/workbench-ai\/WorkbenchAiFilePreviewDialog\.vue'/) assert.match(aiModeTemplate, //) assert.match(aiModeRuntime, /useWorkbenchAiFilePreview\(/) assert.match(aiModeRuntime, /\.\.\.filePreview,/) assert.match(filePreviewRuntime, /URL\.createObjectURL\(rawFile\)/) assert.match(filePreviewRuntime, /attachmentFlow\.resolveAiModeReceiptRecognitionState\(rawFile\)/) assert.match(filePreviewComponent, /class="workbench-ai-file-preview-source"/) assert.match(filePreviewComponent, /class="workbench-ai-file-preview-insight"/) assert.match(filePreviewComponent, / { assert.match(filePreviewStyles, /--workbench-ai-preview-sidebar-offset:\s*var\(--sidebar-expanded-width,\s*304px\);/) assert.match( filePreviewStyles, /\.workbench-ai-file-preview-mask\s*\{[\s\S]*grid-template-columns:\s*var\(--workbench-ai-preview-sidebar-offset\) minmax\(0,\s*1fr\);/ ) assert.match( filePreviewStyles, /\.workbench-ai-file-preview-dialog\s*\{[\s\S]*grid-column:\s*2;[\s\S]*justify-self:\s*center;/ ) assert.match( filePreviewStyles, /@media \(max-width:\s*900px\)\s*\{[\s\S]*--workbench-ai-preview-sidebar-offset:\s*0px;[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\);/ ) })