返回 CodeWhale
tests.rs
根目录 / crates / tui / src / tools / fetch_url / tests.rs
1 use super::*;
2 use crate::tools::spec::ToolContext;
3 use std::path::PathBuf;
4
5 struct ArtifactRootRestore(Option<PathBuf>);
6
7 impl Drop for ArtifactRootRestore {
8 fn drop(&mut self) {
9 crate::artifacts::set_test_artifact_sessions_root(self.0.take());
10 }
11 }
12
13 fn runtime() -> tokio::runtime::Runtime {
14 tokio::runtime::Builder::new_current_thread()
15 .enable_all()
16 .build()
17 .expect("test runtime")
18 }
19
20 #[test]
21 fn raw_pdf_production_path_preserves_exact_bytes_without_extractor() {
22 let _lock = crate::artifacts::TEST_ARTIFACT_SESSIONS_GUARD
23 .lock()
24 .unwrap_or_else(|error| error.into_inner());
25 let temporary = tempfile::tempdir().expect("artifact root");
26 let prior =
27 crate::artifacts::set_test_artifact_sessions_root(Some(temporary.path().join("sessions")));
28 let _restore = ArtifactRootRestore(prior);
29 let bytes = b"%PDF-1.7\nraw fixture that is intentionally not parseable\n%%EOF";
30 let missing = temporary.path().join("definitely-not-pdftotext");
31 let document = runtime()
32 .block_on(extract_fetched_document(
33 Format::Raw,
34 "https://example.com/raw.pdf",
35 "application/pdf",
36 bytes,
37 true,
38 None,
39 PdfTextCommand::test(missing.as_os_str(), Duration::from_millis(50), None),
40 ))
41 .expect("signed raw PDF must bypass the missing extractor");
42 let (content, artifact) = render_extracted(
43 "https://example.com/raw.pdf",
44 "application/pdf",
45 Format::Raw,
46 document,
47 bytes,
48 &ToolContext::new("."),
49 )
50 .expect("raw PDF preservation must not require pdftotext");
51 let artifact = artifact.expect("raw PDF artifact");
52 assert!(content.contains("PDF response saved"), "{content}");
53 assert_eq!(std::fs::read(artifact.absolute_path).unwrap(), bytes);
54 }
55
56 #[test]
57 fn raw_pdf_spoofs_and_contradictory_media_mime_create_no_artifact() {
58 let _lock = crate::artifacts::TEST_ARTIFACT_SESSIONS_GUARD
59 .lock()
60 .unwrap_or_else(|error| error.into_inner());
61 let temporary = tempfile::tempdir().expect("artifact root");
62 let sessions = temporary.path().join("sessions");
63 let prior = crate::artifacts::set_test_artifact_sessions_root(Some(sessions.clone()));
64 let _restore = ArtifactRootRestore(prior);
65 let missing = temporary.path().join("definitely-not-pdftotext");
66 let request = PdfTextCommand::test(missing.as_os_str(), Duration::from_millis(50), None);
67 let runtime = runtime();
68
69 for (url, content_type, bytes, expected) in [
70 (
71 "https://example.com/download",
72 "application/pdf",
73 b"plain text pretending to be a PDF".as_slice(),
74 "PDF signature",
75 ),
76 (
77 "https://example.com/spoof.pdf",
78 "text/plain",
79 b"plain text pretending to be a PDF".as_slice(),
80 "PDF signature",
81 ),
82 (
83 "https://example.com/signed",
84 "image/png",
85 b"%PDF-1.7\n%%EOF".as_slice(),
86 "did not match its PDF bytes",
87 ),
88 ] {
89 let error = runtime
90 .block_on(extract_fetched_document(
91 Format::Raw,
92 url,
93 content_type,
94 bytes,
95 true,
96 None,
97 request,
98 ))
99 .expect_err("invalid PDF response must fail before raw preservation");
100 assert!(error.to_string().contains(expected), "{error}");
101 }
102 assert!(
103 !sessions.exists(),
104 "rejected bytes must not create artifacts"
105 );
106 }
107
108 #[tokio::test]
109 async fn fetched_pdf_missing_helper_is_a_failed_typed_outcome() {
110 let temporary = tempfile::tempdir().expect("tempdir");
111 let missing = temporary.path().join("definitely-not-pdftotext");
112 let error = extract_fetched_document(
113 Format::Text,
114 "https://example.com/document.pdf",
115 "application/pdf",
116 b"%PDF-1.7\n%%EOF",
117 true,
118 None,
119 PdfTextCommand::test(missing.as_os_str(), Duration::from_secs(1), None),
120 )
121 .await
122 .expect_err("missing helper must fail the fetched PDF call");
123 let payload = match &error.error {
124 ToolError::NotAvailable { message } => {
125 serde_json::from_str::<Value>(message).expect("structured unavailable payload")
126 }
127 other => panic!("unexpected error: {other:?}"),
128 };
129 assert_eq!(payload["type"], "binary_unavailable");
130 assert_eq!(
131 crate::tools::spec::ToolExecutionOutcome::from_legacy(Err(error.into())).status,
132 crate::tools::spec::ToolTerminalStatus::Failed
133 );
134 }
135
136 #[cfg(unix)]
137 #[tokio::test(flavor = "current_thread")]
138 async fn fetched_pdf_host_text_matches_default_and_raw_bypasses_host() {
139 use crate::extension_host::{ExtensionHostManager, ExtensionHostOptions, TestManagerGuard};
140 use crate::features::{Feature, Features};
141 use std::os::unix::fs::PermissionsExt;
142 use std::sync::Arc;
143 let _home = crate::test_support::SealedHome::new();
144 let _policy = crate::plugins::activation::TestPolicyGuard::extension_host(false);
145 let Some(node) = crate::extension_host::tests::node_for_tests("fetched_pdf_host") else {
146 return;
147 };
148 let root = tempfile::tempdir().unwrap();
149 let binary = root.path().join("fake-pdftotext");
150 std::fs::write(&binary, "#!/bin/sh\nprintf 'page one\\fpage two\\n'\n").unwrap();
151 std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)).unwrap();
152 let manager = Arc::new(ExtensionHostManager::new(ExtensionHostOptions {
153 runtime: crate::config::ExtensionHostRuntime::Node,
154 node_override: Some(node),
155 root: Some(root.path().join("host")),
156 ..ExtensionHostOptions::default()
157 }));
158 let _manager = TestManagerGuard::install(Arc::clone(&manager));
159 let rust = ToolContext::new(root.path());
160 let mut flags = Features::with_defaults();
161 flags.enable(Feature::PdfHost);
162 let host = ToolContext::new(root.path()).with_features(flags);
163 let bytes = b"%PDF-1.7\nfixture\n%%EOF";
164 for format in [Format::Text, Format::Markdown] {
165 let expected = extract_fetched_document(
166 format,
167 "https://example.com/fixture.pdf",
168 "application/pdf",
169 bytes,
170 true,
171 None,
172 PdfTextCommand::test(binary.as_os_str(), Duration::from_secs(10), None)
173 .with_context(&rust),
174 )
175 .await
176 .unwrap();
177 let actual = extract_fetched_document(
178 format,
179 "https://example.com/fixture.pdf",
180 "application/pdf",
181 bytes,
182 true,
183 None,
184 PdfTextCommand::test(binary.as_os_str(), Duration::from_secs(10), None)
185 .with_context(&host),
186 )
187 .await
188 .unwrap();
189 assert_eq!(actual.text, expected.text);
190 assert_eq!(actual.markdown, expected.markdown);
191 assert_eq!(actual.pdf_pages, expected.pdf_pages);
192 }
193 manager.shutdown().await;
194 drop(_manager);
195 // A signed raw response deliberately has no extraction consumer, even with
196 // the Host flag selected and an unusable runtime/parser.
197 let unavailable = Arc::new(ExtensionHostManager::new(ExtensionHostOptions {
198 runtime: crate::config::ExtensionHostRuntime::Node,
199 node_override: Some(root.path().join("missing-node")),
200 root: Some(root.path().join("absent-host")),
201 ..ExtensionHostOptions::default()
202 }));
203 let _manager = TestManagerGuard::install(unavailable);
204 let missing = root.path().join("missing-parser");
205 let document = extract_fetched_document(
206 Format::Raw,
207 "https://example.com/fixture.pdf",
208 "application/pdf",
209 bytes,
210 true,
211 None,
212 PdfTextCommand::test(missing.as_os_str(), Duration::from_secs(1), None).with_context(&host),
213 )
214 .await
215 .unwrap();
216 assert_eq!(document.kind, DocumentKind::Pdf);
217 assert!(document.text.is_empty());
218 assert!(document.pdf_pages.is_none());
219 }
220
221 #[tokio::test(flavor = "current_thread")]
222 async fn raw_and_non_success_catches_do_not_hide_a_selected_host_failure() {
223 let _home = crate::test_support::SealedHome::new();
224 let root = tempfile::tempdir().unwrap();
225 let manager = std::sync::Arc::new(crate::extension_host::ExtensionHostManager::new(
226 crate::extension_host::ExtensionHostOptions {
227 runtime: crate::config::ExtensionHostRuntime::Node,
228 node_override: Some(root.path().join("missing-node")),
229 root: Some(root.path().join("host")),
230 ..crate::extension_host::ExtensionHostOptions::default()
231 },
232 ));
233 let _manager = crate::extension_host::TestManagerGuard::install(manager);
234 let mut flags = crate::features::Features::with_defaults();
235 flags.enable(crate::features::Feature::WebExtractHost);
236 let context = ToolContext::new(root.path()).with_features(flags);
237 for (format, success) in [(Format::Raw, true), (Format::Text, false)] {
238 let error=extract_fetched_document(format,"https://example.com/private", "text/html",b"<body><main>Five meaningful words survive this valid complete document content.</main></body>",success,None,PdfTextCommand::system(Some(&context))).await.unwrap_err();
239 assert!(
240 !error.content(),
241 "operational Host failure cannot become raw decoded text"
242 );
243 }
244 }
245
245 lines RUST