blob: f7780cbbabfba21d58f73f7859d7106461a35361 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::input::InvocationInput;
use maka_event_log::{EventLog, StoreError, sessions::SessionRecord};
use serde_json::{Value, json};
fn invocation(session: &str) -> Invocation {
Invocation {
session_id: session.into(),
turn_id: format!("turn-{session}"),
run_id: format!("run-{session}"),
invocation_id: format!("invocation-{session}"),
}
}
fn opening(session: &str) -> RuntimeEvent {
RuntimeEvent::new(
invocation(session),
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
source_messages: Vec::new(),
content: "visible".into(),
request_fingerprint: None,
},
},
)
}
async fn catalog(log: &EventLog) -> String {
log.list_sessions::<Value>(None, None, 32)
.await
.unwrap()
.revision
}
async fn acknowledge(log: &EventLog, message: &str) -> SessionRecord<Value> {
log.set_session_read_marker::<Value>("session", message)
.await
.unwrap()
}
#[tokio::test]
async fn active_ack_finalization_replay_and_rebuild_preserve_durable_control() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
let initial = log
.create_session("session", "fingerprint", &json!({}), 10)
.await
.unwrap();
assert!(!initial.read_state.has_unread);
assert_eq!(initial.read_state.last_read_message_id, None);
let opening = opening("session");
log.append(&EventWrite::plain((opening).clone()).unwrap())
.await
.unwrap();
let active = acknowledge(&log, &opening.id).await;
assert_eq!(active.revision, 3);
assert!(!active.read_state.has_unread);
assert_eq!(
active.read_state.last_read_message_id.as_deref(),
Some(opening.id.as_str())
);
let terminal = RuntimeEvent::new(
invocation("session"),
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
);
log.append(&EventWrite::plain((terminal).clone()).unwrap())
.await
.unwrap();
let finished = log.get_session::<Value>("session").await.unwrap().unwrap();
assert_eq!(finished.revision, active.revision + 1);
assert!(finished.read_state.has_unread);
assert_eq!(
finished.read_state.last_read_message_id,
active.read_state.last_read_message_id
);
let bytes = serde_json::to_vec(&log.prefix(32, 65536).await.unwrap()).unwrap();
let acknowledged = acknowledge(&log, &opening.id).await;
assert_eq!(acknowledged.revision, finished.revision + 1);
assert_eq!(acknowledged.execution, finished.execution);
assert_eq!((acknowledged.created_at, acknowledged.updated_at), (10, 10));
let revision = catalog(&log).await;
log.append(&EventWrite::plain((terminal).clone()).unwrap())
.await
.unwrap();
assert_eq!(
log.set_session_read_marker::<Value>("session", &opening.id)
.await
.unwrap(),
acknowledged
);
assert_eq!(catalog(&log).await, revision);
log.close().await.unwrap();
let source = rusqlite::Connection::open(&path).unwrap();
source
.execute_batch("DELETE FROM catalog_messages;")
.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.probe_session_create::<Value>("session", "fingerprint")
.await
.unwrap(),
Some(acknowledged)
);
assert_eq!(catalog(&log).await, revision);
assert_eq!(
serde_json::to_vec(&log.prefix(32, 65536).await.unwrap()).unwrap(),
bytes
);
}
#[tokio::test]
async fn unknown_and_empty_tail_ids_leave_all_authorities_unchanged() {
let temp = tempfile::tempdir().unwrap();
let log = EventLog::open(&temp.path().join("events.sqlite"))
.await
.unwrap();
log.create_session("session", "fingerprint", &json!({}), 7)
.await
.unwrap();
log.create_session("other", "other", &json!({}), 7)
.await
.unwrap();
for populated in [false, true] {
let other = opening("other");
if populated {
log.append(&EventWrite::plain((opening("session")).clone()).unwrap())
.await
.unwrap();
log.append(&EventWrite::plain((other).clone()).unwrap())
.await
.unwrap();
}
let before = log.get_session::<Value>("session").await.unwrap().unwrap();
let revision = catalog(&log).await;
let bytes = serde_json::to_vec(&log.prefix(32, 65536).await.unwrap()).unwrap();
for id in ["unknown", "invocation-session", other.id.as_str()] {
let record = log
.set_session_read_marker::<Value>("session", id)
.await
.unwrap();
assert_eq!(record, before);
}
assert_eq!(catalog(&log).await, revision);
assert_eq!(
serde_json::to_vec(&log.prefix(32, 65536).await.unwrap()).unwrap(),
bytes
);
}
assert!(matches!(
log.set_session_read_marker::<Value>("missing", "unknown")
.await,
Err(StoreError::SessionNotFound)
));
for invalid in ["", "invalid:id", "with space"] {
assert!(matches!(
log.set_session_read_marker::<Value>("session", invalid)
.await,
Err(StoreError::InvalidTransition(_))
));
}
}