use manasight_parser::events::GameEvent;
use manasight_parser::log::entry::LineBuffer;
use manasight_parser::router::Router;
fn route_log(log: &str) -> Vec<GameEvent> {
let mut buf = LineBuffer::new();
let router = Router::new();
let mut events = Vec::new();
for line in log.lines() {
for entry in buf.push_line(line) {
events.extend(router.route(&entry));
}
}
if let Some(entry) = buf.flush() {
events.extend(router.route(&entry));
}
events
}
fn synthetic_log_with_truncation_between_valid_gsms() -> String {
let valid_gsm_before = r#"[UnityCrossThreadLogger]5/13/2026 10:01:11 AM
{"greToClientEvent":{"greToClientMessages":[{"type":"GREMessageType_GameStateMessage","msgId":1,"gameStateId":100,"gameStateMessage":{"type":"GameStateType_Diff","prevGameStateId":99,"zones":[],"gameObjects":[]}}]}}"#;
let marker_block = "[UnityCrossThreadLogger]5/13/2026 10:01:12 AM: Match to <transaction>: GreToClientEvent\n\
[Message summarized because one or more GameStateMessages exceeded the 50 GameObject or 50 Annotation limit.]\n\
::: GameStateMessage\n\
:: GameObject Count = 63\n\
:: Annotation Count = 4\n\
::: ActionsAvailableReq";
let valid_gsm_after = r#"[UnityCrossThreadLogger]5/13/2026 10:01:13 AM
{"greToClientEvent":{"greToClientMessages":[{"type":"GREMessageType_GameStateMessage","msgId":3,"gameStateId":102,"gameStateMessage":{"type":"GameStateType_Diff","prevGameStateId":101,"zones":[],"gameObjects":[]}}]}}"#;
format!("{valid_gsm_before}\n{marker_block}\n{valid_gsm_after}\n")
}
#[test]
fn test_truncation_marker_emits_exactly_one_truncation_event() {
let log = synthetic_log_with_truncation_between_valid_gsms();
let events = route_log(&log);
let truncations: Vec<_> = events
.iter()
.filter(|e| matches!(e, GameEvent::Truncation(_)))
.collect();
assert_eq!(
truncations.len(),
1,
"expected exactly 1 Truncation event from the marker block, got {} (all events: {events:?})",
truncations.len(),
);
let GameEvent::Truncation(ref event) = truncations[0] else {
unreachable!("filter guard");
};
assert_eq!(event.object_count(), Some(63));
assert_eq!(event.annotation_count(), Some(4));
}
#[test]
fn test_truncation_does_not_swallow_adjacent_valid_gsms() {
let log = synthetic_log_with_truncation_between_valid_gsms();
let events = route_log(&log);
let game_states: Vec<_> = events
.iter()
.filter(|e| matches!(e, GameEvent::GameState(_)))
.collect();
assert_eq!(
game_states.len(),
2,
"expected 2 GameState events (one before + one after the marker), got {} (all events: {events:?})",
game_states.len(),
);
}
#[test]
fn test_truncation_marker_carries_log_timestamp() {
let log = "\
[Message summarized because one or more GameStateMessages exceeded the 50 GameObject or 50 Annotation limit.]
::: GameStateMessage
:: GameObject Count = 51
:: Annotation Count = 0
::: ActionsAvailableReq
[UnityCrossThreadLogger]5/13/2026 10:01:13 AM Next
";
let events = route_log(log);
let truncations: Vec<_> = events
.iter()
.filter(|e| matches!(e, GameEvent::Truncation(_)))
.collect();
assert_eq!(truncations.len(), 1);
let GameEvent::Truncation(ref event) = truncations[0] else {
unreachable!();
};
assert!(event.metadata().timestamp().is_none());
assert_eq!(event.object_count(), Some(51));
assert_eq!(event.annotation_count(), Some(0));
}
#[test]
fn test_no_truncation_event_from_valid_gsm_envelopes() {
let log = "\
[UnityCrossThreadLogger]5/13/2026 10:01:11 AM
{\"greToClientEvent\":{\"greToClientMessages\":[{\"type\":\"GREMessageType_GameStateMessage\",\"msgId\":1,\"gameStateId\":100,\"gameStateMessage\":{\"type\":\"GameStateType_Diff\",\"prevGameStateId\":99,\"zones\":[],\"gameObjects\":[]}}]}}
[UnityCrossThreadLogger]5/13/2026 10:01:12 AM
{\"greToClientEvent\":{\"greToClientMessages\":[{\"type\":\"GREMessageType_GameStateMessage\",\"msgId\":2,\"gameStateId\":101,\"gameStateMessage\":{\"type\":\"GameStateType_Diff\",\"prevGameStateId\":100,\"zones\":[],\"gameObjects\":[]}}]}}
";
let events = route_log(log);
let truncations = events
.iter()
.filter(|e| matches!(e, GameEvent::Truncation(_)))
.count();
assert_eq!(truncations, 0);
}