import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' const appShell = readFileSync( fileURLToPath(new URL('../src/views/AppShellRouteView.vue', import.meta.url)), 'utf8' ) const aiSidebar = readFileSync( fileURLToPath(new URL('../src/components/layout/AiSidebarRail.vue', import.meta.url)), 'utf8' ) const aiSidebarStyles = readFileSync( fileURLToPath(new URL('../src/assets/styles/components/ai-sidebar-rail.css', import.meta.url)), 'utf8' ) const aiBusinessAccess = readFileSync( fileURLToPath(new URL('../src/utils/aiSidebarBusinessAccess.js', import.meta.url)), 'utf8' ) const appStyles = readFileSync( fileURLToPath(new URL('../src/assets/styles/app.css', import.meta.url)), 'utf8' ) const extractCssBlock = (source, selector) => source.match(new RegExp(`${selector}\\s*\\{([\\s\\S]*?)\\n\\}`))?.[1] || '' test('workbench AI mode swaps the traditional rail for the AI three-layer rail', () => { assert.match(appShell, /import AiSidebarRail from '\.\.\/components\/layout\/AiSidebarRail\.vue'/) assert.match(appShell, / workbenchMode\.value === 'ai'\)/) assert.match(appShell, /const isWorkbenchAiMode = computed\(\(\) => activeView\.value === 'workbench' && workbenchMode\.value === 'ai'\)/) assert.match(appShell, /@new-chat="openAiSidebarNewChat"/) assert.match(appShell, /@open-recent="openAiSidebarRecent"/) assert.match(appShell, /@rename-conversation="handleAiConversationRename"/) assert.match(appShell, /@logout="handleLogout"/) assert.match(appShell, /import \{ loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation \} from '\.\.\/utils\/aiWorkbenchConversationStore\.js'/) assert.match(appShell, /function openAiSidebarNewChat\(\)/) assert.match(appShell, /function openAiSidebarRecent\(item = \{\}\)/) assert.match(appShell, /function handleAiConversationRename\(payload = \{\}\)/) assert.match(appShell, /import \{ computed, nextTick, onBeforeUnmount, onMounted, ref, watch \} from 'vue'/) assert.match(appShell, /async function openAiConversationWorkspace\(type, payload = null\)/) assert.match(appShell, /const navigation = handleNavigate\('workbench'\)/) assert.match(appShell, /if \(navigation && typeof navigation\.then === 'function'\)[\s\S]*await navigation/) assert.match(appShell, /await nextTick\(\)/) assert.match(appShell, /dispatchAiSidebarCommand\(type, payload\)/) assert.match(appShell, /void openAiConversationWorkspace\('new-chat'\)/) assert.match(appShell, /void openAiConversationWorkspace\('open-recent', item\)/) assert.doesNotMatch(appShell, /openAiSidebarSearchChat/) assert.match(appShell, /const aiSidebarCommand = ref\(\{ seq: 0, type: '', payload: null \}\)/) assert.match(appShell, /const aiConversationHistory = ref\(\[\]\)/) assert.match(appShell, /:active-conversation-id="aiActiveConversationId"/) assert.match(appShell, /:conversation-history="aiConversationHistory"/) assert.match(appShell, /:brand-name="PRODUCT_DISPLAY_NAME"/) assert.match(appShell, /:brand-logo="companyProfile\.logo"/) assert.match(appShell, /:company-name="ENTERPRISE_DISPLAY_NAME"/) assert.match(appShell, /:ai-sidebar-command="aiSidebarCommand"/) assert.match(appShell, /@ai-conversation-change="handleAiConversationChange"/) assert.match(appShell, /@ai-conversation-history-change="handleAiConversationHistoryChange"/) assert.match(appShell, /function dispatchAiSidebarCommand\(type, payload = null\)/) assert.match(appShell, /function handleAiConversationHistoryChange\(payload = \[\]\)/) assert.match(appShell, /loadAiWorkbenchConversationHistory\(user \|\| \{\}\)/) assert.match(appShell, /saveAiWorkbenchConversation\(currentUser\.value \|\| \{\},[\s\S]*\.\.\.target,[\s\S]*title/) assert.match(appShell, /:current-user="currentUser"/) assert.doesNotMatch(appShell, /restoreLatestConversation:\s*true/) assert.match(appShell, /'workbench-ai-sidebar-active': isAiShellMode/) assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/) assert.match(appShell, /sidebarCollapsed\.value = false/) assert.match(appShell, /sidebarCollapsed\.value = sidebarCollapsedBeforeAiMode\.value/) assert.match(appStyles, /\.app\s*\{[\s\S]*--sidebar-expanded-width:\s*304px;/) assert.match(appStyles, /\.sidebar-mode-fade-enter-active,[\s\S]*\.sidebar-mode-fade-leave-active\s*\{[\s\S]*opacity 180ms/) }) test('AI sidebar has quick actions, business navigation and conversation history layers', () => { assert.match(aiSidebar, /aria-label="AI模式导航"/) assert.match(aiSidebar, /class="ai-rail-brand"/) assert.match(aiSidebar, /aria-label="当前产品标识"/) assert.match(aiSidebar, /displayBrandName/) assert.match(aiSidebar, /brandName:\s*\{\s*type:\s*String/) assert.match(aiSidebar, /brandLogo:\s*\{\s*type:\s*String/) assert.match(aiSidebar, /String\(props\.brandName \|\| '易财费控'\)/) assert.doesNotMatch(aiSidebar, /远光软件股份有限公司/) assert.doesNotMatch(aiSidebar, /AI Workbench/) assert.match(aiSidebar, /aria-label="对话操作"/) assert.match(aiSidebar, /新建对话/) assert.match(aiSidebar, /查询对话/) assert.doesNotMatch(aiSidebar, /自定义/) assert.doesNotMatch(aiSidebar, /业务工作舱/) assert.match(aiSidebar, /resolveAiSidebarBusinessViewIds/) assert.match(aiSidebar, /\.filter\(\(item\) => aiBusinessViewIds\.value\.has\(item\.id\)\)/) assert.match(aiSidebar, /class="ai-nav-list"/) assert.match(aiSidebar, /v-for="item in businessNavItems"/) assert.match(aiSidebar, /ai-nav-copy/) assert.match(aiSidebar, /item\.aiIcon/) assert.match(aiSidebar, /aria-current/) assert.doesNotMatch(aiSidebar, /displayHint/) assert.doesNotMatch(aiSidebar, /个人工作台/) assert.doesNotMatch(aiSidebar, /待办与助手/) assert.doesNotMatch(aiSidebar, /v-html="item\.icon"/) assert.match(aiSidebar, /最近对话/) assert.match(aiSidebar, /conversationHistory:\s*\{ type:\s*Array,\s*default:\s*\(\) => \[\] \}/) assert.match(aiSidebar, /const conversationSearchOpen = ref\(false\)/) assert.match(aiSidebar, /const conversationSearchQuery = ref\(''\)/) assert.match(aiSidebar, /function openConversationSearch\(\)/) assert.match(aiSidebar, /\s*