bcode_session 0.0.1-alpha.0

Session lifecycle and attachment management for bcode
use crate::SessionManager;
use bcode_session_models::{
    ClientId, ModelTurnOutcome, SessionEventKind, SessionId, SessionMilestone,
    SessionMilestoneEvaluationRequest, SessionMilestoneProvenance,
    SessionMilestonePublicationRequest, SessionMilestoneQuery, TurnAdmissionMetadata, TurnId,
};

#[tokio::test]
async fn first_automatic_title_publication_and_identical_retry_succeed_without_explicit_name() {
    let root = tempfile::tempdir().unwrap();
    let manager = SessionManager::persistent(root.path()).unwrap();
    let id = manager
        .create_session(None, root.path().into())
        .await
        .unwrap()
        .id;
    let handle = manager.session_handle(id).await.unwrap();
    let ownership = handle
        .acquire_ownership(crate::actor::SessionOwnershipKind::RuntimeWork)
        .await
        .unwrap();
    let start = manager.current_session_generation(id).await.unwrap() + 1;
    let turn_id = TurnId::from_accepted_event(id, start).to_string();
    for kind in [
        SessionEventKind::UserMessage {
            client_id: ClientId::new(),
            text: "original fallback".into(),
            admission: TurnAdmissionMetadata::default(),
        },
        SessionEventKind::ModelTurnStarted {
            turn_id: turn_id.clone(),
        },
        SessionEventKind::AssistantResponseSegment {
            turn_id: turn_id.clone(),
            segment_id: "final".into(),
            segment_order: 0,
            text: "done".into(),
        },
        SessionEventKind::ModelTurnFinished {
            turn_id,
            outcome: ModelTurnOutcome::Completed,
            message: None,
        },
    ] {
        handle.append_event(kind, 100).await.unwrap();
    }
    let context = manager
        .session_milestone_context(id, 8192, 10)
        .await
        .unwrap();
    let source = context.source.unwrap();
    assert!(
        manager
            .request_session_milestone_evaluation(
                id,
                SessionMilestoneEvaluationRequest {
                    evaluation_id: "automatic".into(),
                    source: source.clone(),
                    expected_generation: context.generation,
                    expected_explicit_name: None,
                    max_requests_per_session: Some(10),
                }
            )
            .await
            .unwrap()
    );
    let request = SessionMilestonePublicationRequest {
        milestone: SessionMilestone {
            evaluation_id: "automatic".into(),
            source,
            header: "heading".into(),
            description: "description".into(),
            provenance: SessionMilestoneProvenance {
                provider_id: "offline".into(),
                model_id: "fixture".into(),
                input_truncated: false,
            },
        },
        automatic_title: Some("Generated title".into()),
    };
    manager
        .publish_session_milestone(id, request.clone())
        .await
        .unwrap();
    let generation = manager.current_session_generation(id).await.unwrap();
    manager
        .publish_session_milestone(id, request)
        .await
        .unwrap();
    assert_eq!(
        manager.current_session_generation(id).await.unwrap(),
        generation
    );
    let summary = handle.snapshot().summary;
    assert!(summary.explicit_name.is_none());
    assert_eq!(summary.automatic_title.as_deref(), Some("Generated title"));
    assert_eq!(summary.derived_title.as_deref(), Some("original fallback"));
    assert_eq!(summary.display_title(), "Generated title");
    drop(ownership);
    manager.release_persistent_idle_session_resources(id).await;
}

