1pub mod apex_agents;
8pub mod arc_prize;
9pub mod gdpval;
10pub mod gpqa_diamond;
11pub mod hle;
12pub mod metr;
13pub mod osworld;
14pub mod re_bench;
15pub mod rli;
16pub mod swe_bench;
17
18use agi4_core::evidence::{Evidence, SourceId};
19use parking_lot::Mutex;
20use serde::de::DeserializeOwned;
21use sha2::{Digest, Sha256};
22use std::collections::HashMap;
23use std::error::Error;
24use std::fmt;
25use std::fs;
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28use url::Url;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ModelId(pub String);
33
34impl ModelId {
35 pub fn new(id: impl Into<String>) -> Self {
37 Self(id.into())
38 }
39
40 pub fn as_str(&self) -> &str {
42 &self.0
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum AdapterErrorKind {
49 Parse,
51 Validation,
53}
54
55#[derive(Debug, Clone)]
57pub struct AdapterError {
58 source_id: String,
59 kind: AdapterErrorKind,
60 message: String,
61}
62
63impl AdapterError {
64 pub fn new(source_id: impl Into<String>, message: impl Into<String>) -> Self {
66 Self::with_kind(source_id, AdapterErrorKind::Validation, message)
67 }
68
69 pub fn parse(source_id: impl Into<String>, message: impl Into<String>) -> Self {
71 Self::with_kind(source_id, AdapterErrorKind::Parse, message)
72 }
73
74 pub fn validation(source_id: impl Into<String>, message: impl Into<String>) -> Self {
76 Self::with_kind(source_id, AdapterErrorKind::Validation, message)
77 }
78
79 pub fn with_kind(
81 source_id: impl Into<String>,
82 kind: AdapterErrorKind,
83 message: impl Into<String>,
84 ) -> Self {
85 Self {
86 source_id: source_id.into(),
87 kind,
88 message: message.into(),
89 }
90 }
91
92 pub fn message(&self) -> &str {
94 &self.message
95 }
96
97 pub fn kind(&self) -> AdapterErrorKind {
99 self.kind
100 }
101}
102
103impl fmt::Display for AdapterError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "adapter error [{}]: {}", self.source_id, self.message)
106 }
107}
108
109impl Error for AdapterError {}
110
111pub trait Source {
116 type Raw: DeserializeOwned;
118
119 type Error: Error + Send + Sync + 'static;
121
122 fn id(&self) -> SourceId;
124
125 fn endpoint(&self) -> &Url;
127
128 fn parse(&self, raw: &str) -> Result<Self::Raw, Self::Error>;
131
132 fn to_evidence(&self, raw: Self::Raw, model: &ModelId) -> Result<Vec<Evidence>, Self::Error>;
136}
137
138pub trait Fetcher {
143 type Error: Error + Send + Sync + 'static;
145
146 fn fetch(&self, url: &Url) -> Result<String, Self::Error>;
148}
149
150#[derive(Debug, Clone)]
155pub struct InMemoryFetcher {
156 data: HashMap<String, String>,
157}
158
159impl InMemoryFetcher {
160 pub fn new() -> Self {
162 Self {
163 data: HashMap::new(),
164 }
165 }
166
167 pub fn insert(&mut self, url: impl Into<String>, data: impl Into<String>) {
169 self.data.insert(url.into(), data.into());
170 }
171
172 pub fn with_data(mut self, entries: Vec<(String, String)>) -> Self {
174 for (url, data) in entries {
175 self.data.insert(url, data);
176 }
177 self
178 }
179}
180
181impl Default for InMemoryFetcher {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct InMemoryFetcherError {
190 url: String,
191}
192
193impl fmt::Display for InMemoryFetcherError {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 write!(f, "fixture not found for URL: {}", self.url)
196 }
197}
198
199impl Error for InMemoryFetcherError {}
200
201impl Fetcher for InMemoryFetcher {
202 type Error = InMemoryFetcherError;
203
204 fn fetch(&self, url: &Url) -> Result<String, Self::Error> {
205 self.data
206 .get(url.as_str())
207 .cloned()
208 .ok_or_else(|| InMemoryFetcherError {
209 url: url.to_string(),
210 })
211 }
212}
213
214#[derive(Clone)]
216pub struct HttpFetcher {
217 timeout_secs: u64,
219 max_retries: u32,
221}
222
223#[derive(Debug, Clone)]
225pub struct HttpFetcherError {
226 url: String,
227 message: String,
228}
229
230impl fmt::Display for HttpFetcherError {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 write!(f, "HTTP fetch failed for {}: {}", self.url, self.message)
233 }
234}
235
236impl Error for HttpFetcherError {}
237
238impl HttpFetcher {
239 pub fn new() -> Self {
241 Self {
242 timeout_secs: 30,
243 max_retries: 3,
244 }
245 }
246
247 pub fn with_config(timeout_secs: u64, max_retries: u32) -> Self {
249 Self {
250 timeout_secs,
251 max_retries,
252 }
253 }
254}
255
256impl Default for HttpFetcher {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262impl Fetcher for HttpFetcher {
263 type Error = HttpFetcherError;
264
265 fn fetch(&self, url: &Url) -> Result<String, Self::Error> {
266 let client = reqwest::blocking::Client::builder()
267 .timeout(std::time::Duration::from_secs(self.timeout_secs))
268 .build()
269 .map_err(|e| HttpFetcherError {
270 url: url.to_string(),
271 message: format!("failed to create HTTP client: {}", e),
272 })?;
273
274 for attempt in 0..=self.max_retries {
275 match client.get(url.as_str()).send() {
276 Ok(response) => {
277 return response.text().map_err(|e| HttpFetcherError {
278 url: url.to_string(),
279 message: format!("failed to read response: {}", e),
280 });
281 }
282 Err(e) => {
283 if attempt < self.max_retries {
284 let backoff_ms = 100u64 * (2u64.pow(attempt));
285 std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
286 continue;
287 }
288 return Err(HttpFetcherError {
289 url: url.to_string(),
290 message: format!(
291 "request failed after {} retries: {}",
292 self.max_retries, e
293 ),
294 });
295 }
296 }
297 }
298
299 Err(HttpFetcherError {
300 url: url.to_string(),
301 message: "all retries exhausted".to_string(),
302 })
303 }
304}
305
306#[derive(Debug, Clone)]
308pub struct CachingFetcherError {
309 url: String,
310 message: String,
311}
312
313impl fmt::Display for CachingFetcherError {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 write!(f, "caching fetch failed for {}: {}", self.url, self.message)
316 }
317}
318
319impl Error for CachingFetcherError {}
320
321pub struct CachingFetcher {
328 http_fetcher: HttpFetcher,
329 cache_dir: PathBuf,
330 cache_ttl_secs: u64,
331 url_locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
334}
335
336impl CachingFetcher {
337 pub fn new() -> Result<Self, CachingFetcherError> {
340 let cache_dir =
341 dirs::cache_dir()
342 .map(|d| d.join("agi4"))
343 .ok_or_else(|| CachingFetcherError {
344 url: "cache_dir".to_string(),
345 message: "could not determine cache directory".to_string(),
346 })?;
347
348 fs::create_dir_all(&cache_dir).map_err(|e| CachingFetcherError {
349 url: "cache_dir".to_string(),
350 message: format!("failed to create cache directory: {}", e),
351 })?;
352
353 Ok(Self {
354 http_fetcher: HttpFetcher::new(),
355 cache_dir,
356 cache_ttl_secs: 86400, url_locks: Mutex::new(HashMap::new()),
358 })
359 }
360
361 pub fn with_config(
363 http_fetcher: HttpFetcher,
364 cache_dir: PathBuf,
365 cache_ttl_secs: u64,
366 ) -> Result<Self, CachingFetcherError> {
367 fs::create_dir_all(&cache_dir).map_err(|e| CachingFetcherError {
368 url: "cache_dir".to_string(),
369 message: format!("failed to create cache directory: {}", e),
370 })?;
371
372 Ok(Self {
373 http_fetcher,
374 cache_dir,
375 cache_ttl_secs,
376 url_locks: Mutex::new(HashMap::new()),
377 })
378 }
379
380 fn cache_path(&self, url: &Url) -> PathBuf {
382 let mut hasher = Sha256::new();
383 hasher.update(url.as_str().as_bytes());
384 let hash = format!("{:x}", hasher.finalize());
385 self.cache_dir.join(hash)
386 }
387
388 fn is_cache_valid(&self, cache_path: &Path) -> bool {
390 if !cache_path.exists() {
391 return false;
392 }
393
394 if let Ok(metadata) = fs::metadata(cache_path)
395 && let Ok(modified) = metadata.modified()
396 && let Ok(elapsed) = modified.elapsed()
397 {
398 return elapsed.as_secs() < self.cache_ttl_secs;
399 }
400
401 false
402 }
403
404 fn read_cache(&self, cache_path: &Path) -> Option<String> {
406 if self.is_cache_valid(cache_path) {
407 fs::read_to_string(cache_path).ok()
408 } else {
409 None
410 }
411 }
412
413 fn write_cache(&self, cache_path: &Path, data: &str) {
416 let temp_path = {
418 let mut temp = cache_path.to_path_buf();
419 let filename = match temp.file_name() {
420 Some(name) => name.to_string_lossy().to_string(),
421 None => {
422 return;
424 }
425 };
426 temp.pop();
427 temp.push(format!(".{}.tmp", filename));
428 temp
429 };
430
431 if fs::write(&temp_path, data).is_ok() {
433 let _ = fs::rename(&temp_path, cache_path);
434 }
435 }
436
437 fn get_url_lock(&self, url: &Url) -> Arc<Mutex<()>> {
439 let url_hash = {
440 let mut hasher = Sha256::new();
441 hasher.update(url.as_str().as_bytes());
442 format!("{:x}", hasher.finalize())
443 };
444
445 let mut locks = self.url_locks.lock();
446 locks
447 .entry(url_hash)
448 .or_insert_with(|| Arc::new(Mutex::new(())))
449 .clone()
450 }
451}
452
453impl Fetcher for CachingFetcher {
454 type Error = CachingFetcherError;
455
456 fn fetch(&self, url: &Url) -> Result<String, Self::Error> {
457 let cache_path = self.cache_path(url);
458
459 if let Some(data) = self.read_cache(&cache_path) {
461 return Ok(data);
462 }
463
464 let url_lock = self.get_url_lock(url);
467 let _lock_guard = url_lock.lock();
468
469 if let Some(data) = self.read_cache(&cache_path) {
471 return Ok(data);
472 }
473
474 let data = self
476 .http_fetcher
477 .fetch(url)
478 .map_err(|e| CachingFetcherError {
479 url: url.to_string(),
480 message: format!("HTTP fetch failed: {}", e),
481 })?;
482
483 self.write_cache(&cache_path, &data);
484 Ok(data)
485 }
486}
487
488pub use apex_agents::ApexAgentsAdapter;
490pub use arc_prize::ArcPrizeAdapter;
491pub use gdpval::GdpvalAdapter;
492pub use gpqa_diamond::GpqaDiamondAdapter;
493pub use hle::HleAdapter;
494pub use metr::MetrAdapter;
495pub use osworld::OsworldAdapter;
496pub use re_bench::ReBenchAdapter;
497pub use rli::RliAdapter;
498pub use swe_bench::SweBenchAdapter;
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use std::time::SystemTime;
504
505 #[test]
506 fn model_id_new_and_as_str() {
507 let model = ModelId::new("example-model-v1");
508 assert_eq!(model.as_str(), "example-model-v1");
509 }
510
511 #[test]
512 fn model_id_from_string() {
513 let s = "test-model".to_string();
514 let model = ModelId::new(s);
515 assert_eq!(model.as_str(), "test-model");
516 }
517
518 #[test]
519 fn model_id_equality() {
520 let m1 = ModelId::new("model-a");
521 let m2 = ModelId::new("model-a");
522 let m3 = ModelId::new("model-b");
523 assert_eq!(m1, m2);
524 assert_ne!(m1, m3);
525 }
526
527 #[test]
528 fn adapter_error_new_and_message() {
529 let err = AdapterError::new("arc-agi-3", "failed to parse JSON");
530 assert_eq!(err.message(), "failed to parse JSON");
531 assert!(err.to_string().contains("arc-agi-3"));
532 }
533
534 #[test]
535 fn adapter_error_display() {
536 let err = AdapterError::new("metr", "network timeout");
537 let display_str = err.to_string();
538 assert!(display_str.contains("metr"));
539 assert!(display_str.contains("network timeout"));
540 }
541
542 #[test]
543 fn in_memory_fetcher_new() {
544 let fetcher = InMemoryFetcher::new();
545 assert!(matches!(
546 fetcher.fetch(&Url::parse("http://example.com").unwrap()),
547 Err(InMemoryFetcherError { .. })
548 ));
549 }
550
551 #[test]
552 fn in_memory_fetcher_default() {
553 let fetcher = InMemoryFetcher::default();
554 assert!(matches!(
555 fetcher.fetch(&Url::parse("http://example.com").unwrap()),
556 Err(InMemoryFetcherError { .. })
557 ));
558 }
559
560 #[test]
561 fn in_memory_fetcher_insert_and_fetch() {
562 let mut fetcher = InMemoryFetcher::new();
563 let url = "http://example.com/data.json";
564 let data = r#"{"value": 42}"#;
565 fetcher.insert(url, data);
566
567 let result = fetcher
568 .fetch(&Url::parse(url).unwrap())
569 .expect("should fetch inserted data");
570 assert_eq!(result, data);
571 }
572
573 #[test]
574 fn in_memory_fetcher_with_data() {
575 let entries = vec![
576 ("http://arc.org/data".to_string(), "arc data".to_string()),
577 ("http://metr.org/data".to_string(), "metr data".to_string()),
578 ];
579 let fetcher = InMemoryFetcher::new().with_data(entries);
580
581 let url1 = Url::parse("http://arc.org/data").unwrap();
582 let result1 = fetcher.fetch(&url1).expect("should fetch arc data");
583 assert_eq!(result1, "arc data");
584
585 let url2 = Url::parse("http://metr.org/data").unwrap();
586 let result2 = fetcher.fetch(&url2).expect("should fetch metr data");
587 assert_eq!(result2, "metr data");
588 }
589
590 #[test]
591 fn in_memory_fetcher_missing_url_error() {
592 let fetcher = InMemoryFetcher::new();
593 let url = Url::parse("http://nonexistent.com/data").unwrap();
594 let err = fetcher
595 .fetch(&url)
596 .expect_err("should error for missing URL");
597 assert!(err.to_string().contains("nonexistent.com"));
598 }
599
600 #[test]
601 fn in_memory_fetcher_clone() {
602 let mut fetcher1 = InMemoryFetcher::new();
603 fetcher1.insert("http://test.com/data", "test data");
604
605 let fetcher2 = fetcher1.clone();
606 let url = Url::parse("http://test.com/data").unwrap();
607 let result = fetcher2.fetch(&url).expect("clone should have data");
608 assert_eq!(result, "test data");
609 }
610
611 #[test]
612 fn in_memory_fetcher_multiple_urls() {
613 let mut fetcher = InMemoryFetcher::new();
614 fetcher.insert("http://source-a.com/api", "data-a");
615 fetcher.insert("http://source-b.com/api", "data-b");
616 fetcher.insert("http://source-c.com/api", "data-c");
617
618 let url_a = Url::parse("http://source-a.com/api").unwrap();
619 assert_eq!(fetcher.fetch(&url_a).unwrap(), "data-a");
620
621 let url_b = Url::parse("http://source-b.com/api").unwrap();
622 assert_eq!(fetcher.fetch(&url_b).unwrap(), "data-b");
623
624 let url_c = Url::parse("http://source-c.com/api").unwrap();
625 assert_eq!(fetcher.fetch(&url_c).unwrap(), "data-c");
626 }
627
628 #[test]
629 fn in_memory_fetcher_error_is_send_sync() {
630 fn assert_send_sync<T: Send + Sync>() {}
631 assert_send_sync::<InMemoryFetcherError>();
632 }
633
634 #[test]
635 fn in_memory_fetcher_is_send_sync() {
636 fn assert_send_sync<T: Send + Sync>() {}
637 assert_send_sync::<InMemoryFetcher>();
638 }
639
640 #[test]
641 fn http_fetcher_new() {
642 let fetcher = HttpFetcher::new();
643 assert_eq!(fetcher.timeout_secs, 30);
644 assert_eq!(fetcher.max_retries, 3);
645 }
646
647 #[test]
648 fn http_fetcher_default() {
649 let fetcher = HttpFetcher::default();
650 assert_eq!(fetcher.timeout_secs, 30);
651 assert_eq!(fetcher.max_retries, 3);
652 }
653
654 #[test]
655 fn http_fetcher_with_config() {
656 let fetcher = HttpFetcher::with_config(60, 5);
657 assert_eq!(fetcher.timeout_secs, 60);
658 assert_eq!(fetcher.max_retries, 5);
659 }
660
661 #[test]
662 fn http_fetcher_error_display() {
663 let err = HttpFetcherError {
664 url: "https://example.com".to_string(),
665 message: "connection refused".to_string(),
666 };
667 assert!(err.to_string().contains("example.com"));
668 assert!(err.to_string().contains("connection refused"));
669 }
670
671 #[test]
672 fn http_fetcher_invalid_url() {
673 let fetcher = HttpFetcher::new();
674 let invalid_url =
675 Url::parse("https://invalid-nonexistent-domain-12345.local").expect("URL should parse");
676 let result = fetcher.fetch(&invalid_url);
677 assert!(result.is_err());
678 }
679
680 #[test]
681 fn http_fetcher_is_send_sync() {
682 fn assert_send_sync<T: Send + Sync>() {}
683 assert_send_sync::<HttpFetcher>();
684 }
685
686 #[test]
687 fn caching_fetcher_new() {
688 let fetcher = CachingFetcher::new();
689 assert!(fetcher.is_ok());
690 let cf = fetcher.unwrap();
691 assert!(cf.cache_dir.ends_with("agi4"));
692 }
693
694 #[test]
695 fn caching_fetcher_default() {
696 let fetcher = CachingFetcher::new().expect("create fetcher");
697 assert!(fetcher.cache_dir.ends_with("agi4"));
698 assert_eq!(fetcher.cache_ttl_secs, 86400);
699 }
700
701 #[test]
702 fn caching_fetcher_cache_path() {
703 let fetcher = CachingFetcher::new().expect("create fetcher");
704 let url1 = Url::parse("http://example.com/data").unwrap();
705 let url2 = Url::parse("http://example.com/data").unwrap();
706 let url3 = Url::parse("http://other.com/data").unwrap();
707
708 let path1 = fetcher.cache_path(&url1);
709 let path2 = fetcher.cache_path(&url2);
710 let path3 = fetcher.cache_path(&url3);
711
712 assert_eq!(path1, path2);
714 assert_ne!(path1, path3);
716 }
717
718 #[test]
719 fn caching_fetcher_is_cache_valid_missing() {
720 let fetcher = CachingFetcher::new().expect("create fetcher");
721 let nonexistent = fetcher.cache_dir.join("nonexistent-cache-entry");
722 assert!(!fetcher.is_cache_valid(&nonexistent));
723 }
724
725 #[test]
726 fn caching_fetcher_is_cache_valid_expired() {
727 let fetcher = CachingFetcher::new().expect("create fetcher");
728 let temp_cache = fetcher.cache_dir.join("temp-cache-entry");
729
730 fs::write(&temp_cache, "cached data").expect("write cache");
732
733 let past = SystemTime::now() - std::time::Duration::from_secs(200000);
735 filetime::set_file_mtime(&temp_cache, past.into()).expect("set mtime");
736
737 assert!(!fetcher.is_cache_valid(&temp_cache));
739
740 let _ = fs::remove_file(temp_cache);
742 }
743
744 #[test]
745 fn caching_fetcher_read_write_cache() {
746 let fetcher = CachingFetcher::new().expect("create fetcher");
747 let test_cache = fetcher.cache_dir.join("test-read-write");
748
749 let test_data = "test cached content";
750 fetcher.write_cache(&test_cache, test_data);
751
752 let read_data = fetcher.read_cache(&test_cache);
753 assert_eq!(read_data, Some(test_data.to_string()));
754
755 let _ = fs::remove_file(test_cache);
757 }
758
759 #[test]
760 fn caching_fetcher_read_cache_invalid() {
761 let fetcher = CachingFetcher::new().expect("create fetcher");
762 let expired_cache = fetcher.cache_dir.join("expired-cache");
763
764 fs::write(&expired_cache, "old data").expect("write cache");
766 let past = SystemTime::now() - std::time::Duration::from_secs(200000);
767 filetime::set_file_mtime(&expired_cache, past.into()).expect("set mtime");
768
769 let data = fetcher.read_cache(&expired_cache);
771 assert_eq!(data, None);
772
773 let _ = fs::remove_file(expired_cache);
775 }
776
777 #[test]
778 fn caching_fetcher_with_config() {
779 let temp_dir = std::env::temp_dir().join("agi4-test-cache");
780 let http_fetcher = HttpFetcher::with_config(60, 5);
781
782 let result = CachingFetcher::with_config(http_fetcher, temp_dir.clone(), 3600);
783 assert!(result.is_ok());
784
785 let cf = result.unwrap();
786 assert_eq!(cf.cache_ttl_secs, 3600);
787
788 let _ = fs::remove_dir_all(&temp_dir);
790 }
791
792 #[test]
793 fn caching_fetcher_in_memory_fetch_with_mock() {
794 let mut in_memory = InMemoryFetcher::new();
796 in_memory.insert("http://test.local/api", r#"{"test": "data"}"#);
797
798 let url = Url::parse("http://test.local/api").unwrap();
799 let result = in_memory.fetch(&url);
800 assert_eq!(result.unwrap(), r#"{"test": "data"}"#);
801 }
802
803 #[test]
804 fn caching_fetcher_error_display() {
805 let err = CachingFetcherError {
806 url: "https://example.com".to_string(),
807 message: "cache write failed".to_string(),
808 };
809 let display = err.to_string();
810 assert!(display.contains("example.com"));
811 assert!(display.contains("cache write failed"));
812 }
813
814 #[test]
815 fn caching_fetcher_is_send_sync() {
816 fn assert_send_sync<T: Send + Sync>() {}
817 assert_send_sync::<CachingFetcher>();
818 }
819
820 #[test]
821 fn swe_bench_uses_canonical_source_id() {
822 let adapter =
825 swe_bench::SweBenchAdapter::new().expect("SWE-bench adapter should initialize");
826 assert_eq!(
827 adapter.id().as_str(),
828 "swe-bench-verified",
829 "SWE-bench adapter must return canonical 'swe-bench-verified' ID"
830 );
831 }
832
833 #[test]
834 fn caching_fetcher_concurrent_access_no_panic() {
835 use std::sync::Arc as StdArc;
836 use std::thread;
837
838 let temp_dir = std::env::temp_dir().join("agi4-test-concurrent");
841 let _ = fs::remove_dir_all(&temp_dir);
842 fs::create_dir_all(&temp_dir).unwrap();
843
844 let cache_fetcher = CachingFetcher::new().expect("should create cache fetcher");
845 let cache_fetcher_arc = StdArc::new(cache_fetcher);
846
847 let mut handles = vec![];
849 for _ in 0..5 {
850 let cf = cache_fetcher_arc.clone();
851 let handle = thread::spawn(move || {
852 let url = Url::parse("http://test.example.com/data").unwrap();
853 let _lock = cf.get_url_lock(&url);
855 true
857 });
858 handles.push(handle);
859 }
860
861 for handle in handles {
863 let result = handle.join().expect("thread should not panic");
864 assert!(result, "lock acquisition should succeed");
865 }
866
867 let _ = fs::remove_dir_all(&temp_dir);
868 }
869
870 #[test]
871 fn caching_fetcher_atomic_writes_no_tmp_files() {
872 let temp_dir = std::env::temp_dir().join("agi4-test-atomic-write");
874 let _ = fs::remove_dir_all(&temp_dir);
875 fs::create_dir_all(&temp_dir).unwrap();
876
877 let cache_fetcher = CachingFetcher::new().expect("should create cache fetcher");
878 let test_path = temp_dir.join("test-cache-file");
879
880 cache_fetcher.write_cache(&test_path, "test data");
882
883 assert!(
886 test_path.exists(),
887 "cache file should exist after write_cache"
888 );
889
890 let content = fs::read_to_string(&test_path).expect("should read cache");
892 assert_eq!(content, "test data", "cache should contain correct data");
893
894 let tmp_path = temp_dir.join(".test-cache-file.tmp");
896 assert!(
897 !tmp_path.exists(),
898 "temp file should not remain after atomic write"
899 );
900
901 let _ = fs::remove_dir_all(&temp_dir);
902 }
903
904 #[test]
905 fn adapter_raw_structs_deny_unknown_fields() {
906 let valid_metr = r#"{"value": 168.0}"#;
912 assert!(
913 serde_json::from_str::<metr::MetrRaw>(valid_metr).is_ok(),
914 "valid METR data should parse"
915 );
916
917 let invalid_metr = r#"{"value": 168.0, "unknown_field": "should_fail"}"#;
918 assert!(
919 serde_json::from_str::<metr::MetrRaw>(invalid_metr).is_err(),
920 "METR data with unknown field should be rejected"
921 );
922
923 let valid_arc = r#"{"arc_agi_2": {"pass_rate": 0.85}, "arc_agi_3": {"pass_rate": 0.90}}"#;
925 assert!(
926 serde_json::from_str::<arc_prize::ArcPrizeRaw>(valid_arc).is_ok(),
927 "valid ARC Prize data should parse"
928 );
929
930 let invalid_arc_top = r#"{"arc_agi_2": {"pass_rate": 0.85}, "arc_agi_3": {"pass_rate": 0.90}, "extra_field": "fail"}"#;
931 assert!(
932 serde_json::from_str::<arc_prize::ArcPrizeRaw>(invalid_arc_top).is_err(),
933 "ARC Prize data with unknown top-level field should be rejected"
934 );
935
936 let invalid_arc_nested = r#"{"arc_agi_2": {"pass_rate": 0.85, "extra": "fail"}, "arc_agi_3": {"pass_rate": 0.90}}"#;
937 assert!(
938 serde_json::from_str::<arc_prize::ArcPrizeRaw>(invalid_arc_nested).is_err(),
939 "ARC Prize data with unknown nested field should be rejected"
940 );
941
942 let valid_hle = r#"{"overall_accuracy": 0.75}"#;
944 assert!(
945 serde_json::from_str::<hle::HleRaw>(valid_hle).is_ok(),
946 "valid HLE data should parse"
947 );
948
949 let invalid_hle = r#"{"overall_accuracy": 0.75, "noise": "fail"}"#;
950 assert!(
951 serde_json::from_str::<hle::HleRaw>(invalid_hle).is_err(),
952 "HLE data with unknown field should be rejected"
953 );
954 }
955}