import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; import { compileTemplate, parse } from "vue/compiler-sfc"; const aiAssistantPath = fileURLToPath(new URL("../../apps/desktop/src/components/editor/AiAssistant.vue", import.meta.url)); const zhCnLocalePath = fileURLToPath(new URL("../../apps/desktop/src/i18n/locales/zh-CN.ts", import.meta.url)); const source = readFileSync(aiAssistantPath, "utf8"); const zhCnLocaleSource = readFileSync(zhCnLocalePath, "utf8"); test("AI composer keeps templates available without connections outside plugin conversations", () => { const contextRowStart = source.indexOf("data-ai-composer-context-row"); const contextRowEnd = source.indexOf('v-if="mentionOpen"', contextRowStart); const contextRow = source.slice(contextRowStart, contextRowEnd); assert.notEqual(contextRowStart, -1, "the composer context row should exist"); assert.notEqual(contextRowEnd, -1, "the connection context row should end before mention suggestions"); assert.match(contextRow, /
/); assert.match(contextRow, //); assert.match(contextRow, /max-w-\[40%\]/); assert.match(contextRow, /\{\{ templateSelectorTriggerLabel \}\}<\/span>/); assert.match(contextRow, /:aria-label="templateSelectorTriggerLabel"/); assert.doesNotMatch(contextRow, /ai-prompt-context-break/); assert.match(source, /\.ai-prompt-context-row--schema \.ai-template-selector-trigger \{[\s\S]*?flex: 0 0 1\.5rem;[\s\S]*?max-width: 1\.5rem;/); }); test("AI composer labels an empty template selection explicitly", () => { assert.match(source, /const templateSelectorTriggerLabel = computed\(\(\) => \{[\s\S]*?templateSelectorLabel.*templateSelectorLabel\.value/); assert.match(zhCnLocaleSource, /templateSelectorNone: "未选择"/); }); test("AI composer exposes mode and action as one compact selector", () => { const footerStart = source.indexOf(""); const footerEnd = source.indexOf("", footerStart); const footer = source.slice(footerStart, footerEnd); assert.notEqual(footerStart, -1, "the combined mode and action selector should exist"); assert.notEqual(footerEnd, -1, "the model selector should follow the combined selector"); assert.match(footer, //); assert.match(footer, /:aria-label="modeActionTriggerLabel"/); assert.match(footer, /switchModeActionTab\('ask'\)/); assert.match(footer, /switchModeActionTab\('agent'\)/); assert.match(footer, /selectModeActionItem\(button\.action\)/); assert.doesNotMatch(footer, /selectAction\(button\.action\)/); assert.match(footer, /", source.indexOf("", selectorStart)); const selector = source.slice(selectorStart, selectorEnd); assert.notEqual(selectorStart, -1, "the combined provider and model selector should exist"); assert.match(selector, //); assert.match(selector, //); assert.match(selector, /@mouseenter="openEffortMenu"/); assert.match(selector, /@mouseleave="scheduleEffortMenuClose"/); assert.match(selector, / { const selectorStart = source.indexOf(""); const selectorEnd = source.indexOf("", source.indexOf("", selectorStart)); const selector = source.slice(selectorStart, selectorEnd); assert.match(selector, /
/); assert.match(selector, /class="sticky top-0 z-10 flex w-full items-center gap-2 rounded-sm bg-popover/); assert.match(selector, /class="ml-5 border-l border-border\/60 pl-1"/); assert.match(selector, /v-if="configIndex < configuredProviders\.length - 1" class="my-1 border-t"/); assert.doesNotMatch(selector, /bg-accent text-accent-foreground.*config\.id === settings\.activeModel/); }); test("AI model and effort menu refreshes do not persist effort settings", () => { const loaderStart = source.indexOf("async function ensureModelEffort"); const loaderEnd = source.indexOf("function handleModelSelect", loaderStart); const loader = source.slice(loaderStart, loaderEnd); assert.notEqual(loaderStart, -1, "the effort capability loader should exist"); assert.notEqual(loaderEnd, -1, "the model selection handler should follow the effort loader"); assert.match(loader, /await resolveEffort\(config, modelId, force\)/); assert.doesNotMatch(loader, /updateActiveEffort|persistAiChatSelection/); }); test("AI composer template remains compilable", () => { const { descriptor, errors } = parse(source, { filename: aiAssistantPath }); assert.deepEqual(errors, []); assert.ok(descriptor.template); const result = compileTemplate({ id: aiAssistantPath, filename: aiAssistantPath, source: descriptor.template.content }); assert.deepEqual(result.errors, []); });