#[tokio::test]
async fn loaded_actor_reads_and_exact_publication_retry_preserve_explicit_name() {
    let root = tempfile::tempdir().unwrap();
    let manager = SessionManager::persistent(root.path().to_path_buf()).unwrap();
    let summary = manager
        .create_session(None, root.path().to_path_buf())
        .await
        .unwrap();
    let id = summary.id;
    let handle = manager.session_handle(id).await.unwrap();
    let ownership = handle
        .acquire_ownership(crate::actor::SessionOwnershipKind::RuntimeWork)
        .await
        .unwrap();
    append_settled_fixture_turn(&manager, id, &handle).await;
    let context = manager
        .session_milestone_context(id, 8192, 10)
        .await
        .unwrap();
    assert!(context.text.contains("cancelled"));
    let request = SessionMilestoneEvaluationRequest {
        evaluation_id: "actor".into(),
        source: context.source.unwrap(),
        expected_generation: context.generation,
        expected_explicit_name: None,
        max_requests_per_session: Some(10),
    };
    assert!(
        manager
            .request_session_milestone_evaluation(id, request.clone())
            .await
            .unwrap()
    );
    assert!(
        !manager
            .request_session_milestone_evaluation(id, request.clone())
            .await
            .unwrap()
    );
    let publication = SessionMilestonePublicationRequest {
        milestone: SessionMilestone {
            evaluation_id: "actor".into(),
            source: request.source,
            header: "heading".into(),
            description: "description".into(),
            provenance: SessionMilestoneProvenance {
                provider_id: "fixture".into(),
                model_id: "small".into(),
                input_truncated: false,
            },
        },
        automatic_title: Some("generated".into()),
    };
    manager
        .rename_session(id, Some("manual".into()))
        .await
        .unwrap();
    assert_publication_retry_and_conflict(&manager, id, publication).await;
    assert_completed_publication_queries(&manager, id).await;
    let snapshot = handle.snapshot();
    assert_eq!(snapshot.summary.explicit_name.as_deref(), Some("manual"));
    assert_eq!(
        snapshot.summary.derived_title.as_deref(),
        Some("first prompt")
    );
    assert!(snapshot.summary.automatic_title.is_none());
    drop(ownership);
    manager.release_persistent_idle_session_resources(id).await;
}

async fn assert_publication_retry_and_conflict(
    manager: &SessionManager,
    id: SessionId,
    publication: SessionMilestonePublicationRequest,
) {
    manager
        .publish_session_milestone(id, publication.clone())
        .await
        .unwrap();
    manager
        .publish_session_milestone(id, publication.clone())
        .await
        .unwrap();
    let mut conflict = publication;
    conflict.automatic_title = Some("different".into());
    assert!(
        manager
            .publish_session_milestone(id, conflict)
            .await
            .is_err()
    );
}

async fn append_settled_fixture_turn(
    manager: &SessionManager,
    id: SessionId,
    handle: &crate::actor::SessionHandle,
) {
    let generation = manager.current_session_generation(id).await.unwrap();
    let start = generation + 1;
    let turn_id = TurnId::from_accepted_event(id, start).to_string();
    for kind in [
        SessionEventKind::UserMessage {
            client_id: ClientId::new(),
            text: "first prompt".into(),
            admission: TurnAdmissionMetadata::default(),
        },
        SessionEventKind::ModelTurnStarted {
            turn_id: turn_id.clone(),
        },
        SessionEventKind::AssistantResponseSegment {
            turn_id: turn_id.clone(),
            segment_id: "a".into(),
            segment_order: 0,
            text: "done".into(),
        },
        SessionEventKind::ModelTurnFinished {
            turn_id,
            outcome: ModelTurnOutcome::Cancelled,
            message: None,
        },
    ] {
        handle.append_event(kind, 100).await.unwrap();
    }
}

async fn assert_completed_publication_queries(manager: &SessionManager, id: SessionId) {
    assert_eq!(
        manager
            .session_milestones(
                id,
                SessionMilestoneQuery {
                    after_sequence: None,
                    limit: 1
                }
            )
            .await
            .unwrap()
            .milestones
            .len(),
        1
    );
    assert!(
        manager
            .session_milestone_evaluation(id, "actor")
            .await
            .unwrap()
            .unwrap()
            .completed
    );
    assert!(
        manager
            .session_milestone_evaluation_checkpoint(id)
            .await
            .unwrap()
            .unwrap()
            .completed
    );
}