import assert from 'node:assert/strict' import { execFileSync } from 'node:child_process' import { readdirSync, readFileSync, statSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' function readSource(path) { try { return readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8') } catch { return '' } } function readRuleBody(source, selector) { const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const match = source.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\}`)) return match?.[1] || '' } function countGifFrameBlocks(buffer) { let count = 0 for (let index = 0; index < buffer.length - 2; index += 1) { if (buffer[index] === 0x21 && buffer[index + 1] === 0xf9 && buffer[index + 2] === 0x04) { count += 1 } } return count } function measureGifMotion(assetPath) { const script = ` from PIL import Image, ImageSequence import json import sys image = Image.open(sys.argv[1]) frames = [frame.convert("RGB").resize((64, 64)) for frame in ImageSequence.Iterator(image)] def delta(left, right): left_pixels = left.load() right_pixels = right.load() total = 0 for y in range(64): for x in range(64): a = left_pixels[x, y] b = right_pixels[x, y] total += abs(a[0] - b[0]) + abs(a[1] - b[1]) + abs(a[2] - b[2]) return total / (64 * 64 * 3) adjacent = [delta(frames[index], frames[index + 1]) for index in range(len(frames) - 1)] adjacent_sorted = sorted(adjacent) median = adjacent_sorted[len(adjacent_sorted) // 2] print(json.dumps({ "medianAdjacentDelta": median, "seamDelta": delta(frames[-1], frames[0]) })) ` return JSON.parse(execFileSync('python3', ['-', assetPath], { encoding: 'utf8', input: script }).trim()) } function measureGifDuration(assetPath) { const script = ` from PIL import Image import sys image = Image.open(sys.argv[1]) total = 0 for index in range(getattr(image, "n_frames", 1)): image.seek(index) total += image.info.get("duration", 0) print(total) ` return Number(execFileSync('python3', ['-', assetPath], { encoding: 'utf8', input: script }).trim()) } function measureOrbAssetPresentation(assetPath) { const script = ` from PIL import Image import json import sys image = Image.open(sys.argv[1]) frame_count = getattr(image, "n_frames", 1) width, height = image.size minimum_corner_luma = 255 maximum_corner_luma = 0 minimum_background_similarity_ratio = 1 minimum_foreground_width_ratio = 1 minimum_foreground_height_ratio = 1 for index in range(frame_count): if frame_count > 1: image.seek(index) rgb = image.convert("RGB") corners = [ rgb.getpixel((0, 0)), rgb.getpixel((width - 1, 0)), rgb.getpixel((0, height - 1)), rgb.getpixel((width - 1, height - 1)), ] corner_lumas = [sum(pixel) / 3 for pixel in corners] minimum_corner_luma = min(minimum_corner_luma, min(corner_lumas)) maximum_corner_luma = max(maximum_corner_luma, max(corner_lumas)) background = tuple(round(sum(pixel[channel] for pixel in corners) / len(corners)) for channel in range(3)) foreground_mask = Image.new("L", (width, height), 0) foreground_pixels = foreground_mask.load() background_similarity = 0 rgb_pixels = rgb.load() for y in range(height): for x in range(width): pixel = rgb_pixels[x, y] diff = sum(abs(pixel[channel] - background[channel]) for channel in range(3)) if diff > 22: foreground_pixels[x, y] = 255 if diff <= 12: background_similarity += 1 foreground_box = foreground_mask.getbbox() if foreground_box: minimum_foreground_width_ratio = min( minimum_foreground_width_ratio, (foreground_box[2] - foreground_box[0]) / width ) minimum_foreground_height_ratio = min( minimum_foreground_height_ratio, (foreground_box[3] - foreground_box[1]) / height ) minimum_background_similarity_ratio = min( minimum_background_similarity_ratio, background_similarity / (width * height) ) print(json.dumps({ "minimumCornerLuma": minimum_corner_luma, "maximumCornerLuma": maximum_corner_luma, "minimumBackgroundSimilarityRatio": minimum_background_similarity_ratio, "minimumForegroundWidthRatio": minimum_foreground_width_ratio, "minimumForegroundHeightRatio": minimum_foreground_height_ratio, "width": width, "height": height })) ` return JSON.parse(execFileSync('python3', ['-', assetPath], { encoding: 'utf8', input: script }).trim()) } const appShell = readSource('../src/views/AppShellRouteView.vue') const workbenchView = readSource('../src/views/PersonalWorkbenchView.vue') const aiMode = readSource('../src/components/business/PersonalWorkbenchAiMode.vue') const aiModeTemplate = readSource('../src/components/business/PersonalWorkbenchAiMode.template.html') const aiModeRuntimeDir = fileURLToPath(new URL('../src/composables/workbenchAiMode/', import.meta.url)) const aiModeRuntime = readdirSync(aiModeRuntimeDir) .filter((file) => file.endsWith('.js')) .sort() .map((file) => readFileSync(new URL(`../src/composables/workbenchAiMode/${file}`, import.meta.url), 'utf8')) .join('\n') const aiModeSurface = `${aiMode}\n${aiModeTemplate}\n${aiModeRuntime}` const aiModeStyles = readSource('../src/assets/styles/components/personal-workbench-ai-mode.css') const workbenchViewStyles = readSource('../src/assets/styles/views/personal-workbench-view.css') const appStyles = readSource('../src/assets/styles/app.css') const aiBackgroundRule = readRuleBody(aiModeStyles, '.workbench-ai-mode::after') const orbRule = readRuleBody(aiModeStyles, '.workbench-ai-orb') const orbImageRule = readRuleBody(aiModeStyles, '.workbench-ai-orb__image') const composerRule = readRuleBody(aiModeStyles, '.workbench-ai-composer') const composerTextareaRule = readRuleBody(aiModeStyles, '.workbench-ai-composer textarea') const fileStripRule = readRuleBody(aiModeStyles, '.workbench-ai-file-strip') const fileCardRule = readRuleBody(aiModeStyles, '.workbench-ai-file-card') const orbIconAsset = fileURLToPath( new URL('../src/assets/workbench-ai-mode-orb-icon.gif', import.meta.url) ) const orbIconPngAsset = fileURLToPath( new URL('../src/assets/workbench-ai-mode-orb-icon.png', import.meta.url) ) const orbIconBuffer = readFileSync(orbIconAsset) test('app shell owns the workbench mode and wires it through topbar and content', () => { assert.match(appShell, /function resolveDefaultWorkbenchMode\(user\)\s*\{[\s\S]*isPlatformAdminUser\(user\)[\s\S]*'traditional'[\s\S]*'ai'/) assert.match(appShell, /const workbenchMode = ref\(resolveDefaultWorkbenchMode\(currentUser\.value\)\)/) assert.doesNotMatch(appShell, /const workbenchMode = ref\('traditional'\)/) assert.match(appShell, /watch\(\s*\(\) => currentUser\.value,[\s\S]*resolveDefaultWorkbenchMode\(user\)/) assert.match(appShell, /function toggleWorkbenchMode\(\)/) assert.match(appShell, /const nextMode = workbenchMode\.value === 'ai' \? 'traditional' : 'ai'/) assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/) assert.match(appShell, /workbenchMode\.value = nextMode/) assert.match(appShell, /sidebarCollapsed\.value = sidebarCollapsedBeforeAiMode\.value/) assert.match(appShell, / workbenchMode\.value === 'ai'\)/) assert.match(appShell, /const isWorkbenchAiMode = computed\(\(\) => activeView\.value === 'workbench' && workbenchMode\.value === 'ai'\)/) assert.match(appShell, /'workbench-ai-sidebar-active': isAiShellMode/) assert.match(appShell, /'workbench-workarea-ai-mode': isWorkbenchAiMode/) assert.match(appStyles, /\.workarea\.workbench-workarea\.workbench-workarea-ai-mode\s*\{[\s\S]*padding:\s*0;[\s\S]*background:\s*transparent;/) }) test('personal workbench view swaps the traditional dashboard with the AI mode screen', () => { assert.match(workbenchView, /import PersonalWorkbenchAiMode from '\.\.\/components\/business\/PersonalWorkbenchAiMode\.vue'/) assert.match(workbenchView, / { assert.match(aiModeSurface, /personal-workbench-ai-mode\.css/) assert.doesNotMatch(aiModeSurface, /workbench-ai-mode-robot-bg\.png/) assert.match(aiModeSurface, /workbench-ai-mode-orb-icon\.gif/) assert.match(aiModeSurface, / isLikelyReceiptAssociationFile\(file\)\)/) assert.match(aiModeSurface, /!shouldKeepAiAttachmentInAssistantReply\(prompt\)/) assert.match(aiModeSurface, /const aiAttachmentAssociationRuntime = new Map\(\)/) 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 claims = extractExpenseClaimItems\(claimsPayload\)/) assert.match(aiModeSurface, /aiAttachmentAssociationModel\.resolveAiAttachmentAssociationMatch\(claims, collected\.ocrDocuments\)/) assert.match(aiModeSurface, /aiAttachmentAssociationRuntime\.set\(associationId/) assert.match(aiModeSurface, /attachmentOcrDetails,\s*[\s\S]*includeOcrDetails: Boolean\(attachmentOcrDetails\)/) assert.match(aiModeSurface, /async function confirmAiAttachmentAssociation\(actionPayload = \{\}, sourceMessage = null\)/) assert.match(aiModeSurface, /syncExpenseClaimFilesToDraft\(\{[\s\S]*fetchExpenseClaimDetail,[\s\S]*createExpenseClaimItem,[\s\S]*uploadExpenseClaimItemAttachment/) assert.match(aiModeSurface, /if \(actionType === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION\)/) assert.match(aiModeSurface, /if \(shouldRunAiAttachmentAutoAssociation\(entry, files, cleanPrompt\)\) \{[\s\S]*requestAiAttachmentAssociationReply\(cleanPrompt, entry, files\)/) assert.match(aiModeSurface, /const fileMergeResult = mergeFilesWithLimit\(selectedFiles\.value, Array\.from\(event\.target\.files \|\| \[\]\), MAX_ATTACHMENTS\)/) assert.match(aiModeSurface, /selectedFiles\.value = fileMergeResult\.files/) assert.doesNotMatch(aiModeSurface, /selectedFiles\.value = Array\.from\(event\.target\.files \|\| \[\]\)\.slice\(0, 10\)/) assert.doesNotMatch(aiModeSurface, /已选择 \{\{ selectedFiles\.length \}\} 份附件/) assert.match(aiModeSurface, /Axiom Ultra 3\.1/) assert.match(aiModeSurface, /mdi mdi-calendar-range/) assert.match(aiModeSurface, /workbench-ai-date-popover/) assert.match(aiModeSurface, /type="date"/) assert.doesNotMatch(aiModeSurface, /mdi mdi-web/) assert.match(aiModeSurface, /mdi mdi-microphone-outline/) assert.match(aiModeSurface, /mdi mdi-arrow-up/) assert.match(aiModeSurface, /快速开始/) assert.match(aiModeSurface, /action-icon-wrapper/) assert.match(aiModeSurface, /发起报销/) assert.match(aiModeSurface, /查询预算/) assert.match(aiModeSurface, /解释制度/) assert.match(aiModeSurface, /催办审批/) assert.match(aiModeSurface, //) assert.match(aiModeSurface, /@submit\.prevent="submitAiModePrompt"/) assert.equal((aiModeSurface.match(/type="submit"[\s\S]{0,160}class="workbench-ai-send-btn"/g) || []).length, 2) assert.match(aiModeSurface, /class="workbench-ai-conversation"/) assert.match(aiModeSurface, /class="workbench-ai-thread"[\s\S]*@scroll\.passive="handleInlineConversationScroll"/) assert.match(aiModeSurface, /workbench-ai-answer-card/) assert.match(aiModeSurface, /workbench-ai-answer-markdown/) assert.match(aiModeSurface, /v-html="renderInlineConversationHtml\(message\.content\)"/) assert.match(aiModeSurface, /workbench-ai-message-actions/) assert.match(aiModeSurface, /workbench-ai-conversation-actions/) assert.match(aiModeSurface, /scrollInlineConversationToTop/) assert.match(aiModeSurface, /requestDeleteCurrentConversation/) assert.match(aiModeSurface, /confirmDeleteConversation/) assert.match(aiModeSurface, /workbench-ai-confirm-dialog/) assert.match(aiModeSurface, /workbench-ai-thinking-toggle/) assert.match(aiModeSurface, /小财业务思考/) assert.match(aiModeSurface, /class="workbench-ai-thinking-expanded"/) assert.match(aiModeSurface, /class="workbench-ai-thinking-collapse-btn"/) assert.match(aiModeSurface, /class="workbench-ai-thinking-collapse-btn"[\s\S]*@click="toggleInlineThinking\(message\)"/) assert.doesNotMatch(aiModeSurface, /:disabled="message\.pending"/) assert.match(aiModeSurface, /isInlineThinkingExpanded/) assert.match(aiModeSurface, /toggleInlineThinking/) assert.match(aiModeSurface, /const thinkingCollapsedMessageIds = ref\(new Set\(\)\)/) assert.match(aiModeSurface, /thinkingCollapsedMessageIds\.value\.has\(message\.id\)/) assert.match(aiModeSurface, /nextCollapsedIds\.add\(message\.id\)/) assert.match(aiModeSurface, /nextCollapsedIds\.delete\(message\.id\)/) assert.match(aiModeSurface, /message\.pending && !hasInlineThinking\(message\)/) assert.doesNotMatch(aiModeSurface, /小财管家正在思考/) assert.doesNotMatch(aiModeSurface, /思考过程/) assert.doesNotMatch(aiModeSurface, /message\.pending \?/) assert.match(aiModeSurface, /继续和小财管家对话\.\.\./) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary__scope\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card-list\) \{[\s\S]*gap:\s*16px;/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\) \{[\s\S]*url\("\.\.\/\.\.\/ai-document-card-bg\.png"\);/) assert.doesNotMatch(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)::before/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\) \{[\s\S]*background: var\(--ai-document-card-head-bg\);/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\.is-success \.ai-document-card__head\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__body\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__summary\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field--wide\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__label\)/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__amount\)/) assert.match( aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\) \{[\s\S]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/ ) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field--action \.ai-document-card__action\)/) 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(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'/) assert.match(aiModeSurface, /loadAiWorkbenchConversationHistory/) assert.match(aiModeSurface, /saveAiWorkbenchConversation/) assert.match(aiModeSurface, /deleteAiWorkbenchConversation/) assert.match(aiModeSurface, /import \{ renderAiConversationHtml \} from '\.\.\/\.\.\/utils\/aiConversationHtmlRenderer\.js'/) assert.match(aiModeSurface, /function renderInlineConversationHtml\(content\) \{[\s\S]*return renderAiConversationHtml\(content\)[\s\S]*\}/) assert.doesNotMatch(aiModeSurface, /import \{ renderMarkdown \} from '\.\.\/\.\.\/utils\/markdown\.js'/) 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, /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\(\)/) assert.match(aiModeSurface, /refreshConversationHistory\(\)/) assert.match(aiModeSurface, /fetchStewardPlanStream\(/) assert.match(aiModeSurface, /fetchStewardPlan\(/) assert.match(aiModeSurface, /const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6/) assert.match(aiModeSurface, /function updateInlineMessageContent\(message, content\)/) assert.match(aiModeSurface, /async function streamInlineAssistantContent\(messageId, content\)/) assert.match(aiModeSurface, /const requiredApplicationContinuationFlow = resolveRequiredApplicationGateContinuationFlow\(normalizedPlan\)/) assert.match(aiModeSurface, /const finalMessageText = requiredApplicationContinuationFlow[\s\S]*buildAiRequiredApplicationGateAutoMessage\(normalizedPlan, requiredApplicationContinuationFlow\)[\s\S]*buildStewardPlanMessageText\(plan\)/) assert.match(aiModeSurface, /const hasServerStreamedContent = Boolean\(String\(pendingMessage\.content \|\| ''\)\.trim\(\)\)/) 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.doesNotMatch(aiModeSurface, /\*\*申请单号:\*\*/) assert.doesNotMatch(aiModeSurface, /createInlineMessage\('assistant', buildStewardPlanMessageText\(plan\)/) assert.doesNotMatch(aiModeSurface, /runOrchestrator\(/) assert.doesNotMatch(aiModeSurface, /buildFallbackAnswer/) assert.doesNotMatch(aiModeSurface, /已使用本地回复/) assert.doesNotMatch(aiModeSurface, /emit\('open-assistant'/) assert.match(aiModeStyles, /--ai-theme-rgb:\s*var\(--theme-primary-rgb/) assert.match(aiModeStyles, /\.workbench-ai-mode\s*\{[\s\S]*min-height:\s*100%;[\s\S]*background:/) assert.match(aiModeStyles, /\.workbench-ai-mode\.has-conversation\s*\{[\s\S]*place-items:\s*stretch;[\s\S]*padding:\s*0;/) assert.match(aiModeStyles, /\.workbench-ai-composer\s*\{[\s\S]*border-radius:\s*20px;[\s\S]*box-shadow:/) assert.match(fileStripRule, /flex-wrap:\s*wrap;/) assert.match(fileStripRule, /justify-content:\s*flex-start;/) assert.match(fileCardRule, /grid-template-columns:\s*48px minmax\(0,\s*1fr\) 30px;/) assert.match(fileCardRule, /border-radius:\s*16px;/) assert.match(aiModeStyles, /\.workbench-ai-file-card__body strong,[\s\S]*\.workbench-ai-file-card__body small\s*\{[\s\S]*text-overflow:\s*ellipsis;/) assert.match(aiModeStyles, /\.workbench-ai-file-card__remove\s*\{[\s\S]*border-radius:\s*999px;/) assert.match(aiModeStyles, /\.ai-attachment-association-card/) assert.match(aiModeStyles, /\.ai-ocr-recognition-card/) assert.match(aiModeStyles, /\.ai-attachment-association__note/) assert.match(aiModeStyles, /\.workbench-ai-ocr-detail-panel/) assert.match(aiModeStyles, /\.workbench-ai-ocr-document__fields/) assert.match(composerRule, /min-height:\s*154px;/) assert.match(composerRule, /grid-template-rows:\s*minmax\(80px,\s*1fr\) auto;/) assert.match(composerTextareaRule, /min-height:\s*80px;/) assert.doesNotMatch(aiModeStyles, /--workbench-ai-robot-image/) assert.match(aiBackgroundRule, /inset:\s*0;/) assert.match(aiBackgroundRule, /linear-gradient\(90deg,\s*rgba\(var\(--ai-theme-rgb\)/) assert.match(aiBackgroundRule, /background-size:\s*56px 56px,\s*56px 56px,\s*auto,\s*auto;/) assert.match(aiModeStyles, /\.workbench-ai-orb\s*\{[\s\S]*border-radius:\s*50%;/) assert.match(orbRule, /rgba\(255,\s*255,\s*255,\s*0\.98\)/) assert.match(orbRule, /rgba\(47,\s*124,\s*255,\s*0\.18\)/) assert.match(orbRule, /width:\s*clamp\(118px,\s*8vw,\s*132px\);/) assert.match(orbRule, /height:\s*clamp\(118px,\s*8vw,\s*132px\);/) assert.match(orbRule, /animation:\s*workbenchAiControlIn/) assert.match(orbImageRule, /width:\s*100%;/) assert.match(orbImageRule, /height:\s*100%;/) assert.match(orbImageRule, /object-fit:\s*contain;/) assert.match(orbImageRule, /object-position:\s*center center;/) assert.doesNotMatch(orbImageRule, /transform:/) assert.match(aiModeStyles, /@keyframes workbenchAiControlIn\s*\{[\s\S]*opacity:\s*0;[\s\S]*translateY\(18px\)[\s\S]*opacity:\s*1;/) assert.match(aiModeStyles, /\.workbench-ai-copy\s*\{[\s\S]*animation:\s*workbenchAiControlIn/) assert.match(aiModeStyles, /\.workbench-ai-composer\s*\{[\s\S]*animation:\s*workbenchAiControlIn/) assert.match(aiModeStyles, /\.workbench-ai-composer textarea\s*\{[\s\S]*animation:\s*workbenchAiControlIn/) assert.match(aiModeStyles, /\.workbench-ai-icon-btn\s*\{[\s\S]*animation:\s*workbenchAiControlIn/) assert.match(aiModeStyles, /\.workbench-ai-send-btn\s*\{[\s\S]*animation:\s*workbenchAiControlIn/) assert.match(aiModeStyles, /\.workbench-ai-action:nth-child\(4\)\s*\{[\s\S]*animation-delay:\s*520ms;/) assert.match(aiModeStyles, /\.workbench-ai-conversation\s*\{[\s\S]*grid-template-rows:\s*minmax\(0,\s*1fr\) auto;/) assert.match(aiModeSurface, /const inlineConversationAutoScrollPinned = ref\(true\)/) assert.match(aiModeSurface, /const INLINE_AUTO_SCROLL_THRESHOLD = 96/) assert.match(aiModeSurface, /const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260/) assert.match(aiModeSurface, /function isInlineConversationNearBottom\(\)/) assert.match(aiModeSurface, /function handleInlineConversationScroll\(\)\s*\{[\s\S]*inlineConversationAutoScrollPinned\.value = isInlineConversationNearBottom\(\)[\s\S]*\}/) assert.match(aiModeSurface, /function forceInlineConversationToBottom\(\)/) assert.match(aiModeSurface, /el\.scrollTop = el\.scrollHeight/) assert.match(aiModeSurface, /function scrollInlineConversationToBottom\(options = \{\}\)/) assert.match(aiModeSurface, /const shouldScroll = options\.force !== false/) assert.match(aiModeSurface, /if \(!shouldScroll\) \{[\s\S]*return[\s\S]*\}/) assert.match(aiModeSurface, /window\.requestAnimationFrame\(\(\) => \{[\s\S]*forceInlineConversationToBottom\(\)[\s\S]*\}\)/) assert.match(aiModeSurface, /window\.setTimeout\(\(\) => \{[\s\S]*if \(inlineConversationAutoScrollPinned\.value\) \{[\s\S]*forceInlineConversationToBottom\(\)[\s\S]*\}[\s\S]*\}, INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS\)/) assert.match(aiModeSurface, /const shouldAutoScroll = inlineConversationAutoScrollPinned\.value[\s\S]*updateInlineMessageContent\(message, streamedContent\)[\s\S]*scrollInlineConversationToBottom\(\{ force: shouldAutoScroll \}\)/) assert.match(aiModeSurface, /const shouldAutoScroll = inlineConversationAutoScrollPinned\.value[\s\S]*appendInlineMessageContent\(message, data\.delta \|\| data\.content \|\| data\.text \|\| ''\)[\s\S]*scrollInlineConversationToBottom\(\{ force: shouldAutoScroll \}\)/) assert.match(aiModeSurface, /inlineConversationAutoScrollPinned\.value = true[\s\S]*conversationMessages\.value\.push\(createInlineMessage\('user', cleanPrompt\)\)/) assert.match(aiModeSurface, /function openInlineRecentConversation\(item = \{\}\) \{[\s\S]*inlineConversationAutoScrollPinned\.value = true[\s\S]*conversationMessages\.value =/) assert.doesNotMatch(aiModeSurface, /scrollTo\(\{ top: el\.scrollHeight, behavior: 'smooth' \}\)/) assert.match(aiModeStyles, /\.workbench-ai-thread\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-direction:\s*column;[\s\S]*overflow-y:\s*auto;[\s\S]*scrollbar-width:\s*none;/) assert.match(aiModeStyles, /\.workbench-ai-thread\s*>\s*:first-child\s*\{[\s\S]*margin-top:\s*auto;/) assert.match(aiModeStyles, /\.workbench-ai-message\s*\{[\s\S]*flex:\s*0 0 auto;/) assert.match(aiModeStyles, /\.workbench-ai-empty-thread\s*\{[\s\S]*flex:\s*0 0 auto;/) assert.doesNotMatch(aiModeStyles, /align-content:\s*end;/) assert.doesNotMatch(aiModeStyles, /\.workbench-ai-thread\s*\{[\s\S]*scroll-behavior:\s*smooth;/) assert.match(aiModeStyles, /\.workbench-ai-thread::-webkit-scrollbar\s*\{[\s\S]*display:\s*none;/) assert.match(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*position:\s*relative;[\s\S]*z-index:\s*6;/) assert.doesNotMatch(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*position:\s*sticky;/) assert.doesNotMatch(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*bottom:\s*0;/) assert.match(aiModeStyles, /\.workbench-ai-conversation-bottom::before\s*\{[\s\S]*display:\s*none;/) assert.match(aiModeStyles, /\.workbench-ai-thinking-panel\s*\{[\s\S]*display:\s*grid;[\s\S]*border:\s*1px solid rgba\(191,\s*219,\s*254,\s*0\.58\);/) assert.match(aiModeStyles, /\.workbench-ai-thinking-toggle\s*\{[\s\S]*border:\s*0;[\s\S]*background:\s*transparent;/) assert.match(aiModeStyles, /\.workbench-ai-thinking-list\s*\{[\s\S]*border:\s*0;[\s\S]*background:\s*transparent;[\s\S]*overflow:\s*visible;/) assert.match(aiModeStyles, /\.workbench-ai-thinking-item\s*\{[\s\S]*grid-template-columns:\s*18px minmax\(0,\s*1fr\);/) assert.match(aiModeStyles, /\.workbench-ai-thinking-dot\s*\{[\s\S]*justify-self:\s*center;/) assert.doesNotMatch(aiModeStyles, /\.workbench-ai-thinking-collapse-btn:disabled/) assert.match(aiModeStyles, /\.workbench-ai-thinking-collapse-enter-active,[\s\S]*\.workbench-ai-thinking-collapse-leave-active\s*\{[\s\S]*max-height 220ms ease/) assert.match(aiModeStyles, /\.workbench-ai-confirm-dialog\s*\{[\s\S]*border-radius:\s*18px;/) assert.match(aiModeStyles, /\.workbench-ai-answer-card\s*\{[\s\S]*box-shadow:\s*none;[\s\S]*backdrop-filter:\s*none;/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown\s*\{[\s\S]*line-height:\s*1\.86;/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(h3\)\s*\{[\s\S]*font-size:\s*21px;/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-grid\)\s*\{[\s\S]*border-left:\s*3px solid/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-card\)\s*\{[\s\S]*background:\s*transparent;/) assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-step-index\)\s*\{[\s\S]*background:\s*transparent;[\s\S]*font-size:\s*17px;/) assert.match(aiModeStyles, /\.workbench-ai-date-popover\s*\{[\s\S]*animation:\s*workbenchAiPopoverIn/) assert.match(aiModeStyles, /\.workbench-ai-send-btn:not\(:disabled\)\s*\{[\s\S]*linear-gradient\(135deg,[\s\S]*#1d4ed8/) assert.match(aiModeStyles, /\.workbench-ai-composer--inline\s*\{[\s\S]*min-height:\s*126px;[\s\S]*box-shadow:\s*none;/) assert.match(aiModeStyles, /@media \(prefers-reduced-motion:\s*reduce\)[\s\S]*\.workbench-ai-action,[\s\S]*\.workbench-ai-message,[\s\S]*\.workbench-ai-composer--inline,[\s\S]*\.workbench-ai-date-popover,[\s\S]*\.workbench-ai-thinking-dot\s*\{[\s\S]*animation:\s*none;/) assert.ok(statSync(orbIconAsset).size > 100 * 1024) assert.ok(statSync(orbIconAsset).size < 3 * 1024 * 1024) assert.ok(statSync(orbIconPngAsset).size > 100 * 1024) assert.equal(orbIconBuffer.subarray(0, 6).toString('ascii'), 'GIF89a') assert.ok(countGifFrameBlocks(orbIconBuffer) >= 120) const gifMotion = measureGifMotion(orbIconAsset) assert.ok(gifMotion.seamDelta > gifMotion.medianAdjacentDelta * 0.35) assert.ok(gifMotion.seamDelta < gifMotion.medianAdjacentDelta * 1.8) assert.ok(measureGifDuration(orbIconAsset) >= 8000) assert.ok(measureGifDuration(orbIconAsset) / countGifFrameBlocks(orbIconBuffer) <= 75) const gifPresentation = measureOrbAssetPresentation(orbIconAsset) assert.equal(gifPresentation.width, 192) assert.equal(gifPresentation.height, 192) assert.ok(gifPresentation.minimumCornerLuma > 225) assert.ok(gifPresentation.maximumCornerLuma < 250) assert.ok(gifPresentation.minimumBackgroundSimilarityRatio > 0.25) assert.ok(gifPresentation.minimumForegroundWidthRatio > 0.9) assert.ok(gifPresentation.minimumForegroundHeightRatio > 0.9) const pngPresentation = measureOrbAssetPresentation(orbIconPngAsset) assert.ok(pngPresentation.minimumCornerLuma > 225) assert.ok(pngPresentation.maximumCornerLuma < 250) assert.ok(pngPresentation.minimumBackgroundSimilarityRatio > 0.25) assert.ok(pngPresentation.minimumForegroundWidthRatio > 0.9) assert.ok(pngPresentation.minimumForegroundHeightRatio > 0.9) }) test('AI mode normal assistant requests include OCR context for uploaded receipts', () => { assert.match(aiModeSurface, /function isLikelyAiModeOcrFile\(file = \{\}\)/) assert.match(aiModeSurface, /async function collectAiModeReceiptContext\(files = \[\]\)/) 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, /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/) })