33from quickapp .agent ._attachment_filter import _AttachmentFilter
44
55
6- def _user_msg (content : str = "" , attachments : list [Attachment ] | None = None ) -> Message :
7- msg = Message (role = Role .USER , content = content )
6+ def _msg (
7+ role : Role , content : str | None = "" , attachments : list [Attachment ] | None = None
8+ ) -> Message :
9+ msg = Message (role = role , content = content )
810 if attachments :
911 msg .custom_content = CustomContent (attachments = attachments )
1012 return msg
1113
1214
13- def _attachment (title : str , url : str , mime_type : str ) -> Attachment :
15+ def _user_msg (content : str = "" , attachments : list [Attachment ] | None = None ) -> Message :
16+ return _msg (Role .USER , content , attachments )
17+
18+
19+ def _attachment (
20+ title : str ,
21+ url : str ,
22+ mime_type : str ,
23+ reference_url : str | None = None ,
24+ ) -> Attachment :
1425 return Attachment (
1526 title = title ,
1627 url = url ,
1728 type = mime_type ,
29+ reference_url = reference_url ,
1830 )
1931
2032
@@ -38,7 +50,7 @@ def test_non_image_attachments_removed(self):
3850 result = transformer .filter_attachments ([msg ])
3951 assert len (result [0 ].custom_content .attachments ) == 0
4052
41- def test_text_metadata_injected_for_attachments (self ):
53+ def test_xml_metadata_injected_for_attachments (self ):
4254 transformer = _AttachmentFilter ()
4355 msg = _user_msg (
4456 "original content" ,
@@ -49,12 +61,13 @@ def test_text_metadata_injected_for_attachments(self):
4961 )
5062 result = transformer .filter_attachments ([msg ])
5163 content = str (result [0 ].content )
52- assert "Attachment doc.pdf" in content
53- assert "application/pdf" in content
64+ assert "<attachments>" in content
65+ assert "<title>doc.pdf</title>" in content
66+ assert "<type>application/pdf</type>" in content
67+ assert "<title>photo.png</title>" in content
68+ assert "<type>image/png</type>" in content
5469
55- # Image attachments are kept inline AND get text metadata injected
56- assert "Attachment photo.png" in content
57- assert "image/png" in content
70+ # Image attachments are kept inline AND get XML metadata injected
5871 assert result [0 ].custom_content .attachments [0 ].type == "image/png"
5972 assert result [0 ].custom_content .attachments [0 ].title == "photo.png"
6073
@@ -105,4 +118,123 @@ def test_filter_idempotent_on_repeated_calls(self):
105118 second_content = str (second_pass [0 ].content )
106119
107120 assert first_content == second_content
108- assert first_content .count ("Attachment doc.pdf" ) == 1
121+ assert first_content .count ("<title>doc.pdf</title>" ) == 1
122+
123+ # --- Multi-message tests ---
124+
125+ def test_multi_message_each_filtered_independently (self ):
126+ transformer = _AttachmentFilter ()
127+ msg1 = _user_msg (
128+ "first" ,
129+ [
130+ _attachment ("doc.pdf" , "/files/doc.pdf" , "application/pdf" ),
131+ _attachment ("photo.png" , "/files/photo.png" , "image/png" ),
132+ ],
133+ )
134+ msg2 = _user_msg (
135+ "second" ,
136+ [_attachment ("data.csv" , "/files/data.csv" , "text/csv" )],
137+ )
138+ result = transformer .filter_attachments ([msg1 , msg2 ])
139+
140+ # First message: image kept, pdf removed
141+ assert len (result [0 ].custom_content .attachments ) == 1
142+ assert result [0 ].custom_content .attachments [0 ].type == "image/png"
143+ content0 = str (result [0 ].content )
144+ assert "<title>doc.pdf</title>" in content0
145+ assert "<title>photo.png</title>" in content0
146+
147+ # Second message: csv removed
148+ assert len (result [1 ].custom_content .attachments ) == 0
149+ content1 = str (result [1 ].content )
150+ assert "<title>data.csv</title>" in content1
151+
152+ def test_multi_message_non_attachment_messages_unchanged (self ):
153+ transformer = _AttachmentFilter ()
154+ plain_msg = _user_msg ("just text" )
155+ attach_msg = _user_msg (
156+ "with file" ,
157+ [_attachment ("doc.pdf" , "/files/doc.pdf" , "application/pdf" )],
158+ )
159+ result = transformer .filter_attachments ([plain_msg , attach_msg ])
160+
161+ # Plain message is passed through as-is (same object, no deepcopy)
162+ assert result [0 ] is plain_msg
163+ assert result [0 ].content == "just text"
164+
165+ # Attachment message is filtered
166+ assert "<title>doc.pdf</title>" in str (result [1 ].content )
167+
168+ # --- Role-based tests ---
169+
170+ def test_assistant_message_image_attachments_stripped (self ):
171+ transformer = _AttachmentFilter ()
172+ msg = _msg (
173+ Role .ASSISTANT ,
174+ "response" ,
175+ [_attachment ("photo.png" , "/files/photo.png" , "image/png" )],
176+ )
177+ result = transformer .filter_attachments ([msg ])
178+ # Non-USER roles: images are NOT kept inline
179+ assert len (result [0 ].custom_content .attachments ) == 0
180+ content = str (result [0 ].content )
181+ assert "<title>photo.png</title>" in content
182+
183+ def test_tool_message_attachments_stripped (self ):
184+ transformer = _AttachmentFilter ()
185+ msg = _msg (
186+ Role .TOOL ,
187+ "tool output" ,
188+ [_attachment ("result.png" , "/files/result.png" , "image/png" )],
189+ )
190+ result = transformer .filter_attachments ([msg ])
191+ assert len (result [0 ].custom_content .attachments ) == 0
192+ content = str (result [0 ].content )
193+ assert "<title>result.png</title>" in content
194+
195+ # --- Edge case tests ---
196+
197+ def test_empty_message_list (self ):
198+ transformer = _AttachmentFilter ()
199+ result = transformer .filter_attachments ([])
200+ assert result == []
201+
202+ def test_content_none_with_attachments (self ):
203+ transformer = _AttachmentFilter ()
204+ msg = _msg (
205+ Role .USER ,
206+ None ,
207+ [_attachment ("doc.pdf" , "/files/doc.pdf" , "application/pdf" )],
208+ )
209+ result = transformer .filter_attachments ([msg ])
210+ content = str (result [0 ].content )
211+ assert "<attachments>" in content
212+ assert "<title>doc.pdf</title>" in content
213+
214+ def test_reference_url_conditional_absent (self ):
215+ transformer = _AttachmentFilter ()
216+ msg = _user_msg (
217+ "test" ,
218+ [_attachment ("doc.pdf" , "/files/doc.pdf" , "application/pdf" )],
219+ )
220+ result = transformer .filter_attachments ([msg ])
221+ content = str (result [0 ].content )
222+ # reference_url is None by default → no element
223+ assert "<reference_url>" not in content
224+
225+ def test_reference_url_conditional_present (self ):
226+ transformer = _AttachmentFilter ()
227+ msg = _user_msg (
228+ "test" ,
229+ [
230+ _attachment (
231+ "doc.pdf" ,
232+ "/files/doc.pdf" ,
233+ "application/pdf" ,
234+ reference_url = "/refs/doc.pdf" ,
235+ )
236+ ],
237+ )
238+ result = transformer .filter_attachments ([msg ])
239+ content = str (result [0 ].content )
240+ assert "<reference_url>/refs/doc.pdf</reference_url>" in content
0 commit comments