1use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16use std::collections::BTreeMap;
17use uuid::Uuid;
18
19use crate::typed_id::{
20 AgentId, AppId, EvalCaseId, EvalDatasetId, EvalId, EvalResultId, EvalRunId, HarnessId, ModelId,
21 SessionId,
22};
23
24#[cfg(feature = "openapi")]
25use utoipa::ToSchema;
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[cfg_attr(feature = "openapi", derive(ToSchema))]
30pub struct ArtifactSpec {
31 pub name: String,
33 pub path: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49#[cfg_attr(feature = "openapi", derive(ToSchema))]
50#[serde(tag = "type", rename_all = "snake_case")]
51pub enum EvalTarget {
52 Session {
54 #[serde(skip_serializing_if = "Option::is_none")]
56 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
57 harness_id: Option<HarnessId>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 harness_name: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
64 agent_id: Option<AgentId>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 model_id: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 system_prompt: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 max_iterations: Option<usize>,
74 },
75 App {
77 #[cfg_attr(feature = "openapi", schema(value_type = String))]
78 app_id: AppId,
79 },
80 External {
86 provider: String,
87 model: String,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 params: Option<serde_json::Value>,
90 },
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99#[cfg_attr(feature = "openapi", derive(ToSchema))]
100#[serde(rename_all = "lowercase")]
101pub enum EvalStatus {
102 Active,
103 Archived,
104 Deleted,
105}
106
107impl std::fmt::Display for EvalStatus {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 EvalStatus::Active => write!(f, "active"),
111 EvalStatus::Archived => write!(f, "archived"),
112 EvalStatus::Deleted => write!(f, "deleted"),
113 }
114 }
115}
116
117impl From<&str> for EvalStatus {
118 fn from(s: &str) -> Self {
119 match s {
120 "archived" => EvalStatus::Archived,
121 "deleted" => EvalStatus::Deleted,
122 _ => EvalStatus::Active,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[cfg_attr(feature = "openapi", derive(ToSchema))]
134#[serde(rename_all = "lowercase")]
135pub enum EvalRunStatus {
136 Pending,
137 Running,
138 Completed,
139 Failed,
140 Cancelled,
141}
142
143impl std::fmt::Display for EvalRunStatus {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 match self {
146 EvalRunStatus::Pending => write!(f, "pending"),
147 EvalRunStatus::Running => write!(f, "running"),
148 EvalRunStatus::Completed => write!(f, "completed"),
149 EvalRunStatus::Failed => write!(f, "failed"),
150 EvalRunStatus::Cancelled => write!(f, "cancelled"),
151 }
152 }
153}
154
155impl From<&str> for EvalRunStatus {
156 fn from(s: &str) -> Self {
157 match s {
158 "running" => EvalRunStatus::Running,
159 "completed" => EvalRunStatus::Completed,
160 "failed" => EvalRunStatus::Failed,
161 "cancelled" => EvalRunStatus::Cancelled,
162 _ => EvalRunStatus::Pending,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
178#[cfg_attr(feature = "openapi", derive(ToSchema))]
179#[serde(rename_all = "lowercase")]
180pub enum EvalRunSource {
181 #[default]
182 Internal,
183 External,
184}
185
186impl std::fmt::Display for EvalRunSource {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 EvalRunSource::Internal => write!(f, "internal"),
190 EvalRunSource::External => write!(f, "external"),
191 }
192 }
193}
194
195impl From<&str> for EvalRunSource {
196 fn from(s: &str) -> Self {
197 match s {
198 "external" => EvalRunSource::External,
199 _ => EvalRunSource::Internal,
200 }
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
210#[cfg_attr(feature = "openapi", derive(ToSchema))]
211#[serde(rename_all = "lowercase")]
212pub enum CaseResultStatus {
213 Pending,
214 Running,
215 Passed,
216 Failed,
217 Errored,
218 Timeout,
219 Skipped,
222}
223
224impl std::fmt::Display for CaseResultStatus {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 CaseResultStatus::Pending => write!(f, "pending"),
228 CaseResultStatus::Running => write!(f, "running"),
229 CaseResultStatus::Passed => write!(f, "passed"),
230 CaseResultStatus::Failed => write!(f, "failed"),
231 CaseResultStatus::Errored => write!(f, "errored"),
232 CaseResultStatus::Timeout => write!(f, "timeout"),
233 CaseResultStatus::Skipped => write!(f, "skipped"),
234 }
235 }
236}
237
238impl From<&str> for CaseResultStatus {
239 fn from(s: &str) -> Self {
240 match s {
241 "running" => CaseResultStatus::Running,
242 "passed" => CaseResultStatus::Passed,
243 "failed" => CaseResultStatus::Failed,
244 "errored" => CaseResultStatus::Errored,
245 "timeout" => CaseResultStatus::Timeout,
246 "skipped" => CaseResultStatus::Skipped,
247 _ => CaseResultStatus::Pending,
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
258#[cfg_attr(feature = "openapi", derive(ToSchema))]
259#[serde(tag = "type", rename_all = "snake_case")]
260pub enum Scorer {
261 Contains {
263 text: String,
264 #[serde(default = "default_weight")]
265 weight: f64,
266 },
267 NotContains {
269 text: String,
270 #[serde(default = "default_weight")]
271 weight: f64,
272 },
273 Regex {
275 pattern: String,
276 #[serde(default = "default_weight")]
277 weight: f64,
278 },
279 ToolCalled {
281 tool: String,
282 #[serde(default = "default_min_one")]
283 min: u32,
284 #[serde(default = "default_weight")]
285 weight: f64,
286 },
287 ToolNotCalled {
289 tool: String,
290 #[serde(default = "default_weight")]
291 weight: f64,
292 },
293 ToolCallCount {
295 #[serde(skip_serializing_if = "Option::is_none")]
296 min: Option<u32>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 max: Option<u32>,
299 #[serde(default = "default_weight")]
300 weight: f64,
301 },
302 TurnsWithin {
304 max: u32,
305 #[serde(default = "default_weight")]
306 weight: f64,
307 },
308 FileContains {
310 path: String,
311 text: String,
312 #[serde(default = "default_weight")]
313 weight: f64,
314 },
315 JsonSchema {
317 schema: serde_json::Value,
318 #[serde(default = "default_weight")]
319 weight: f64,
320 },
321 CitationFaithful {
326 #[serde(default)]
328 #[cfg_attr(feature = "openapi", schema(example = 1))]
329 min_citations: u32,
330 #[serde(default = "default_pass_threshold")]
332 #[cfg_attr(feature = "openapi", schema(example = 0.8))]
333 pass_threshold: f64,
334 #[serde(default = "default_weight")]
336 #[cfg_attr(feature = "openapi", schema(example = 1.0))]
337 weight: f64,
338 },
339 CitationJudged {
343 #[serde(default, skip_serializing_if = "Option::is_none")]
345 #[cfg_attr(
346 feature = "openapi",
347 schema(example = "Score the fraction of cited claims supported by their source.")
348 )]
349 rubric: Option<String>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
352 model_id: Option<ModelId>,
353 #[serde(default = "default_pass_threshold")]
355 #[cfg_attr(feature = "openapi", schema(example = 0.8))]
356 pass_threshold: f64,
357 #[serde(default = "default_weight")]
359 #[cfg_attr(feature = "openapi", schema(example = 1.0))]
360 weight: f64,
361 },
362}
363
364fn default_pass_threshold() -> f64 {
365 0.8
366}
367
368impl Scorer {
369 pub fn kind(&self) -> &'static str {
376 match self {
377 Scorer::Contains { .. } => "contains",
378 Scorer::NotContains { .. } => "not_contains",
379 Scorer::Regex { .. } => "regex",
380 Scorer::ToolCalled { .. } => "tool_called",
381 Scorer::ToolNotCalled { .. } => "tool_not_called",
382 Scorer::ToolCallCount { .. } => "tool_call_count",
383 Scorer::TurnsWithin { .. } => "turns_within",
384 Scorer::FileContains { .. } => "file_contains",
385 Scorer::JsonSchema { .. } => "json_schema",
386 Scorer::CitationFaithful { .. } => "citation_faithful",
387 Scorer::CitationJudged { .. } => "citation_judged",
388 }
389 }
390}
391
392fn default_weight() -> f64 {
393 1.0
394}
395
396fn default_min_one() -> u32 {
397 1
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
406#[cfg_attr(feature = "openapi", derive(ToSchema))]
407pub struct Score {
408 pub pass: bool,
410 pub value: f64,
412 pub reason: String,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
422#[cfg_attr(feature = "openapi", derive(ToSchema))]
423pub struct RunSummary {
424 pub total: u32,
425 pub passed: u32,
426 pub failed: u32,
427 pub errored: u32,
428 pub pass_rate: f64,
429 pub avg_score: f64,
430 pub avg_turns: f64,
431 pub avg_latency_ms: u64,
432 pub total_input_tokens: u64,
433 pub total_output_tokens: u64,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
442#[cfg_attr(feature = "openapi", derive(ToSchema))]
443pub struct EvalInputMessage {
444 pub content: String,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
454#[cfg_attr(feature = "openapi", derive(ToSchema))]
455pub struct Eval {
456 #[serde(rename = "id")]
458 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "eval_01933b5a000070008000000000000001"))]
459 pub public_id: EvalId,
460 #[serde(skip, default = "Uuid::nil")]
462 pub internal_id: Uuid,
463 #[serde(skip, default)]
465 pub org_id: i64,
466 pub name: String,
468 #[serde(skip_serializing_if = "Option::is_none")]
470 pub description: Option<String>,
471 #[serde(skip_serializing_if = "Option::is_none")]
473 pub target: Option<EvalTarget>,
474 #[serde(skip_serializing_if = "Option::is_none")]
476 pub model_override: Option<String>,
477 #[serde(default)]
479 pub tags: Vec<String>,
480 pub status: EvalStatus,
482 #[serde(default)]
484 pub case_count: i64,
485 #[serde(skip_serializing_if = "Option::is_none")]
487 pub last_run: Option<EvalRunSummaryView>,
488 pub created_at: DateTime<Utc>,
489 pub updated_at: DateTime<Utc>,
490 #[serde(skip_serializing_if = "Option::is_none")]
491 pub archived_at: Option<DateTime<Utc>>,
492 #[serde(skip_serializing_if = "Option::is_none")]
493 pub deleted_at: Option<DateTime<Utc>>,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
498#[cfg_attr(feature = "openapi", derive(ToSchema))]
499pub struct EvalRunSummaryView {
500 #[serde(rename = "id")]
501 #[cfg_attr(feature = "openapi", schema(value_type = String))]
502 pub public_id: EvalRunId,
503 pub status: EvalRunStatus,
504 #[serde(skip_serializing_if = "Option::is_none")]
505 pub summary: Option<RunSummary>,
506 pub created_at: DateTime<Utc>,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
511#[cfg_attr(feature = "openapi", derive(ToSchema))]
512pub struct EvalCase {
513 #[serde(rename = "id")]
515 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evalcase_01933b5a000070008000000000000001"))]
516 pub public_id: EvalCaseId,
517 #[serde(skip, default = "Uuid::nil")]
518 pub internal_id: Uuid,
519 pub name: String,
520 #[serde(skip_serializing_if = "Option::is_none")]
521 pub description: Option<String>,
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub target: Option<EvalTarget>,
525 #[serde(default)]
526 pub tags: Vec<String>,
527 pub conversation: Vec<EvalInputMessage>,
529 #[serde(skip_serializing_if = "Option::is_none")]
532 pub post: Option<Vec<EvalInputMessage>>,
533 #[serde(skip_serializing_if = "Option::is_none")]
535 pub artifacts: Option<Vec<ArtifactSpec>>,
536 pub scorers: Vec<Scorer>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub max_turns: Option<u32>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub timeout_seconds: Option<u32>,
544 pub position: i32,
546 pub created_at: DateTime<Utc>,
547 pub updated_at: DateTime<Utc>,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
552#[cfg_attr(feature = "openapi", derive(ToSchema))]
553pub struct EvalRun {
554 #[serde(rename = "id")]
555 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evalrun_01933b5a000070008000000000000001"))]
556 pub public_id: EvalRunId,
557 #[serde(skip, default = "Uuid::nil")]
558 pub internal_id: Uuid,
559 #[serde(skip, default)]
560 pub org_id: i64,
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub target: Option<EvalTarget>,
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub model_override: Option<String>,
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub filter_tags: Option<Vec<String>>,
570 pub status: EvalRunStatus,
571 #[serde(default)]
574 pub source: EvalRunSource,
575 #[serde(default, skip_serializing_if = "Option::is_none")]
579 pub attribution: Option<serde_json::Value>,
580 pub triggered_by: String,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub started_at: Option<DateTime<Utc>>,
584 #[serde(skip_serializing_if = "Option::is_none")]
585 pub completed_at: Option<DateTime<Utc>>,
586 #[serde(skip_serializing_if = "Option::is_none")]
588 pub summary: Option<RunSummary>,
589 #[serde(default, skip_serializing_if = "Vec::is_empty")]
591 pub results: Vec<EvalCaseResult>,
592 pub created_at: DateTime<Utc>,
593 pub updated_at: DateTime<Utc>,
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
602#[cfg_attr(feature = "openapi", derive(ToSchema))]
603#[serde(rename_all = "lowercase")]
604pub enum EvalDatasetStatus {
605 Pending,
607 Running,
609 Completed,
611 Failed,
613}
614
615impl std::fmt::Display for EvalDatasetStatus {
616 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617 match self {
618 EvalDatasetStatus::Pending => write!(f, "pending"),
619 EvalDatasetStatus::Running => write!(f, "running"),
620 EvalDatasetStatus::Completed => write!(f, "completed"),
621 EvalDatasetStatus::Failed => write!(f, "failed"),
622 }
623 }
624}
625
626impl From<&str> for EvalDatasetStatus {
627 fn from(s: &str) -> Self {
628 match s {
629 "running" => EvalDatasetStatus::Running,
630 "completed" => EvalDatasetStatus::Completed,
631 "failed" => EvalDatasetStatus::Failed,
632 _ => EvalDatasetStatus::Pending,
633 }
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
641#[cfg_attr(feature = "openapi", derive(ToSchema))]
642pub struct EvalRunDataset {
643 #[serde(rename = "id")]
644 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evaldataset_01933b5a000070008000000000000001"))]
645 pub public_id: EvalDatasetId,
646 #[cfg_attr(feature = "openapi", schema(value_type = String))]
648 pub eval_run_id: EvalRunId,
649 pub status: EvalDatasetStatus,
650 #[serde(skip_serializing_if = "Option::is_none")]
652 pub record_count: Option<u64>,
653 #[serde(skip_serializing_if = "Option::is_none")]
655 pub error_message: Option<String>,
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub body: Option<String>,
659 pub created_at: DateTime<Utc>,
660 pub updated_at: DateTime<Utc>,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
665#[cfg_attr(feature = "openapi", derive(ToSchema))]
666pub struct EvalCaseResult {
667 #[serde(rename = "id")]
668 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evalresult_01933b5a000070008000000000000001"))]
669 pub public_id: EvalResultId,
670 #[serde(skip, default = "Uuid::nil")]
671 pub internal_id: Uuid,
672 #[cfg_attr(feature = "openapi", schema(value_type = String))]
674 pub eval_case_id: EvalCaseId,
675 #[serde(skip_serializing_if = "Option::is_none")]
677 pub case_name: Option<String>,
678 #[serde(skip_serializing_if = "Option::is_none")]
680 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
681 pub session_id: Option<SessionId>,
682 #[serde(skip_serializing_if = "Option::is_none")]
684 pub target: Option<EvalTarget>,
685 #[serde(skip_serializing_if = "Option::is_none")]
687 pub target_snapshot: Option<EvalTarget>,
688 pub status: CaseResultStatus,
689 #[serde(skip_serializing_if = "Option::is_none")]
691 pub scores: Option<serde_json::Value>,
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub metadata: Option<serde_json::Value>,
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub turns: Option<u32>,
698 #[serde(skip_serializing_if = "Option::is_none")]
700 pub latency_ms: Option<u64>,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub input_tokens: Option<u64>,
704 #[serde(skip_serializing_if = "Option::is_none")]
705 pub output_tokens: Option<u64>,
706 #[serde(skip_serializing_if = "Option::is_none")]
708 pub error_message: Option<String>,
709 #[serde(skip_serializing_if = "Option::is_none")]
711 pub artifacts: Option<BTreeMap<String, String>>,
712 pub created_at: DateTime<Utc>,
713 pub updated_at: DateTime<Utc>,
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 #[test]
721 fn test_eval_status_display() {
722 assert_eq!(EvalStatus::Active.to_string(), "active");
723 assert_eq!(EvalStatus::Archived.to_string(), "archived");
724 assert_eq!(EvalStatus::Deleted.to_string(), "deleted");
725 }
726
727 #[test]
728 fn test_eval_status_from_str() {
729 assert_eq!(EvalStatus::from("active"), EvalStatus::Active);
730 assert_eq!(EvalStatus::from("archived"), EvalStatus::Archived);
731 assert_eq!(EvalStatus::from("deleted"), EvalStatus::Deleted);
732 assert_eq!(EvalStatus::from("unknown"), EvalStatus::Active);
733 }
734
735 #[test]
736 fn test_eval_status_serde_roundtrip() {
737 let json = serde_json::to_string(&EvalStatus::Archived).unwrap();
738 assert_eq!(json, r#""archived""#);
739 let parsed: EvalStatus = serde_json::from_str(&json).unwrap();
740 assert_eq!(parsed, EvalStatus::Archived);
741 }
742
743 #[test]
744 fn test_eval_run_status_display() {
745 assert_eq!(EvalRunStatus::Pending.to_string(), "pending");
746 assert_eq!(EvalRunStatus::Running.to_string(), "running");
747 assert_eq!(EvalRunStatus::Completed.to_string(), "completed");
748 assert_eq!(EvalRunStatus::Failed.to_string(), "failed");
749 assert_eq!(EvalRunStatus::Cancelled.to_string(), "cancelled");
750 }
751
752 #[test]
753 fn test_eval_run_status_from_str() {
754 assert_eq!(EvalRunStatus::from("pending"), EvalRunStatus::Pending);
755 assert_eq!(EvalRunStatus::from("running"), EvalRunStatus::Running);
756 assert_eq!(EvalRunStatus::from("completed"), EvalRunStatus::Completed);
757 assert_eq!(EvalRunStatus::from("failed"), EvalRunStatus::Failed);
758 assert_eq!(EvalRunStatus::from("cancelled"), EvalRunStatus::Cancelled);
759 assert_eq!(EvalRunStatus::from("unknown"), EvalRunStatus::Pending);
760 }
761
762 #[test]
763 fn test_case_result_status_display() {
764 assert_eq!(CaseResultStatus::Pending.to_string(), "pending");
765 assert_eq!(CaseResultStatus::Passed.to_string(), "passed");
766 assert_eq!(CaseResultStatus::Failed.to_string(), "failed");
767 assert_eq!(CaseResultStatus::Errored.to_string(), "errored");
768 assert_eq!(CaseResultStatus::Timeout.to_string(), "timeout");
769 }
770
771 #[test]
772 fn test_case_result_status_from_str() {
773 assert_eq!(CaseResultStatus::from("passed"), CaseResultStatus::Passed);
774 assert_eq!(CaseResultStatus::from("failed"), CaseResultStatus::Failed);
775 assert_eq!(CaseResultStatus::from("errored"), CaseResultStatus::Errored);
776 assert_eq!(CaseResultStatus::from("timeout"), CaseResultStatus::Timeout);
777 assert_eq!(CaseResultStatus::from("unknown"), CaseResultStatus::Pending);
778 }
779
780 #[test]
781 fn test_scorer_serde_roundtrip() {
782 let scorer = Scorer::Contains {
783 text: "hello".to_string(),
784 weight: 1.0,
785 };
786 let json = serde_json::to_value(&scorer).unwrap();
787 assert_eq!(json["type"], "contains");
788 assert_eq!(json["text"], "hello");
789 assert_eq!(json["weight"], 1.0);
790
791 let parsed: Scorer = serde_json::from_value(json).unwrap();
792 match parsed {
793 Scorer::Contains { text, weight } => {
794 assert_eq!(text, "hello");
795 assert_eq!(weight, 1.0);
796 }
797 _ => panic!("wrong variant"),
798 }
799 }
800
801 #[test]
802 fn test_scorer_tool_called_defaults() {
803 let json = r#"{"type": "tool_called", "tool": "read_file"}"#;
804 let scorer: Scorer = serde_json::from_str(json).unwrap();
805 match scorer {
806 Scorer::ToolCalled { tool, min, weight } => {
807 assert_eq!(tool, "read_file");
808 assert_eq!(min, 1);
809 assert_eq!(weight, 1.0);
810 }
811 _ => panic!("wrong variant"),
812 }
813 }
814
815 #[test]
816 fn test_score_serde() {
817 let score = Score {
818 pass: true,
819 value: 0.85,
820 reason: "Output contains expected text".to_string(),
821 };
822 let json = serde_json::to_value(&score).unwrap();
823 assert_eq!(json["pass"], true);
824 assert_eq!(json["value"], 0.85);
825 }
826
827 #[test]
828 fn test_run_summary_serde() {
829 let summary = RunSummary {
830 total: 10,
831 passed: 8,
832 failed: 1,
833 errored: 1,
834 pass_rate: 0.8,
835 avg_score: 0.85,
836 avg_turns: 3.5,
837 avg_latency_ms: 2500,
838 total_input_tokens: 50000,
839 total_output_tokens: 10000,
840 };
841 let json = serde_json::to_value(&summary).unwrap();
842 assert_eq!(json["total"], 10);
843 assert_eq!(json["pass_rate"], 0.8);
844 }
845
846 #[test]
847 fn test_eval_input_message_serde() {
848 let msg = EvalInputMessage {
849 content: "What is 2+2?".to_string(),
850 };
851 let json = serde_json::to_value(&msg).unwrap();
852 assert_eq!(json["content"], "What is 2+2?");
853 }
854
855 #[test]
856 fn test_eval_target_session_serde_roundtrip() {
857 let target = EvalTarget::Session {
858 harness_id: Some(HarnessId::from_uuid(Uuid::nil())),
859 harness_name: None,
860 agent_id: Some(AgentId::from_uuid(Uuid::nil())),
861 model_id: Some("gpt-4".to_string()),
862 system_prompt: None,
863 max_iterations: None,
864 };
865 let json = serde_json::to_value(&target).unwrap();
866 assert_eq!(json["type"], "session");
867 assert!(json.get("harness_id").is_some());
868 assert!(json.get("model_id").is_some());
869 assert!(json.get("system_prompt").is_none()); assert!(json.get("harness_name").is_none());
871 assert!(json.get("max_iterations").is_none());
872
873 let parsed: EvalTarget = serde_json::from_value(json).unwrap();
874 assert_eq!(parsed, target);
875 }
876
877 #[test]
878 fn test_eval_target_session_minimal() {
879 let target = EvalTarget::Session {
881 harness_id: None,
882 harness_name: Some("generic".to_string()),
883 agent_id: None,
884 model_id: None,
885 system_prompt: None,
886 max_iterations: None,
887 };
888 let json = serde_json::to_value(&target).unwrap();
889 assert_eq!(json["type"], "session");
890 assert_eq!(json["harness_name"], "generic");
891 assert!(json.get("harness_id").is_none());
892
893 let parsed: EvalTarget = serde_json::from_value(json).unwrap();
894 assert_eq!(parsed, target);
895 }
896
897 #[test]
898 fn test_eval_target_app_variant() {
899 let target = EvalTarget::App {
900 app_id: AppId::from_uuid(Uuid::nil()),
901 };
902 let json = serde_json::to_value(&target).unwrap();
903 assert_eq!(json["type"], "app");
904 assert!(json.get("app_id").is_some());
905
906 let parsed: EvalTarget = serde_json::from_value(json).unwrap();
907 assert_eq!(parsed, target);
908 }
909
910 #[test]
911 fn test_eval_serde_skips_internal_fields() {
912 let eval = Eval {
913 public_id: EvalId::from_uuid(Uuid::nil()),
914 internal_id: Uuid::nil(),
915 org_id: 1,
916 name: "test".into(),
917 description: None,
918 target: Some(EvalTarget::Session {
919 harness_id: Some(HarnessId::from_uuid(Uuid::nil())),
920 harness_name: None,
921 agent_id: Some(AgentId::from_uuid(Uuid::nil())),
922 model_id: None,
923 system_prompt: None,
924 max_iterations: None,
925 }),
926 model_override: None,
927 tags: vec![],
928 status: EvalStatus::Active,
929 case_count: 0,
930 last_run: None,
931 created_at: Utc::now(),
932 updated_at: Utc::now(),
933 archived_at: None,
934 deleted_at: None,
935 };
936 let json = serde_json::to_value(&eval).unwrap();
937 assert!(json.get("id").is_some());
938 assert!(json.get("internal_id").is_none());
939 assert!(json.get("org_id").is_none());
940 assert!(json.get("target").is_some());
941 assert!(json.get("description").is_none());
942 assert!(json.get("model_override").is_none());
943 }
944
945 #[test]
946 fn test_eval_case_artifacts_serde_roundtrip() {
947 let json = serde_json::json!({
948 "id": "evalcase_01933b5a000070008000000000000001",
949 "name": "case",
950 "conversation": [{"content": "hello"}],
951 "artifacts": [{"name": "patch", "path": "/workspace/fix.patch"}],
952 "scorers": [{"type": "contains", "text": "done", "weight": 1.0}],
953 "tags": [],
954 "position": 0,
955 "created_at": "2026-01-01T00:00:00Z",
956 "updated_at": "2026-01-01T00:00:00Z"
957 });
958
959 let case: EvalCase = serde_json::from_value(json.clone()).unwrap();
960 assert_eq!(
961 case.artifacts,
962 Some(vec![ArtifactSpec {
963 name: "patch".to_string(),
964 path: "/workspace/fix.patch".to_string(),
965 }])
966 );
967 assert_eq!(serde_json::to_value(case).unwrap(), json);
968 }
969}