|
1 | | -import { describe, expect, it } from 'vitest' |
2 | | -import { toCopyableMarkdown } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/copyable-markdown' |
| 1 | +import { describe, expect, it, vi } from 'vitest' |
| 2 | +import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' |
| 3 | +import { |
| 4 | + prepareCopyableMarkdown, |
| 5 | + toCopyableMarkdown, |
| 6 | +} from '@/app/workspace/[workspaceId]/home/components/mothership-chat/copyable-markdown' |
| 7 | +import { parseChipLinks } from '@/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec' |
| 8 | + |
| 9 | +const WORKSPACE_FILES: WorkspaceFileRecord[] = [ |
| 10 | + { |
| 11 | + id: 'file_bell', |
| 12 | + workspaceId: 'workspace-1', |
| 13 | + name: 'The Bell at Low Tide.md', |
| 14 | + key: 'workspace/workspace-1/file_bell', |
| 15 | + path: '/api/files/view/file_bell', |
| 16 | + size: 0, |
| 17 | + type: 'text/markdown', |
| 18 | + uploadedBy: 'user-1', |
| 19 | + uploadedAt: new Date(0), |
| 20 | + updatedAt: new Date(0), |
| 21 | + }, |
| 22 | +] |
3 | 23 |
|
4 | 24 | describe('toCopyableMarkdown', () => { |
5 | 25 | it('preserves message Markdown, including fenced code and its language', () => { |
@@ -44,4 +64,99 @@ describe('toCopyableMarkdown', () => { |
44 | 64 |
|
45 | 65 | expect(toCopyableMarkdown(message)).toBe(message) |
46 | 66 | }) |
| 67 | + |
| 68 | + it('copies workspace resources as portable Markdown links with real ids', () => { |
| 69 | + const message = [ |
| 70 | + 'Read', |
| 71 | + '<workspace_resource>{"type":"file","path":"files/The%20Bell%20at%20Low%20Tide.md","title":"The Bell at Low Tide.md"}</workspace_resource>', |
| 72 | + 'and', |
| 73 | + `<workspace_resource>${JSON.stringify({ |
| 74 | + type: 'table', |
| 75 | + id: 'tbl_f26af6dae98d4222b014b250494d00fb', |
| 76 | + title: 'Checked_[rare]\\portal', |
| 77 | + })}</workspace_resource>.`, |
| 78 | + ].join('') |
| 79 | + |
| 80 | + const markdown = toCopyableMarkdown(message, WORKSPACE_FILES) |
| 81 | + |
| 82 | + expect(markdown).toBe( |
| 83 | + 'Read [The Bell at Low Tide.md](sim:file/file_bell) and [Checked_\\[rare\\]\\\\portal](sim:table/tbl_f26af6dae98d4222b014b250494d00fb).' |
| 84 | + ) |
| 85 | + expect(parseChipLinks(markdown)).toEqual([ |
| 86 | + { |
| 87 | + kind: 'file', |
| 88 | + id: 'file_bell', |
| 89 | + label: 'The Bell at Low Tide.md', |
| 90 | + start: 5, |
| 91 | + end: 50, |
| 92 | + }, |
| 93 | + { |
| 94 | + kind: 'table', |
| 95 | + id: 'tbl_f26af6dae98d4222b014b250494d00fb', |
| 96 | + label: 'Checked_[rare]\\portal', |
| 97 | + start: 55, |
| 98 | + end: 129, |
| 99 | + }, |
| 100 | + ]) |
| 101 | + }) |
| 102 | + |
| 103 | + it('uses resolved file metadata for a resource without a title', () => { |
| 104 | + const message = |
| 105 | + 'Read <workspace_resource>{"type":"file","path":"files/The%20Bell%20at%20Low%20Tide.md"}</workspace_resource>.' |
| 106 | + |
| 107 | + expect(toCopyableMarkdown(message, WORKSPACE_FILES)).toBe( |
| 108 | + 'Read [The Bell at Low Tide.md](sim:file/file_bell).' |
| 109 | + ) |
| 110 | + }) |
| 111 | + |
| 112 | + it('refreshes missing file metadata before producing copyable Markdown', async () => { |
| 113 | + const message = |
| 114 | + 'Read <workspace_resource>{"type":"file","path":"files/The%20Bell%20at%20Low%20Tide.md","title":"The Bell at Low Tide.md"}</workspace_resource>.' |
| 115 | + const refreshWorkspaceFiles = vi.fn().mockResolvedValue(WORKSPACE_FILES) |
| 116 | + |
| 117 | + const content = prepareCopyableMarkdown(message, [], refreshWorkspaceFiles) |
| 118 | + expect(content).not.toBeTypeOf('string') |
| 119 | + if (typeof content === 'string') throw new Error('Expected deferred clipboard content') |
| 120 | + expect(content.fallback).toBe('Read The Bell at Low Tide.md.') |
| 121 | + expect(parseChipLinks(content.fallback)).toEqual([]) |
| 122 | + await expect(content.prepare()).resolves.toBe( |
| 123 | + 'Read [The Bell at Low Tide.md](sim:file/file_bell).' |
| 124 | + ) |
| 125 | + expect(refreshWorkspaceFiles).toHaveBeenCalledOnce() |
| 126 | + }) |
| 127 | + |
| 128 | + it('copies unresolved file references as plain text', () => { |
| 129 | + const message = |
| 130 | + 'Read <workspace_resource>{"type":"file","path":"files/Q1 plan).md","title":"Q1 plan).md"}</workspace_resource>.' |
| 131 | + |
| 132 | + const markdown = toCopyableMarkdown(message) |
| 133 | + |
| 134 | + expect(markdown).toBe('Read Q1 plan).md.') |
| 135 | + expect(parseChipLinks(markdown)).toEqual([]) |
| 136 | + }) |
| 137 | + |
| 138 | + it('keeps the plain-text fallback when refreshing file metadata fails', async () => { |
| 139 | + const message = |
| 140 | + 'Read <workspace_resource>{"type":"file","path":"files/notes.md","title":"notes.md"}</workspace_resource>.' |
| 141 | + const refreshWorkspaceFiles = vi.fn().mockRejectedValue(new Error('Refresh failed')) |
| 142 | + |
| 143 | + const content = prepareCopyableMarkdown(message, [], refreshWorkspaceFiles) |
| 144 | + |
| 145 | + expect(content).not.toBeTypeOf('string') |
| 146 | + if (typeof content === 'string') throw new Error('Expected deferred clipboard content') |
| 147 | + expect(content.fallback).toBe('Read notes.md.') |
| 148 | + await expect(content.prepare()).resolves.toBe('Read notes.md.') |
| 149 | + expect(refreshWorkspaceFiles).toHaveBeenCalledOnce() |
| 150 | + }) |
| 151 | + |
| 152 | + it('does not refresh metadata when all workspace resources already resolve', () => { |
| 153 | + const message = |
| 154 | + 'Read <workspace_resource>{"type":"file","path":"files/The%20Bell%20at%20Low%20Tide.md","title":"The Bell at Low Tide.md"}</workspace_resource>.' |
| 155 | + const refreshWorkspaceFiles = vi.fn() |
| 156 | + |
| 157 | + expect(prepareCopyableMarkdown(message, WORKSPACE_FILES, refreshWorkspaceFiles)).toBe( |
| 158 | + 'Read [The Bell at Low Tide.md](sim:file/file_bell).' |
| 159 | + ) |
| 160 | + expect(refreshWorkspaceFiles).not.toHaveBeenCalled() |
| 161 | + }) |
47 | 162 | }) |
0 commit comments