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,
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}
322
323impl Scorer {
324 pub fn kind(&self) -> &'static str {
331 match self {
332 Scorer::Contains { .. } => "contains",
333 Scorer::NotContains { .. } => "not_contains",
334 Scorer::Regex { .. } => "regex",
335 Scorer::ToolCalled { .. } => "tool_called",
336 Scorer::ToolNotCalled { .. } => "tool_not_called",
337 Scorer::ToolCallCount { .. } => "tool_call_count",
338 Scorer::TurnsWithin { .. } => "turns_within",
339 Scorer::FileContains { .. } => "file_contains",
340 Scorer::JsonSchema { .. } => "json_schema",
341 }
342 }
343}
344
345fn default_weight() -> f64 {
346 1.0
347}
348
349fn default_min_one() -> u32 {
350 1
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
359#[cfg_attr(feature = "openapi", derive(ToSchema))]
360pub struct Score {
361 pub pass: bool,
363 pub value: f64,
365 pub reason: String,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
375#[cfg_attr(feature = "openapi", derive(ToSchema))]
376pub struct RunSummary {
377 pub total: u32,
378 pub passed: u32,
379 pub failed: u32,
380 pub errored: u32,
381 pub pass_rate: f64,
382 pub avg_score: f64,
383 pub avg_turns: f64,
384 pub avg_latency_ms: u64,
385 pub total_input_tokens: u64,
386 pub total_output_tokens: u64,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
395#[cfg_attr(feature = "openapi", derive(ToSchema))]
396pub struct EvalInputMessage {
397 pub content: String,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
407#[cfg_attr(feature = "openapi", derive(ToSchema))]
408pub struct Eval {
409 #[serde(rename = "id")]
411 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "eval_01933b5a000070008000000000000001"))]
412 pub public_id: EvalId,
413 #[serde(skip, default = "Uuid::nil")]
415 pub internal_id: Uuid,
416 #[serde(skip, default)]
418 pub org_id: i64,
419 pub name: String,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub description: Option<String>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub target: Option<EvalTarget>,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub model_override: Option<String>,
430 #[serde(default)]
432 pub tags: Vec<String>,
433 pub status: EvalStatus,
435 #[serde(default)]
437 pub case_count: i64,
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub last_run: Option<EvalRunSummaryView>,
441 pub created_at: DateTime<Utc>,
442 pub updated_at: DateTime<Utc>,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub archived_at: Option<DateTime<Utc>>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub deleted_at: Option<DateTime<Utc>>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451#[cfg_attr(feature = "openapi", derive(ToSchema))]
452pub struct EvalRunSummaryView {
453 #[serde(rename = "id")]
454 #[cfg_attr(feature = "openapi", schema(value_type = String))]
455 pub public_id: EvalRunId,
456 pub status: EvalRunStatus,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub summary: Option<RunSummary>,
459 pub created_at: DateTime<Utc>,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464#[cfg_attr(feature = "openapi", derive(ToSchema))]
465pub struct EvalCase {
466 #[serde(rename = "id")]
468 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evalcase_01933b5a000070008000000000000001"))]
469 pub public_id: EvalCaseId,
470 #[serde(skip, default = "Uuid::nil")]
471 pub internal_id: Uuid,
472 pub name: String,
473 #[serde(skip_serializing_if = "Option::is_none")]
474 pub description: Option<String>,
475 #[serde(skip_serializing_if = "Option::is_none")]
477 pub target: Option<EvalTarget>,
478 #[serde(default)]
479 pub tags: Vec<String>,
480 pub conversation: Vec<EvalInputMessage>,
482 #[serde(skip_serializing_if = "Option::is_none")]
485 pub post: Option<Vec<EvalInputMessage>>,
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub artifacts: Option<Vec<ArtifactSpec>>,
489 pub scorers: Vec<Scorer>,
491 #[serde(skip_serializing_if = "Option::is_none")]
493 pub max_turns: Option<u32>,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub timeout_seconds: Option<u32>,
497 pub position: i32,
499 pub created_at: DateTime<Utc>,
500 pub updated_at: DateTime<Utc>,
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
505#[cfg_attr(feature = "openapi", derive(ToSchema))]
506pub struct EvalRun {
507 #[serde(rename = "id")]
508 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evalrun_01933b5a000070008000000000000001"))]
509 pub public_id: EvalRunId,
510 #[serde(skip, default = "Uuid::nil")]
511 pub internal_id: Uuid,
512 #[serde(skip, default)]
513 pub org_id: i64,
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub target: Option<EvalTarget>,
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub model_override: Option<String>,
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub filter_tags: Option<Vec<String>>,
523 pub status: EvalRunStatus,
524 #[serde(default)]
527 pub source: EvalRunSource,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub attribution: Option<serde_json::Value>,
533 pub triggered_by: String,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub started_at: Option<DateTime<Utc>>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub completed_at: Option<DateTime<Utc>>,
539 #[serde(skip_serializing_if = "Option::is_none")]
541 pub summary: Option<RunSummary>,
542 #[serde(default, skip_serializing_if = "Vec::is_empty")]
544 pub results: Vec<EvalCaseResult>,
545 pub created_at: DateTime<Utc>,
546 pub updated_at: DateTime<Utc>,
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
555#[cfg_attr(feature = "openapi", derive(ToSchema))]
556#[serde(rename_all = "lowercase")]
557pub enum EvalDatasetStatus {
558 Pending,
560 Running,
562 Completed,
564 Failed,
566}
567
568impl std::fmt::Display for EvalDatasetStatus {
569 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570 match self {
571 EvalDatasetStatus::Pending => write!(f, "pending"),
572 EvalDatasetStatus::Running => write!(f, "running"),
573 EvalDatasetStatus::Completed => write!(f, "completed"),
574 EvalDatasetStatus::Failed => write!(f, "failed"),
575 }
576 }
577}
578
579impl From<&str> for EvalDatasetStatus {
580 fn from(s: &str) -> Self {
581 match s {
582 "running" => EvalDatasetStatus::Running,
583 "completed" => EvalDatasetStatus::Completed,
584 "failed" => EvalDatasetStatus::Failed,
585 _ => EvalDatasetStatus::Pending,
586 }
587 }
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
594#[cfg_attr(feature = "openapi", derive(ToSchema))]
595pub struct EvalRunDataset {
596 #[serde(rename = "id")]
597 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evaldataset_01933b5a000070008000000000000001"))]
598 pub public_id: EvalDatasetId,
599 #[cfg_attr(feature = "openapi", schema(value_type = String))]
601 pub eval_run_id: EvalRunId,
602 pub status: EvalDatasetStatus,
603 #[serde(skip_serializing_if = "Option::is_none")]
605 pub record_count: Option<u64>,
606 #[serde(skip_serializing_if = "Option::is_none")]
608 pub error_message: Option<String>,
609 #[serde(skip_serializing_if = "Option::is_none")]
611 pub body: Option<String>,
612 pub created_at: DateTime<Utc>,
613 pub updated_at: DateTime<Utc>,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
618#[cfg_attr(feature = "openapi", derive(ToSchema))]
619pub struct EvalCaseResult {
620 #[serde(rename = "id")]
621 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "evalresult_01933b5a000070008000000000000001"))]
622 pub public_id: EvalResultId,
623 #[serde(skip, default = "Uuid::nil")]
624 pub internal_id: Uuid,
625 #[cfg_attr(feature = "openapi", schema(value_type = String))]
627 pub eval_case_id: EvalCaseId,
628 #[serde(skip_serializing_if = "Option::is_none")]
630 pub case_name: Option<String>,
631 #[serde(skip_serializing_if = "Option::is_none")]
633 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
634 pub session_id: Option<SessionId>,
635 #[serde(skip_serializing_if = "Option::is_none")]
637 pub target: Option<EvalTarget>,
638 #[serde(skip_serializing_if = "Option::is_none")]
640 pub target_snapshot: Option<EvalTarget>,
641 pub status: CaseResultStatus,
642 #[serde(skip_serializing_if = "Option::is_none")]
644 pub scores: Option<serde_json::Value>,
645 #[serde(skip_serializing_if = "Option::is_none")]
647 pub metadata: Option<serde_json::Value>,
648 #[serde(skip_serializing_if = "Option::is_none")]
650 pub turns: Option<u32>,
651 #[serde(skip_serializing_if = "Option::is_none")]
653 pub latency_ms: Option<u64>,
654 #[serde(skip_serializing_if = "Option::is_none")]
656 pub input_tokens: Option<u64>,
657 #[serde(skip_serializing_if = "Option::is_none")]
658 pub output_tokens: Option<u64>,
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub error_message: Option<String>,
662 #[serde(skip_serializing_if = "Option::is_none")]
664 pub artifacts: Option<BTreeMap<String, String>>,
665 pub created_at: DateTime<Utc>,
666 pub updated_at: DateTime<Utc>,
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn test_eval_status_display() {
675 assert_eq!(EvalStatus::Active.to_string(), "active");
676 assert_eq!(EvalStatus::Archived.to_string(), "archived");
677 assert_eq!(EvalStatus::Deleted.to_string(), "deleted");
678 }
679
680 #[test]
681 fn test_eval_status_from_str() {
682 assert_eq!(EvalStatus::from("active"), EvalStatus::Active);
683 assert_eq!(EvalStatus::from("archived"), EvalStatus::Archived);
684 assert_eq!(EvalStatus::from("deleted"), EvalStatus::Deleted);
685 assert_eq!(EvalStatus::from("unknown"), EvalStatus::Active);
686 }
687
688 #[test]
689 fn test_eval_status_serde_roundtrip() {
690 let json = serde_json::to_string(&EvalStatus::Archived).unwrap();
691 assert_eq!(json, r#""archived""#);
692 let parsed: EvalStatus = serde_json::from_str(&json).unwrap();
693 assert_eq!(parsed, EvalStatus::Archived);
694 }
695
696 #[test]
697 fn test_eval_run_status_display() {
698 assert_eq!(EvalRunStatus::Pending.to_string(), "pending");
699 assert_eq!(EvalRunStatus::Running.to_string(), "running");
700 assert_eq!(EvalRunStatus::Completed.to_string(), "completed");
701 assert_eq!(EvalRunStatus::Failed.to_string(), "failed");
702 assert_eq!(EvalRunStatus::Cancelled.to_string(), "cancelled");
703 }
704
705 #[test]
706 fn test_eval_run_status_from_str() {
707 assert_eq!(EvalRunStatus::from("pending"), EvalRunStatus::Pending);
708 assert_eq!(EvalRunStatus::from("running"), EvalRunStatus::Running);
709 assert_eq!(EvalRunStatus::from("completed"), EvalRunStatus::Completed);
710 assert_eq!(EvalRunStatus::from("failed"), EvalRunStatus::Failed);
711 assert_eq!(EvalRunStatus::from("cancelled"), EvalRunStatus::Cancelled);
712 assert_eq!(EvalRunStatus::from("unknown"), EvalRunStatus::Pending);
713 }
714
715 #[test]
716 fn test_case_result_status_display() {
717 assert_eq!(CaseResultStatus::Pending.to_string(), "pending");
718 assert_eq!(CaseResultStatus::Passed.to_string(), "passed");
719 assert_eq!(CaseResultStatus::Failed.to_string(), "failed");
720 assert_eq!(CaseResultStatus::Errored.to_string(), "errored");
721 assert_eq!(CaseResultStatus::Timeout.to_string(), "timeout");
722 }
723
724 #[test]
725 fn test_case_result_status_from_str() {
726 assert_eq!(CaseResultStatus::from("passed"), CaseResultStatus::Passed);
727 assert_eq!(CaseResultStatus::from("failed"), CaseResultStatus::Failed);
728 assert_eq!(CaseResultStatus::from("errored"), CaseResultStatus::Errored);
729 assert_eq!(CaseResultStatus::from("timeout"), CaseResultStatus::Timeout);
730 assert_eq!(CaseResultStatus::from("unknown"), CaseResultStatus::Pending);
731 }
732
733 #[test]
734 fn test_scorer_serde_roundtrip() {
735 let scorer = Scorer::Contains {
736 text: "hello".to_string(),
737 weight: 1.0,
738 };
739 let json = serde_json::to_value(&scorer).unwrap();
740 assert_eq!(json["type"], "contains");
741 assert_eq!(json["text"], "hello");
742 assert_eq!(json["weight"], 1.0);
743
744 let parsed: Scorer = serde_json::from_value(json).unwrap();
745 match parsed {
746 Scorer::Contains { text, weight } => {
747 assert_eq!(text, "hello");
748 assert_eq!(weight, 1.0);
749 }
750 _ => panic!("wrong variant"),
751 }
752 }
753
754 #[test]
755 fn test_scorer_tool_called_defaults() {
756 let json = r#"{"type": "tool_called", "tool": "read_file"}"#;
757 let scorer: Scorer = serde_json::from_str(json).unwrap();
758 match scorer {
759 Scorer::ToolCalled { tool, min, weight } => {
760 assert_eq!(tool, "read_file");
761 assert_eq!(min, 1);
762 assert_eq!(weight, 1.0);
763 }
764 _ => panic!("wrong variant"),
765 }
766 }
767
768 #[test]
769 fn test_score_serde() {
770 let score = Score {
771 pass: true,
772 value: 0.85,
773 reason: "Output contains expected text".to_string(),
774 };
775 let json = serde_json::to_value(&score).unwrap();
776 assert_eq!(json["pass"], true);
777 assert_eq!(json["value"], 0.85);
778 }
779
780 #[test]
781 fn test_run_summary_serde() {
782 let summary = RunSummary {
783 total: 10,
784 passed: 8,
785 failed: 1,
786 errored: 1,
787 pass_rate: 0.8,
788 avg_score: 0.85,
789 avg_turns: 3.5,
790 avg_latency_ms: 2500,
791 total_input_tokens: 50000,
792 total_output_tokens: 10000,
793 };
794 let json = serde_json::to_value(&summary).unwrap();
795 assert_eq!(json["total"], 10);
796 assert_eq!(json["pass_rate"], 0.8);
797 }
798
799 #[test]
800 fn test_eval_input_message_serde() {
801 let msg = EvalInputMessage {
802 content: "What is 2+2?".to_string(),
803 };
804 let json = serde_json::to_value(&msg).unwrap();
805 assert_eq!(json["content"], "What is 2+2?");
806 }
807
808 #[test]
809 fn test_eval_target_session_serde_roundtrip() {
810 let target = EvalTarget::Session {
811 harness_id: Some(HarnessId::from_uuid(Uuid::nil())),
812 harness_name: None,
813 agent_id: Some(AgentId::from_uuid(Uuid::nil())),
814 model_id: Some("gpt-4".to_string()),
815 system_prompt: None,
816 max_iterations: None,
817 };
818 let json = serde_json::to_value(&target).unwrap();
819 assert_eq!(json["type"], "session");
820 assert!(json.get("harness_id").is_some());
821 assert!(json.get("model_id").is_some());
822 assert!(json.get("system_prompt").is_none()); assert!(json.get("harness_name").is_none());
824 assert!(json.get("max_iterations").is_none());
825
826 let parsed: EvalTarget = serde_json::from_value(json).unwrap();
827 assert_eq!(parsed, target);
828 }
829
830 #[test]
831 fn test_eval_target_session_minimal() {
832 let target = EvalTarget::Session {
834 harness_id: None,
835 harness_name: Some("generic".to_string()),
836 agent_id: None,
837 model_id: None,
838 system_prompt: None,
839 max_iterations: None,
840 };
841 let json = serde_json::to_value(&target).unwrap();
842 assert_eq!(json["type"], "session");
843 assert_eq!(json["harness_name"], "generic");
844 assert!(json.get("harness_id").is_none());
845
846 let parsed: EvalTarget = serde_json::from_value(json).unwrap();
847 assert_eq!(parsed, target);
848 }
849
850 #[test]
851 fn test_eval_target_app_variant() {
852 let target = EvalTarget::App {
853 app_id: AppId::from_uuid(Uuid::nil()),
854 };
855 let json = serde_json::to_value(&target).unwrap();
856 assert_eq!(json["type"], "app");
857 assert!(json.get("app_id").is_some());
858
859 let parsed: EvalTarget = serde_json::from_value(json).unwrap();
860 assert_eq!(parsed, target);
861 }
862
863 #[test]
864 fn test_eval_serde_skips_internal_fields() {
865 let eval = Eval {
866 public_id: EvalId::from_uuid(Uuid::nil()),
867 internal_id: Uuid::nil(),
868 org_id: 1,
869 name: "test".into(),
870 description: None,
871 target: Some(EvalTarget::Session {
872 harness_id: Some(HarnessId::from_uuid(Uuid::nil())),
873 harness_name: None,
874 agent_id: Some(AgentId::from_uuid(Uuid::nil())),
875 model_id: None,
876 system_prompt: None,
877 max_iterations: None,
878 }),
879 model_override: None,
880 tags: vec![],
881 status: EvalStatus::Active,
882 case_count: 0,
883 last_run: None,
884 created_at: Utc::now(),
885 updated_at: Utc::now(),
886 archived_at: None,
887 deleted_at: None,
888 };
889 let json = serde_json::to_value(&eval).unwrap();
890 assert!(json.get("id").is_some());
891 assert!(json.get("internal_id").is_none());
892 assert!(json.get("org_id").is_none());
893 assert!(json.get("target").is_some());
894 assert!(json.get("description").is_none());
895 assert!(json.get("model_override").is_none());
896 }
897
898 #[test]
899 fn test_eval_case_artifacts_serde_roundtrip() {
900 let json = serde_json::json!({
901 "id": "evalcase_01933b5a000070008000000000000001",
902 "name": "case",
903 "conversation": [{"content": "hello"}],
904 "artifacts": [{"name": "patch", "path": "/workspace/fix.patch"}],
905 "scorers": [{"type": "contains", "text": "done", "weight": 1.0}],
906 "tags": [],
907 "position": 0,
908 "created_at": "2026-01-01T00:00:00Z",
909 "updated_at": "2026-01-01T00:00:00Z"
910 });
911
912 let case: EvalCase = serde_json::from_value(json.clone()).unwrap();
913 assert_eq!(
914 case.artifacts,
915 Some(vec![ArtifactSpec {
916 name: "patch".to_string(),
917 path: "/workspace/fix.patch".to_string(),
918 }])
919 );
920 assert_eq!(serde_json::to_value(case).unwrap(), json);
921 }
922}