tandem-server 0.5.8

HTTP server for Tandem engine APIs
#[cfg(test)]
mod tests {
    use super::*;

    fn text_message(role: MessageRole, id: &str, text: &str) -> Message {
        let mut message = Message::new(
            role,
            vec![MessagePart::Text {
                text: text.to_string(),
            }],
        );
        message.id = id.to_string();
        message
    }

    #[test]
    fn latest_archived_exchange_candidate_uses_latest_user_assistant_pair() {
        let mut session = Session::new(Some("chat".to_string()), Some(".".to_string()));
        session.workspace_root = Some("/tmp/tandem".to_string());
        session.project_id = Some("workspace-123".to_string());
        session.messages = vec![
            text_message(MessageRole::User, "u1", "first request"),
            text_message(MessageRole::Assistant, "a1", "first answer"),
            text_message(MessageRole::User, "u2", "second request"),
            text_message(MessageRole::Assistant, "a2", "second answer"),
        ];

        let candidate = latest_archived_exchange_candidate(&session).expect("candidate");
        assert_eq!(candidate.user_message_id, "u2");
        assert_eq!(candidate.assistant_message_id, "a2");
        assert_eq!(candidate.user_text, "second request");
        assert_eq!(candidate.assistant_text, "second answer");
    }

    #[test]
    fn latest_archived_exchange_candidate_skips_slash_commands_and_errors() {
        let mut session = Session::new(Some("chat".to_string()), Some(".".to_string()));
        session.messages = vec![
            text_message(MessageRole::User, "u1", "/new"),
            text_message(
                MessageRole::Assistant,
                "a1",
                "ENGINE_ERROR: ENGINE_DISPATCH_FAILED: boom",
            ),
            text_message(MessageRole::User, "u2", "real question"),
            text_message(MessageRole::Assistant, "a2", "real answer"),
        ];

        let candidate = latest_archived_exchange_candidate(&session).expect("candidate");
        assert_eq!(candidate.user_message_id, "u2");
        assert_eq!(candidate.assistant_message_id, "a2");
    }

    #[test]
    fn latest_archived_exchange_candidate_ignores_reasoning_parts() {
        let mut session = Session::new(Some("chat".to_string()), Some(".".to_string()));
        let mut user = Message::new(
            MessageRole::User,
            vec![MessagePart::Text {
                text: "what changed?".to_string(),
            }],
        );
        user.id = "u1".to_string();
        let mut assistant = Message::new(
            MessageRole::Assistant,
            vec![
                MessagePart::Reasoning {
                    text: "private chain of thought".to_string(),
                },
                MessagePart::Text {
                    text: "We archived the exchange.".to_string(),
                },
            ],
        );
        assistant.id = "a1".to_string();
        session.messages = vec![user, assistant];

        let candidate = latest_archived_exchange_candidate(&session).expect("candidate");
        assert_eq!(candidate.user_text, "what changed?");
        assert_eq!(candidate.assistant_text, "We archived the exchange.");
    }

    #[test]
    fn archive_source_hash_is_stable_for_same_exchange() {
        let candidate = ArchivedExchangeCandidate {
            user_message_id: "u1".to_string(),
            assistant_message_id: "a1".to_string(),
            user_text: "hello".to_string(),
            assistant_text: "world".to_string(),
        };

        let a = archive_source_hash("session-1", &candidate);
        let b = archive_source_hash("session-1", &candidate);
        let c = archive_source_hash("session-2", &candidate);

        assert_eq!(a, b);
        assert_ne!(a, c);
    }
}