11import { describe , expect , it } from "vitest" ;
2+ import { mkdtempSync , writeFileSync } from "node:fs" ;
3+ import { tmpdir } from "node:os" ;
4+ import { join } from "node:path" ;
5+ import { pathToFileURL } from "node:url" ;
26import type { LanguageModelV3Prompt } from "@ai-sdk/provider" ;
37import {
48 latestUserMessage ,
59 promptToCursorMessage ,
610} from "../src/provider/message-map.js" ;
711
12+ /** Write a temp file and return its absolute path + file:// URL string. */
13+ function tempFile ( name : string , bytes : Buffer ) : { path : string ; url : string } {
14+ const dir = mkdtempSync ( join ( tmpdir ( ) , "cursor-msgmap-" ) ) ;
15+ const path = join ( dir , name ) ;
16+ writeFileSync ( path , bytes ) ;
17+ return { path, url : pathToFileURL ( path ) . href } ;
18+ }
19+
20+ const PNG_BYTES = Buffer . from (
21+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" ,
22+ "base64" ,
23+ ) ;
24+
825describe ( "promptToCursorMessage" , ( ) => {
926 it ( "flattens a multi-role conversation into a transcript" , ( ) => {
1027 const prompt : LanguageModelV3Prompt = [
@@ -21,24 +38,32 @@ describe("promptToCursorMessage", () => {
2138 expect ( msg . images ) . toBeUndefined ( ) ;
2239 } ) ;
2340
24- it ( "attaches images from the final user turn as base64 data" , ( ) => {
41+ // Cursor's LOCAL SDK agent (the only backend the chat path uses) cannot
42+ // accept images in any form: `{url}` throws "URL images are only supported
43+ // for cloud SDK agents", and `{data,mimeType}` base64 fails the run with an
44+ // empty `status:"error"` (the "Cursor run ended with status error" a user
45+ // hits on an `@image` mention). So we never populate `images`; instead every
46+ // file part is surfaced as a text note so the run always completes.
47+ it ( "never attaches images; notes an image file part as text" , ( ) => {
2548 const bytes = new Uint8Array ( [ 1 , 2 , 3 , 4 ] ) ;
2649 const prompt : LanguageModelV3Prompt = [
2750 {
2851 role : "user" ,
2952 content : [
3053 { type : "text" , text : "Describe this" } ,
31- { type : "file" , data : bytes , mediaType : "image/png" } ,
54+ {
55+ type : "file" ,
56+ data : bytes ,
57+ mediaType : "image/png" ,
58+ filename : "shot.png" ,
59+ } ,
3260 ] ,
3361 } ,
3462 ] ;
3563 const msg = promptToCursorMessage ( prompt ) ;
36- expect ( msg . images ) . toHaveLength ( 1 ) ;
37- expect ( msg . images ! [ 0 ] ) . toEqual ( {
38- data : Buffer . from ( bytes ) . toString ( "base64" ) ,
39- mimeType : "image/png" ,
40- } ) ;
41- expect ( msg . text ) . toContain ( "[image attached]" ) ;
64+ expect ( msg . images ) . toBeUndefined ( ) ;
65+ expect ( msg . text ) . toContain ( "shot.png" ) ;
66+ expect ( msg . text ) . toContain ( "image/png" ) ;
4267 } ) ;
4368
4469 it ( "includes tool outputs (truncated) instead of dropping them" , ( ) => {
@@ -90,7 +115,7 @@ describe("promptToCursorMessage", () => {
90115 expect ( msg . text . length ) . toBeLessThan ( 3000 ) ;
91116 } ) ;
92117
93- it ( "passes through image URLs " , ( ) => {
118+ it ( "notes an http(s) image URL as text without attaching it " , ( ) => {
94119 const prompt : LanguageModelV3Prompt = [
95120 {
96121 role : "user" ,
@@ -99,12 +124,77 @@ describe("promptToCursorMessage", () => {
99124 type : "file" ,
100125 data : "https://example.com/a.png" ,
101126 mediaType : "image/png" ,
127+ filename : "a.png" ,
102128 } ,
103129 ] ,
104130 } ,
105131 ] ;
106132 const msg = promptToCursorMessage ( prompt ) ;
107- expect ( msg . images ! [ 0 ] ) . toEqual ( { url : "https://example.com/a.png" } ) ;
133+ expect ( msg . images ) . toBeUndefined ( ) ;
134+ expect ( msg . text ) . toContain ( "a.png" ) ;
135+ } ) ;
136+
137+ it ( "notes a file:// image URL (URL object) as text, never as an image" , ( ) => {
138+ const { url } = tempFile ( "px.png" , PNG_BYTES ) ;
139+ const prompt : LanguageModelV3Prompt = [
140+ {
141+ role : "user" ,
142+ content : [
143+ { type : "text" , text : "look" } ,
144+ {
145+ type : "file" ,
146+ data : new URL ( url ) ,
147+ mediaType : "image/png" ,
148+ filename : "px.png" ,
149+ } ,
150+ ] ,
151+ } ,
152+ ] ;
153+ const msg = promptToCursorMessage ( prompt ) ;
154+ expect ( msg . images ) . toBeUndefined ( ) ;
155+ expect ( msg . text ) . toContain ( "px.png" ) ;
156+ } ) ;
157+
158+ it ( "notes a data: URI image as text, never as an image" , ( ) => {
159+ const b64 = PNG_BYTES . toString ( "base64" ) ;
160+ const prompt : LanguageModelV3Prompt = [
161+ {
162+ role : "user" ,
163+ content : [
164+ {
165+ type : "file" ,
166+ data : `data:image/png;base64,${ b64 } ` ,
167+ mediaType : "image/png" ,
168+ filename : "inline.png" ,
169+ } ,
170+ ] ,
171+ } ,
172+ ] ;
173+ const msg = promptToCursorMessage ( prompt ) ;
174+ expect ( msg . images ) . toBeUndefined ( ) ;
175+ expect ( msg . text ) . toContain ( "inline.png" ) ;
176+ } ) ;
177+
178+ it ( "notes a non-image file attachment as text instead of dropping it" , ( ) => {
179+ const { url } = tempFile ( "doc.pdf" , Buffer . from ( "%PDF-1.4\n" ) ) ;
180+ const prompt : LanguageModelV3Prompt = [
181+ {
182+ role : "user" ,
183+ content : [
184+ { type : "text" , text : "summarize" } ,
185+ {
186+ type : "file" ,
187+ data : new URL ( url ) ,
188+ mediaType : "application/pdf" ,
189+ filename : "doc.pdf" ,
190+ } ,
191+ ] ,
192+ } ,
193+ ] ;
194+ const msg = promptToCursorMessage ( prompt ) ;
195+ expect ( msg . images ) . toBeUndefined ( ) ;
196+ expect ( msg . text ) . toContain ( "doc.pdf" ) ;
197+ expect ( msg . text ) . toContain ( "application/pdf" ) ;
108198 } ) ;
109199} ) ;
110200
@@ -126,4 +216,46 @@ describe("latestUserMessage", () => {
126216 ] ;
127217 expect ( latestUserMessage ( prompt ) ) . toBeUndefined ( ) ;
128218 } ) ;
219+
220+ it ( "notes an image in the final user turn as text, never as an image" , ( ) => {
221+ const { url } = tempFile ( "px.png" , PNG_BYTES ) ;
222+ const prompt : LanguageModelV3Prompt = [
223+ {
224+ role : "user" ,
225+ content : [
226+ { type : "text" , text : "look" } ,
227+ {
228+ type : "file" ,
229+ data : new URL ( url ) ,
230+ mediaType : "image/png" ,
231+ filename : "px.png" ,
232+ } ,
233+ ] ,
234+ } ,
235+ ] ;
236+ const msg = latestUserMessage ( prompt ) ;
237+ expect ( msg ?. images ) . toBeUndefined ( ) ;
238+ expect ( msg ?. text ) . toContain ( "px.png" ) ;
239+ } ) ;
240+
241+ it ( "notes a non-image attachment in the final user turn as text" , ( ) => {
242+ const { url } = tempFile ( "doc.pdf" , Buffer . from ( "%PDF-1.4\n" ) ) ;
243+ const prompt : LanguageModelV3Prompt = [
244+ {
245+ role : "user" ,
246+ content : [
247+ { type : "text" , text : "summarize" } ,
248+ {
249+ type : "file" ,
250+ data : new URL ( url ) ,
251+ mediaType : "application/pdf" ,
252+ filename : "doc.pdf" ,
253+ } ,
254+ ] ,
255+ } ,
256+ ] ;
257+ const msg = latestUserMessage ( prompt ) ;
258+ expect ( msg ?. images ) . toBeUndefined ( ) ;
259+ expect ( msg ?. text ) . toContain ( "doc.pdf" ) ;
260+ } ) ;
129261} ) ;
0 commit comments