1use crate::events::{DraftBotEvent, EventMetadata, GameEvent};
29use crate::log::entry::LogEntry;
30use crate::parsers::api_common;
31
32const DRAFT_STATUS_MARKER: &str = "BotDraftDraftStatus";
36
37const DRAFT_PICK_MARKER: &str = "BotDraftDraftPick";
42
43pub fn try_parse(
55 entry: &LogEntry,
56 timestamp: Option<chrono::DateTime<chrono::Utc>>,
57) -> Option<GameEvent> {
58 let body = &entry.body;
59
60 if let Some(payload) = try_parse_pack_presentation(body) {
62 let metadata = EventMetadata::new(timestamp, body.as_bytes().to_vec());
63 return Some(GameEvent::DraftBot(DraftBotEvent::new(metadata, payload)));
64 }
65
66 if let Some(payload) = try_parse_draft_pick(body) {
68 let metadata = EventMetadata::new(timestamp, body.as_bytes().to_vec());
69 return Some(GameEvent::DraftBot(DraftBotEvent::new(metadata, payload)));
70 }
71
72 None
73}
74
75fn try_parse_pack_presentation(body: &str) -> Option<serde_json::Value> {
91 let parsed = if api_common::is_api_response(body, DRAFT_STATUS_MARKER) {
92 let top = api_common::parse_json_from_body(body, DRAFT_STATUS_MARKER)?;
93 api_common::parse_nested_json(&top, "Payload", Some(DRAFT_STATUS_MARKER))
94 } else if api_common::is_api_response(body, DRAFT_PICK_MARKER) {
95 let top = api_common::parse_json_from_body(body, DRAFT_PICK_MARKER)?;
96 api_common::parse_nested_json(&top, "Payload", Some(DRAFT_PICK_MARKER))
97 } else {
98 None
99 }?;
100
101 let status_val = parsed.get("DraftStatus")?;
103 if status_val.as_str() != Some("PickNext") {
104 return None;
105 }
106
107 let pack_idx = parsed
108 .get("PackNumber")
109 .and_then(serde_json::Value::as_i64)
110 .unwrap_or(0);
111
112 let selection_idx = parsed
113 .get("PickNumber")
114 .and_then(serde_json::Value::as_i64)
115 .unwrap_or(0);
116
117 let event_name = api_common::extract_event_name(&parsed);
118
119 let draft_pack = extract_draft_pack(&parsed);
121
122 Some(serde_json::json!({
123 "type": "draft_bot_pack",
124 "event_name": event_name,
125 "pack_number": pack_idx,
126 "pick_number": selection_idx,
127 "draft_pack": draft_pack,
128 "raw_draft_status": parsed,
129 }))
130}
131
132fn try_parse_draft_pick(body: &str) -> Option<serde_json::Value> {
147 let parsed = if api_common::is_api_request(body, DRAFT_PICK_MARKER) {
148 let top = api_common::parse_json_from_body(body, DRAFT_PICK_MARKER)?;
149 api_common::parse_nested_json(&top, "request", Some(DRAFT_PICK_MARKER))
150 } else {
151 None
152 }?;
153
154 let pick_info = parsed.get("PickInfo")?;
155
156 if pick_info.get("CardIds").is_none()
158 && pick_info.get("PackNumber").is_none()
159 && pick_info.get("PickNumber").is_none()
160 {
161 return None;
162 }
163
164 let pack_idx = pick_info
165 .get("PackNumber")
166 .and_then(serde_json::Value::as_i64)
167 .unwrap_or(0);
168
169 let selection_idx = pick_info
170 .get("PickNumber")
171 .and_then(serde_json::Value::as_i64)
172 .unwrap_or(0);
173
174 let card_ids: Vec<i64> = pick_info
175 .get("CardIds")
176 .and_then(|v| v.as_array())
177 .map(|arr| {
178 arr.iter()
179 .filter_map(|v| {
180 v.as_i64()
181 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
182 })
183 .collect()
184 })
185 .unwrap_or_default();
186
187 let card_id = *card_ids.first()?;
188 if card_id == 0 {
189 return None;
190 }
191
192 let event_name = api_common::extract_event_name(&parsed);
193
194 Some(serde_json::json!({
195 "type": "draft_bot_pick",
196 "event_name": event_name,
197 "card_id": card_id,
198 "pack_number": pack_idx,
199 "pick_number": selection_idx,
200 "raw_pick_info": parsed,
201 }))
202}
203
204fn extract_draft_pack(parsed: &serde_json::Value) -> Vec<i64> {
209 let Some(pack) = parsed.get("DraftPack").and_then(|v| v.as_array()) else {
210 return Vec::new();
211 };
212
213 pack.iter()
214 .filter_map(|v| {
215 v.as_i64()
217 .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
218 })
219 .collect()
220}
221
222#[cfg(test)]
227#[allow(deprecated)]
228mod tests {
229 use super::*;
230 use crate::events::PerformanceClass;
231 use crate::parsers::test_helpers::{
232 draft_bot_payload, test_timestamp, unity_entry, EntryHeader,
233 };
234
235 mod pack_presentation {
238 use super::*;
239
240 #[test]
241 fn test_try_parse_pack_presentation_basic() {
242 let body = "[UnityCrossThreadLogger]2/01/2026 10:23:51 AM\n\
243 <== BotDraftDraftStatus(uuid)\n\
244 {\"Payload\":\"{\\\"EventName\\\":\\\"QuickDraft_MKM_20260201\\\",\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0,\\\"DraftPack\\\":[\\\"12345\\\",\\\"67890\\\",\\\"11111\\\"]}\"}";
245 let entry = unity_entry(body);
246 let result = try_parse(&entry, Some(test_timestamp()));
247
248 assert!(result.is_some());
249 let event = result.as_ref().unwrap_or_else(|| unreachable!());
250 let payload = draft_bot_payload(event);
251
252 assert_eq!(payload["type"], "draft_bot_pack");
253 assert_eq!(payload["event_name"], "QuickDraft_MKM_20260201");
254 assert_eq!(payload["pack_number"], 0);
255 assert_eq!(payload["pick_number"], 0);
256 assert_eq!(
257 payload["draft_pack"],
258 serde_json::json!([12345, 67890, 11111])
259 );
260 }
261
262 #[test]
263 fn test_try_parse_pack_presentation_second_pack() {
264 let body = "[UnityCrossThreadLogger]1/18/2026 8:42:01 PM\n\
265 <== BotDraftDraftPick(uuid)\n\
266 {\"Payload\":\"{\\\"EventName\\\":\\\"QuickDraft_DSK_20260115\\\",\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":1,\\\"PickNumber\\\":3,\\\"DraftPack\\\":[\\\"22222\\\",\\\"33333\\\"]}\"}";
267 let entry = unity_entry(body);
268 let result = try_parse(&entry, Some(test_timestamp()));
269
270 assert!(result.is_some());
271 let event = result.as_ref().unwrap_or_else(|| unreachable!());
272 let payload = draft_bot_payload(event);
273
274 assert_eq!(payload["pack_number"], 1);
275 assert_eq!(payload["pick_number"], 3);
276 assert_eq!(payload["event_name"], "QuickDraft_DSK_20260115");
277 assert_eq!(payload["draft_pack"], serde_json::json!([22222, 33333]));
278 }
279
280 #[test]
281 fn test_try_parse_pack_presentation_third_pack_last_pick() {
282 let body = "[UnityCrossThreadLogger]2/12/2026 1:11:11 PM\n\
283 <== BotDraftDraftPick(uuid)\n\
284 {\"Payload\":\"{\\\"EventName\\\":\\\"QuickDraft_MKM_20260201\\\",\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":2,\\\"PickNumber\\\":13,\\\"DraftPack\\\":[\\\"44444\\\"]}\"}";
285 let entry = unity_entry(body);
286 let result = try_parse(&entry, Some(test_timestamp()));
287
288 assert!(result.is_some());
289 let event = result.as_ref().unwrap_or_else(|| unreachable!());
290 let payload = draft_bot_payload(event);
291
292 assert_eq!(payload["pack_number"], 2);
293 assert_eq!(payload["pick_number"], 13);
294 assert_eq!(payload["draft_pack"], serde_json::json!([44444]));
295 }
296
297 #[test]
298 fn test_try_parse_pack_presentation_integer_card_ids() {
299 let body = "[UnityCrossThreadLogger]2/01/2026 10:23:51 AM\n\
300 <== BotDraftDraftStatus(uuid)\n\
301 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0,\\\"DraftPack\\\":[12345, 67890]}\"}";
302 let entry = unity_entry(body);
303 let result = try_parse(&entry, Some(test_timestamp()));
304
305 assert!(result.is_some());
306 let event = result.as_ref().unwrap_or_else(|| unreachable!());
307 let payload = draft_bot_payload(event);
308
309 assert_eq!(payload["draft_pack"], serde_json::json!([12345, 67890]));
310 }
311
312 #[test]
313 fn test_try_parse_pack_presentation_empty_pack() {
314 let body = "[UnityCrossThreadLogger]2/12/2026 1:11:11 PM\n\
315 <== BotDraftDraftPick(uuid)\n\
316 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0,\\\"DraftPack\\\":[]}\"}";
317 let entry = unity_entry(body);
318 let result = try_parse(&entry, Some(test_timestamp()));
319
320 assert!(result.is_some());
321 let event = result.as_ref().unwrap_or_else(|| unreachable!());
322 let payload = draft_bot_payload(event);
323
324 assert_eq!(payload["draft_pack"], serde_json::json!([]));
325 }
326
327 #[test]
328 fn test_try_parse_pack_presentation_missing_draft_pack() {
329 let body = "[UnityCrossThreadLogger]2/12/2026 1:11:11 PM\n\
330 <== BotDraftDraftPick(uuid)\n\
331 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0}\"}";
332 let entry = unity_entry(body);
333 let result = try_parse(&entry, Some(test_timestamp()));
334
335 assert!(result.is_some());
336 let event = result.as_ref().unwrap_or_else(|| unreachable!());
337 let payload = draft_bot_payload(event);
338
339 assert_eq!(payload["draft_pack"], serde_json::json!([]));
340 }
341
342 #[test]
343 fn test_try_parse_pack_presentation_preserves_raw_payload() {
344 let body = "[UnityCrossThreadLogger]2/01/2026 10:23:51 AM\n\
345 <== BotDraftDraftStatus(uuid)\n\
346 {\n\
347 \"Payload\":\"{\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0,\\\"DraftPack\\\":[\\\"11111\\\"],\\\"ExtraField\\\":\\\"preserved\\\"}\"
348 }";
349 let entry = unity_entry(body);
350 let result = try_parse(&entry, Some(test_timestamp()));
351
352 assert!(result.is_some());
353 let event = result.as_ref().unwrap_or_else(|| unreachable!());
354 let payload = draft_bot_payload(event);
355
356 assert_eq!(payload["raw_draft_status"]["ExtraField"], "preserved");
357 }
358 }
359
360 mod draft_pick {
363 use super::*;
364
365 #[test]
366 fn test_try_parse_draft_pick_returns_pick() {
367 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"EventName\":\"QuickDraft_TMT_20260313\",\"PickInfo\":{\"EventName\":\"QuickDraft_TMT_20260313\",\"CardIds\":[\"12345\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
368 let entry = unity_entry(body);
369 let result = try_parse(&entry, Some(test_timestamp()));
370
371 assert!(result.is_some());
372 let event = result.as_ref().unwrap_or_else(|| unreachable!());
373 let payload = draft_bot_payload(event);
374
375 assert_eq!(payload["type"], "draft_bot_pick");
376 assert_eq!(payload["card_id"], 12345);
377 assert_eq!(payload["pack_number"], 0);
378 assert_eq!(payload["pick_number"], 0);
379 }
380
381 #[test]
382 fn test_try_parse_draft_pick_later_in_draft() {
383 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"EventName\":\"QuickDraft_TMT_20260313\",\"PickInfo\":{\"EventName\":\"QuickDraft_TMT_20260313\",\"CardIds\":[\"67890\"],\"PackNumber\":1,\"PickNumber\":7}}"}"#;
384 let entry = unity_entry(body);
385 let result = try_parse(&entry, Some(test_timestamp()));
386
387 assert!(result.is_some());
388 let event = result.as_ref().unwrap_or_else(|| unreachable!());
389 let payload = draft_bot_payload(event);
390
391 assert_eq!(payload["card_id"], 67890);
392 assert_eq!(payload["pack_number"], 1);
393 assert_eq!(payload["pick_number"], 7);
394 }
395
396 #[test]
397 fn test_try_parse_draft_pick_event_name_in_request_root_returns_name() {
398 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"EventName\":\"QuickDraft_SOS_20260430\",\"PickInfo\":{\"CardIds\":[\"12345\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
399 let entry = unity_entry(body);
400 let result = try_parse(&entry, Some(test_timestamp()));
401
402 assert!(result.is_some());
403 let event = result.as_ref().unwrap_or_else(|| unreachable!());
404 let payload = draft_bot_payload(event);
405
406 assert_eq!(payload["event_name"], "QuickDraft_SOS_20260430");
407 }
408
409 #[test]
410 fn test_try_parse_draft_pick_event_name_in_pick_info_returns_name() {
411 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"EventName\":\"QuickDraft_SOS_20260430\",\"CardIds\":[\"12345\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
412 let entry = unity_entry(body);
413 let result = try_parse(&entry, Some(test_timestamp()));
414
415 assert!(result.is_some());
416 let event = result.as_ref().unwrap_or_else(|| unreachable!());
417 let payload = draft_bot_payload(event);
418
419 assert_eq!(payload["event_name"], "QuickDraft_SOS_20260430");
420 }
421
422 #[test]
423 fn test_try_parse_draft_pick_missing_event_name_defaults_to_empty() {
424 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"CardIds\":[\"98546\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
425 let entry = unity_entry(body);
426 let result = try_parse(&entry, Some(test_timestamp()));
427
428 assert!(result.is_some());
429 let event = result.as_ref().unwrap_or_else(|| unreachable!());
430 let payload = draft_bot_payload(event);
431
432 assert_eq!(payload["event_name"], "");
433 }
434
435 #[test]
436 fn test_try_parse_draft_pick_missing_card_id_returns_none() {
437 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"EventName\":\"Test\",\"PackNumber\":0,\"PickNumber\":0}}"}"#;
438 let entry = unity_entry(body);
439 let result = try_parse(&entry, Some(test_timestamp()));
440
441 assert!(result.is_none());
442 }
443
444 #[test]
445 fn test_try_parse_draft_pick_preserves_raw_payload() {
446 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"EventName\":\"Test\",\"CardIds\":[\"12345\"],\"PackNumber\":0,\"PickNumber\":0},\"ExtraField\":\"kept\"}"}"#;
447 let entry = unity_entry(body);
448 let result = try_parse(&entry, Some(test_timestamp()));
449
450 assert!(result.is_some());
451 let event = result.as_ref().unwrap_or_else(|| unreachable!());
452 let payload = draft_bot_payload(event);
453
454 assert_eq!(payload["raw_pick_info"]["ExtraField"], "kept");
455 }
456 }
457
458 mod metadata {
461 use super::*;
462
463 #[test]
464 fn test_try_parse_preserves_raw_bytes_pack() {
465 let body = "[UnityCrossThreadLogger]2/01/2026 10:23:51 AM\n\
466 <== BotDraftDraftStatus(uuid)\n\
467 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0,\\\"DraftPack\\\":[]}\"}";
468 let entry = unity_entry(body);
469 let result = try_parse(&entry, Some(test_timestamp()));
470
471 assert!(result.is_some());
472 let event = result.as_ref().unwrap_or_else(|| unreachable!());
473 assert_eq!(event.metadata().raw_bytes(), body.as_bytes());
474 }
475
476 #[test]
477 fn test_try_parse_preserves_raw_bytes_pick() {
478 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"CardIds\":[\"1\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
479 let entry = unity_entry(body);
480 let result = try_parse(&entry, Some(test_timestamp()));
481
482 assert!(result.is_some());
483 let event = result.as_ref().unwrap_or_else(|| unreachable!());
484 assert_eq!(event.metadata().raw_bytes(), body.as_bytes());
485 }
486
487 #[test]
488 fn test_try_parse_stores_timestamp_pack() {
489 let body = "[UnityCrossThreadLogger]2/01/2026 10:23:51 AM\n\
490 <== BotDraftDraftStatus(uuid)\n\
491 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0,\\\"DraftPack\\\":[]}\"}";
492 let entry = unity_entry(body);
493 let ts = Some(test_timestamp());
494 let result = try_parse(&entry, ts);
495
496 assert!(result.is_some());
497 let event = result.as_ref().unwrap_or_else(|| unreachable!());
498 assert_eq!(event.metadata().timestamp(), ts);
499 }
500
501 #[test]
502 fn test_try_parse_stores_timestamp_pick() {
503 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"CardIds\":[\"1\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
504 let entry = unity_entry(body);
505 let ts = Some(test_timestamp());
506 let result = try_parse(&entry, ts);
507
508 assert!(result.is_some());
509 let event = result.as_ref().unwrap_or_else(|| unreachable!());
510 assert_eq!(event.metadata().timestamp(), ts);
511 }
512 }
513
514 mod non_matching {
517 use super::*;
518
519 #[test]
520 fn test_try_parse_unrelated_entry_returns_none() {
521 let body = "[UnityCrossThreadLogger]greToClientEvent\n{\"data\": 1}";
522 let entry = unity_entry(body);
523 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
524 }
525
526 #[test]
527 fn test_try_parse_empty_body_returns_none() {
528 let body = "[UnityCrossThreadLogger]";
529 let entry = unity_entry(body);
530 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
531 }
532
533 #[test]
534 fn test_try_parse_draft_status_not_pick_next_returns_none() {
535 let body = "[UnityCrossThreadLogger]2/26/2026 1:11:11 PM\n\
536 <== BotDraftDraftStatus(uuid)\n\
537 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"Completed\\\",\\\"PackNumber\\\":2,\\\"PickNumber\\\":13,\\\"DraftPack\\\":[]}\"}";
538 let entry = unity_entry(body);
539 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
540 }
541
542 #[test]
543 fn test_try_parse_pack_presentation_wrong_status_returns_none() {
544 let body = "[UnityCrossThreadLogger]2/26/2026 1:11:11 PM\n\
545 <== BotDraftDraftPick(uuid)\n\
546 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"Completed\\\",\\\"PackNumber\\\":2,\\\"PickNumber\\\":13,\\\"DraftPack\\\":[]}\"}";
547 let entry = unity_entry(body);
548 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
549 }
550
551 #[test]
552 fn test_try_parse_malformed_json_returns_none() {
553 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":broken!!!}"}"#;
554 let entry = unity_entry(body);
555 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
556 }
557
558 #[test]
559 fn test_try_parse_marker_only_no_json_returns_none() {
560 let body = "[UnityCrossThreadLogger]==> BotDraftDraftPick";
561 let entry = unity_entry(body);
562 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
563 }
564
565 #[test]
566 fn test_try_parse_missing_pick_fields_returns_none() {
567 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{}}"}"#;
568 let entry = unity_entry(body);
569 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
570 }
571
572 #[test]
573 fn test_try_parse_empty_card_ids_returns_none() {
574 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"CardIds\":[],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
575 let entry = unity_entry(body);
576 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
577 }
578
579 #[test]
580 fn test_try_parse_zero_card_id_returns_none() {
581 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"CardIds\":[\"0\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
584 let entry = unity_entry(body);
585 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
586 }
587
588 #[test]
589 fn test_try_parse_draft_status_marker_in_text_only_returns_none() {
590 let body = "[UnityCrossThreadLogger]2/26/2026 1:11:11 PM\n\
592 <== BotDraftDraftStatus(uuid)\n\
593 DraftStatus is PickNext";
594 let entry = unity_entry(body);
595 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
596 }
597
598 #[test]
599 fn test_try_parse_draft_pick_missing_payload_returns_none() {
600 let body = "[UnityCrossThreadLogger]2/26/2026 1:11:11 PM\n\
601 <== BotDraftDraftPick(uuid)\n\
602 {\n\
603 \"Result\": \"Success\"\n\
604 }";
605 let entry = unity_entry(body);
606 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
607 }
608
609 #[test]
610 fn test_try_parse_draft_pick_malformed_payload_returns_none() {
611 let body = "[UnityCrossThreadLogger]2/26/2026 1:11:11 PM\n\
612 <== BotDraftDraftPick(uuid)\n\
613 {\n\
614 \"Payload\": \"not json\"\n\
615 }";
616 let entry = unity_entry(body);
617 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
618 }
619
620 #[test]
621 fn test_try_parse_human_draft_entry_returns_none() {
622 let body = "[UnityCrossThreadLogger]Draft.Notify\n\
624 {\n\
625 \"draftId\": \"abc-123\",\n\
626 \"SelfPack\": 0,\n\
627 \"SelfPick\": 0,\n\
628 \"PackCards\": \"12345,67890\"\n\
629 }";
630 let entry = unity_entry(body);
631 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
632 }
633
634 #[test]
635 fn test_try_parse_connection_manager_header_returns_none() {
636 let entry = LogEntry {
637 header: EntryHeader::ConnectionManager,
638 body: "[ConnectionManager]some connection message".to_owned(),
639 };
640 assert!(try_parse(&entry, Some(test_timestamp())).is_none());
641 }
642 }
643
644 mod performance_class {
647 use super::*;
648
649 #[test]
650 fn test_draft_bot_event_is_durable_per_event() {
651 let body = "[UnityCrossThreadLogger]2/01/2026 10:23:51 AM\n\
652 <== BotDraftDraftStatus(uuid)\n\
653 {\"Payload\":\"{\\\"DraftStatus\\\":\\\"PickNext\\\",\\\"PackNumber\\\":0,\\\"PickNumber\\\":0,\\\"DraftPack\\\":[]}\"}";
654 let entry = unity_entry(body);
655 let result = try_parse(&entry, Some(test_timestamp()));
656
657 assert!(result.is_some());
658 let event = result.as_ref().unwrap_or_else(|| unreachable!());
659 assert_eq!(event.performance_class(), PerformanceClass::DurablePerEvent);
660 }
661
662 #[test]
663 fn test_draft_bot_pick_event_is_durable_per_event() {
664 let body = r#"[UnityCrossThreadLogger]==> BotDraftDraftPick {"id":"uuid","request":"{\"PickInfo\":{\"CardIds\":[\"1\"],\"PackNumber\":0,\"PickNumber\":0}}"}"#;
665 let entry = unity_entry(body);
666 let result = try_parse(&entry, Some(test_timestamp()));
667
668 assert!(result.is_some());
669 let event = result.as_ref().unwrap_or_else(|| unreachable!());
670 assert_eq!(event.performance_class(), PerformanceClass::DurablePerEvent);
671 }
672 }
673
674 mod helpers {
677 use super::*;
678
679 #[test]
680 fn test_extract_draft_pack_string_ids() {
681 let parsed = serde_json::json!({
682 "DraftPack": ["12345", "67890", "11111"]
683 });
684 let pack = extract_draft_pack(&parsed);
685 assert_eq!(pack, vec![12345, 67890, 11111]);
686 }
687
688 #[test]
689 fn test_extract_draft_pack_integer_ids() {
690 let parsed = serde_json::json!({
691 "DraftPack": [12345, 67890]
692 });
693 let pack = extract_draft_pack(&parsed);
694 assert_eq!(pack, vec![12345, 67890]);
695 }
696
697 #[test]
698 fn test_extract_draft_pack_empty() {
699 let parsed = serde_json::json!({"DraftPack": []});
700 let pack = extract_draft_pack(&parsed);
701 assert!(pack.is_empty());
702 }
703
704 #[test]
705 fn test_extract_draft_pack_missing_field() {
706 let parsed = serde_json::json!({"other": "data"});
707 let pack = extract_draft_pack(&parsed);
708 assert!(pack.is_empty());
709 }
710
711 #[test]
712 fn test_extract_draft_pack_mixed_types() {
713 let parsed = serde_json::json!({
714 "DraftPack": [12345, "67890", "not_a_number", 11111]
715 });
716 let pack = extract_draft_pack(&parsed);
717 assert_eq!(pack, vec![12345, 67890, 11111]);
719 }
720 }
721}