blob: 1c6cf7ff1490cbed4e00264f2b365dd824ccd404 [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::CommitError;
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::event::ToolOutcome;
use maka_event_log::EventLog;
use serde_json::{Value, json};
#[tokio::test]
async fn batch_rolls_back_every_fact_and_catalog_change_then_replays_exactly_after_reopen() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
let commits = log.subscribe_commits();
log.create_session("session", "request", &json!({}), 1)
.await
.unwrap();
let catalog = log.list_sessions::<Value>(None, None, 10).await.unwrap();
let session = log.get_session::<Value>("session").await.unwrap();
let invocation = Invocation {
session_id: "session".into(),
turn_id: "turn".into(),
run_id: "run".into(),
invocation_id: "invocation".into(),
};
let event = |fact| RuntimeEvent::new(invocation.clone(), fact);
let opening = event(Fact::InvocationOpened {
configuration: None,
input: maka_agent_facts::input::InvocationInput::Message {
source_messages: Vec::new(),
content: "".into(),
request_fingerprint: None,
},
});
let settled = event(Fact::ToolSettled {
operation_id: "call".into(),
outcome: ToolOutcome::Failed {
message: "not executed".into(),
},
});
assert!(matches!(
log.append_batch(
&([opening.clone(), settled.clone()])
.iter()
.cloned()
.map(EventWrite::plain)
.collect::<Result<Vec<_>, _>>()
.unwrap()
)
.await,
Err(CommitError::Rejected(_))
));
assert!(!commits.has_changed().unwrap());
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert!(log.prefix(10, 16_384).await.unwrap().events.is_empty());
assert_eq!(log.get_session::<Value>("session").await.unwrap(), session);
assert_eq!(
log.list_sessions::<Value>(None, None, 10)
.await
.unwrap()
.revision,
catalog.revision
);
log.append(&EventWrite::plain((opening).clone()).unwrap())
.await
.unwrap();
let mut commits = log.subscribe_commits();
let dispatched = event(Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "call".into(),
call: maka_agent_facts::tool_call::ToolCallIdentity::standalone("call-call".into()),
name: "write_file".into(),
input: json!({}),
});
let mut conflicting = opening.clone();
conflicting.fact = Fact::InvocationOpened {
configuration: None,
input: maka_agent_facts::input::InvocationInput::Code {
source: "different".into(),
},
};
assert!(matches!(
log.append_batch(
&([dispatched.clone(), conflicting])
.iter()
.cloned()
.map(EventWrite::plain)
.collect::<Result<Vec<_>, _>>()
.unwrap()
)
.await,
Err(CommitError::Rejected(_))
));
assert_eq!(log.prefix(10, 16_384).await.unwrap().events.len(), 1);
assert!(!commits.has_changed().unwrap());
let pair = [dispatched, settled];
let sequences = log
.append_batch(
&(pair)
.iter()
.cloned()
.map(EventWrite::plain)
.collect::<Result<Vec<_>, _>>()
.unwrap(),
)
.await
.unwrap();
assert_eq!(sequences.len(), 2);
assert_eq!(sequences[1], sequences[0] + 1);
assert!(commits.has_changed().unwrap());
assert_eq!(*commits.borrow_and_update(), sequences[1]);
// A replay mixed with a new terminal still validates and commits in order.
let terminal = event(Fact::InvocationEnded {
outcome: InvocationOutcome::Failed {
model_failure: None,
class: "host_interrupted".into(),
message: None,
},
});
let mixed = log
.append_batch(
&([pair[1].clone(), terminal])
.iter()
.cloned()
.map(EventWrite::plain)
.collect::<Result<Vec<_>, _>>()
.unwrap(),
)
.await
.unwrap();
assert_eq!(mixed[0], sequences[1]);
assert_eq!(mixed[1], sequences[1] + 1);
let committed = log.prefix(10, 16_384).await.unwrap();
let catalog = log.list_sessions::<Value>(None, None, 10).await.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
let commits = log.subscribe_commits();
assert_eq!(
log.append_batch(
&(pair)
.iter()
.cloned()
.map(EventWrite::plain)
.collect::<Result<Vec<_>, _>>()
.unwrap()
)
.await
.unwrap(),
sequences
);
assert_eq!(
log.append_batch(
&([pair[0].clone(), pair[0].clone()])
.iter()
.cloned()
.map(EventWrite::plain)
.collect::<Result<Vec<_>, _>>()
.unwrap()
)
.await
.unwrap(),
vec![sequences[0]; 2]
);
assert!(log.append_batch(&[]).await.unwrap().is_empty());
assert!(!commits.has_changed().unwrap());
let reopened = log.prefix(10, 16_384).await.unwrap();
assert_eq!(reopened.digest, committed.digest);
assert_eq!(reopened.high_water, committed.high_water);
assert!(
reopened
.project_invocation("invocation")
.uncertain_operations
.is_empty()
);
assert_eq!(
log.list_sessions::<Value>(None, None, 10)
.await
.unwrap()
.revision,
catalog.revision
);
log.close().await.unwrap();
}