import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; function source(path: string) { return readFileSync(resolve(import.meta.dir, path), "utf8"); } describe("canvas resource mention editor", () => { test("refreshes async reference previews even when prompt text has not changed", () => { const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx"); const unchangedTextBranch = component.match(/if \(currentValue === value && lastRenderedValueRef.current === value\) \{([^}]+)\}/)?.[1] || ""; expect(unchangedTextBranch).toContain("syncInlineMentionPreviews(editor, activeReferences)"); const sync = component.slice(component.indexOf("function syncInlineMentionPreviews("), component.indexOf("function MentionMenu(")); expect(sync).toContain('byId.get(chip.dataset.mentionReferenceId || "")'); expect(sync).toContain('preview.getAttribute("src") !== src'); expect(sync).toContain("preview.replaceWith(createInlinePreview(reference))"); expect(sync).not.toContain("replaceChildren"); expect(sync).not.toContain("onChange("); }); test("uses stable component classes for inline media references", () => { const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx"); expect(component).toContain("chip.className = `canvas-resource-inline-mention"); expect(component).toContain("canvas-resource-inline-preview is-${reference.kind}"); expect(component).not.toContain("size-[1.18em]"); }); test("clamps native media dimensions so video previews cannot cover prompt text", () => { const css = source("../src/styles/globals.css"); const previewRule = css.match(/\.canvas-resource-inline-preview \{[^}]+}/)?.[0] || ""; expect(previewRule).toContain("width: var(--canvas-mention-chip-preview-size)"); expect(previewRule).toContain("min-width: var(--canvas-mention-chip-preview-size)"); expect(previewRule).toContain("max-width: var(--canvas-mention-chip-preview-size)"); expect(previewRule).toContain("height: var(--canvas-mention-chip-preview-size)"); expect(previewRule).toContain("flex: 0 0 var(--canvas-mention-chip-preview-size)"); expect(previewRule).toContain("object-fit: cover"); }); test("exposes inline image references as replacement drop targets", () => { const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx"); const css = source("../src/styles/globals.css"); expect(component).toContain("activeDropReferenceId?: string | null"); expect(component).toContain("onReferenceFilesDrop?:"); expect(component).toContain("chip.dataset.mentionReferenceId = reference.id"); expect(component).toContain('chip.classList.toggle("is-replace-target"'); expect(component).toContain("onReferenceFilesDrop(reference, files)"); expect(css).toContain(".canvas-resource-inline-mention.is-replace-target"); expect(css).toContain('content: "替换"'); }); test("resolves storage-backed previews and renders a visible loading spinner", () => { const editor = source("../src/components/canvas/canvas-resource-mention-textarea.tsx"); const panel = source("../src/components/canvas/canvas-node-prompt-panel.tsx"); const configComposer = source("../src/components/canvas/canvas-config-composer.tsx"); const project = source("../src/pages/canvas/project.tsx"); expect(editor).toContain("useResolvedCanvasResourceReferences"); expect(panel).toContain(" { const imageExecutor = source("../src/pages/canvas/canvas-image-generation-executor.ts"); const mediaExecutors = source("../src/pages/canvas/canvas-media-generation-executors.ts"); const textExecutor = source("../src/pages/canvas/canvas-text-generation-executor.ts"); const generationExecutor = source("../src/pages/canvas/use-canvas-generation-executor.ts"); expect(imageExecutor.match(/canvasGenerationPromptMetadata\(prompt, effectivePrompt\)/g)?.length).toBeGreaterThanOrEqual(3); expect(imageExecutor).toContain("imageBatchExpanded: count > 1 ? true : undefined"); expect(imageExecutor).toContain("imageGenerationReferenceConnections"); expect(imageExecutor).toContain("retireImageBatchChildren"); expect(mediaExecutors.match(/canvasGenerationPromptMetadata\(prompt, effectivePrompt\)/g)?.length).toBe(2); expect(textExecutor.match(/canvasGenerationPromptMetadata\(prompt, effectivePrompt\)/g)?.length).toBe(2); expect(generationExecutor).toContain("composerContent: prompt"); expect(generationExecutor).toContain("canvasGenerationPromptMetadata(prompt, statusPrompt)"); }); test("anchors the mention menu to the caret instead of the textarea edge", () => { const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx"); expect(component).toContain("cursorOffset={mention.end}"); expect(component).toContain("mentionCaretRect(anchor, cursorOffset)"); expect(component).toContain("textareaCaretRect(anchor, cursorOffset)"); expect(component).toContain('transform: position.showAbove ? "translateY(-100%)" : undefined'); expect(component).toContain('window.addEventListener("scroll", updatePosition, true)'); }); test("renders skill references as descriptive workflow rows", () => { const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx"); const css = source("../src/styles/globals.css"); expect(component).toContain('reference.kind === "skill" ? "is-skill" : ""'); expect(component).toContain("reference.skill?.description"); expect(component).toContain("reference.skill?.fileCount"); expect(component).toContain(""); expect(css).toContain(".canvas-resource-mention-item.is-skill"); expect(css).toContain(".canvas-resource-mention-meta"); }); test("agent composer attachments stay large, previewable, and mentionable", () => { const component = source("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx"); expect(component).toContain("w-20 shrink-0"); expect(component).toContain("insertAttachmentMention"); expect(component).toContain("@[attachment:"); expect(component).toContain("AgentImagePreview"); expect(component).toContain("composerReferences"); expect(component).toContain("点击放大预览"); }); test("agent composer renders slash skill references as stable skill chips and consumes the typed slash query", () => { const component = source("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx"); expect(component).toContain("const token = `@[skill:${skill.skillId}] `"); expect(component).toContain("slash.start + 1 + slash.query.length"); expect(component).toContain("buildSkillMentionReferences(availableSlashSkills)"); expect(component).toContain("[/、]([^\\s/、]*)$"); // 保留主分支已恢复的固定 Skills 提示,不能因合并旧分支退回失效的外观配置断言。 expect(source("../src/lib/canvas/agent-appearance.ts")).toContain("用 / 或 、 引用 Skills"); expect(source("../src/components/canvas/canvas-cloud-agent-panel.tsx")).toContain("用 / 或 、 引用 Skills"); }); test("skill chips use one colored icon instead of exposing the serialized token", () => { const component = source("../src/components/canvas/canvas-resource-mention-textarea.tsx"); const chat = source("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx"); const css = source("../src/components/canvas/canvas-cloud-agent.css"); expect(component).toContain('const isDecorated = reference.kind === "skill" || reference.kind === "tool"'); expect(component).toContain('chip.className = `canvas-resource-inline-mention ${isDecorated ? `is-${reference.kind}` : ""}`'); expect(component).toMatch(/if \(reference.kind === "skill"\)\s*\{\s*prefix.textContent = "✦"/); expect(component).toContain('prefix.textContent = "@"'); expect(component).toContain("if (!isDecorated) chip.appendChild(createInlinePreview(reference));"); expect(component).toContain('chip.style.setProperty("--canvas-skill-mention-color", skillMentionColor(reference))'); expect(chat).toContain('sendOnEnter={canSubmit ? "both" : false}'); expect(chat).toContain("agent-composer-resize-handle"); expect(chat).toContain("Enter 发送 · Shift+Enter 换行"); expect(css).toContain(".agent-composer-send-hint-full"); expect(css).toContain(".agent-composer-send-hint-compact"); expect(css).toContain(".agent-composer-prompt-scroll"); expect(css).not.toContain(".agent-tool-row:hover"); }); });