mobius 0.9.20

A small, modular Rust framework for building coding agents
Documentation
use super::*;

#[tokio::test]
async fn event_journal_sequences_and_pages_normalized_events() {
    let workspace = tempfile::tempdir().expect("create workspace");
    let store = SqliteCheckpoint::new(workspace.path().join("checkpoints.sqlite3"))
        .expect("open checkpoint database");
    store
        .save(&Checkpoint::empty("session"), &[], None)
        .await
        .expect("save session");

    for (recorded_at_ms, message) in [(10, "first"), (20, "second")] {
        store
            .append_event(
                "session",
                recorded_at_ms,
                &Event {
                    submission_id: None,
                    msg: EventMsg::Warning(crate::protocol::WarningEvent {
                        message: message.into(),
                    }),
                },
            )
            .await
            .expect("append event");
    }

    let newest = store
        .event_page(
            "session",
            EventPageRequest {
                before_sequence: None,
                limit: 1,
            },
        )
        .await
        .expect("newest event");
    let older = store
        .event_page(
            "session",
            EventPageRequest {
                before_sequence: newest.next_before_sequence,
                limit: 1,
            },
        )
        .await
        .expect("older event");

    assert_eq!(newest.events[0].sequence, 2);
    assert_eq!(newest.latest_sequence, 2);
    assert_eq!(newest.events[0].recorded_at_ms, 20);
    assert_eq!(newest.next_before_sequence, Some(2));
    assert_eq!(older.events[0].sequence, 1);
    assert_eq!(older.next_before_sequence, None);
}

#[tokio::test]
async fn event_turn_page_keeps_long_turns_whole() {
    let workspace = tempfile::tempdir().expect("create workspace");
    let store = SqliteCheckpoint::new(workspace.path().join("checkpoints.sqlite3"))
        .expect("open checkpoint database");
    store
        .save(&Checkpoint::empty("session"), &[], None)
        .await
        .expect("save session");

    for event in [
        EventMsg::TurnStarted(TurnStartedEvent {
            turn_id: "older".into(),
            model_context_window: None,
        }),
        EventMsg::Warning(crate::protocol::WarningEvent {
            message: "older work".into(),
        }),
    ] {
        store
            .append_event(
                "session",
                0,
                &Event {
                    submission_id: None,
                    msg: event,
                },
            )
            .await
            .expect("append older turn event");
    }
    for index in 0..100 {
        store
            .append_event(
                "session",
                index,
                &Event {
                    submission_id: None,
                    msg: EventMsg::Warning(crate::protocol::WarningEvent {
                        message: format!("older work {index}"),
                    }),
                },
            )
            .await
            .expect("append long older turn");
    }
    store
        .append_event(
            "session",
            100,
            &Event {
                submission_id: None,
                msg: EventMsg::TurnAborted(TurnAbortedEvent {
                    turn_id: "older".into(),
                    reason: "interrupted".into(),
                }),
            },
        )
        .await
        .expect("complete older turn");
    store
        .append_event(
            "session",
            101,
            &Event {
                submission_id: None,
                msg: EventMsg::TurnStarted(TurnStartedEvent {
                    turn_id: "latest".into(),
                    model_context_window: None,
                }),
            },
        )
        .await
        .expect("start latest turn");
    store
        .append_event(
            "session",
            102,
            &Event {
                submission_id: None,
                msg: EventMsg::TurnComplete(TurnCompleteEvent {
                    turn_id: "latest".into(),
                }),
            },
        )
        .await
        .expect("complete latest turn");
    store
        .append_event(
            "session",
            103,
            &Event {
                submission_id: None,
                msg: EventMsg::Warning(crate::protocol::WarningEvent {
                    message: "between turns".into(),
                }),
            },
        )
        .await
        .expect("append inter-turn metadata");

    let latest = event_turn_page(&store, "session", None)
        .await
        .expect("load latest turn");
    let older = event_turn_page(&store, "session", latest.next_before_sequence)
        .await
        .expect("load older turn");

    assert!(matches!(
        latest.into_chronological().as_slice(),
        [
            JournalEvent {
                event: Event {
                    msg: EventMsg::TurnStarted(started),
                    ..
                },
                ..
            },
            JournalEvent {
                event: Event {
                    msg: EventMsg::TurnComplete(completed),
                    ..
                },
                ..
            }
        ] if started.turn_id == "latest" && completed.turn_id == "latest"
    ));
    assert_eq!(older.events.len(), 103);
    assert_eq!(older.next_before_sequence, None);
}

#[tokio::test]
async fn transient_controls_advance_sequence_without_entering_history() {
    use crate::protocol::FrontendEvent;
    use crate::protocol::SessionContext;
    use crate::protocol::SessionResumeRequestedEvent;

    let workspace = tempfile::tempdir().expect("create workspace");
    let store = SqliteCheckpoint::new(workspace.path().join("checkpoints.sqlite3"))
        .expect("open checkpoint database");
    store
        .save(&Checkpoint::empty("session"), &[], None)
        .await
        .expect("save session");
    let events = [
        EventMsg::Warning(crate::protocol::WarningEvent {
            message: "durable".into(),
        }),
        EventMsg::SessionResumeRequested(SessionResumeRequestedEvent {
            session_id: "session".into(),
            context: SessionContext::default(),
        }),
        EventMsg::Frontend(FrontendEvent::Picker {
            title: "Choose".into(),
            options: Vec::new(),
        }),
        EventMsg::Frontend(FrontendEvent::Preview {
            id: "preview".into(),
            title: "Preview".into(),
            subtitle: String::new(),
            page_id: "preview:latest".into(),
            update: crate::protocol::FrontendPreviewUpdate::Replace,
            events: Vec::new(),
            next: None,
        }),
        EventMsg::Frontend(FrontendEvent::Widget {
            capability: "test".into(),
            item: crate::protocol::FrontendWidget {
                id: "status".into(),
                slot: crate::protocol::FrontendSlot::Header,
                text: "Current".into(),
                tone: crate::protocol::FrontendTone::Neutral,
                symbol: None,
                icon_only: false,
                progress: None,
                content: None,
                action: None,
            },
        }),
        EventMsg::Frontend(FrontendEvent::RemoveWidget {
            capability: "test".into(),
            id: "status".into(),
        }),
    ];
    for (index, msg) in events.into_iter().enumerate() {
        store
            .append_event(
                "session",
                i64::try_from(index).expect("timestamp"),
                &Event {
                    submission_id: None,
                    msg,
                },
            )
            .await
            .expect("append event");
    }

    let page = store
        .event_page(
            "session",
            EventPageRequest {
                before_sequence: None,
                limit: 10,
            },
        )
        .await
        .expect("event page");

    assert_eq!(page.latest_sequence, 6);
    assert_eq!(
        page.events
            .iter()
            .map(|event| event.sequence)
            .collect::<Vec<_>>(),
        [1]
    );
}