blob: 03aae4c05087024b65a80631dc0c1ba4e102b0b6 [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::InvocationInput;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::session::CopyPurpose;
use maka_event_log::{
EventLog, StoreError,
bundle::{BundleError, StagedBundle},
sessions::{MaterialCollection, SessionCopy},
};
use serde_json::{Value, json};
use sqlx::Connection;
use std::collections::BTreeMap;
pub(super) async fn roundtrip(bytes: &[u8]) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("destination.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("destination", "destination", &json!({}), 1)
.await
.unwrap();
let invocation = Invocation {
session_id: "destination".into(),
turn_id: "destination-turn".into(),
run_id: "destination-run".into(),
invocation_id: "destination-invocation".into(),
};
for fact in [
Fact::InvocationOpened {
input: InvocationInput::Code {
source: "local history".into(),
},
configuration: None,
},
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
] {
log.append(&EventWrite::plain(RuntimeEvent::new(invocation.clone(), fact)).unwrap())
.await
.unwrap();
}
let binding = maka_agent_facts::artifact::content_digest(b"selected destination");
let staged = StagedBundle::read(bytes).await.unwrap();
let root = staged.summary().inventory.root_session_id.clone();
let bindings: BTreeMap<String, Value> = staged
.summary()
.inventory
.sessions
.iter()
.map(|s| (s.id.clone(), json!({"workspace":"destination policy"})))
.collect();
let mut faults = sqlx::SqliteConnection::connect_with(
&sqlx::sqlite::SqliteConnectOptions::new().filename(&path),
)
.await
.unwrap();
sqlx::raw_sql(
"CREATE TRIGGER fail_bundle AFTER INSERT ON event_log BEGIN SELECT RAISE(ABORT,'fixture publication fault'); END;",
).execute(&mut faults).await.unwrap();
assert!(
log.import_bundle(staged, &binding, bindings.clone())
.await
.is_err()
);
for query in [
"SELECT COUNT(*) FROM session_bundle_imports",
"SELECT COUNT(*) FROM session_bundle_members",
"SELECT COUNT(*) FROM imported_invocations",
"SELECT COUNT(*) FROM session_history_copies",
] {
assert_eq!(
sqlx::query_scalar::<_, i64>(query)
.fetch_one(&mut faults)
.await
.unwrap(),
0
);
}
assert!(log.get_session::<Value>(&root).await.unwrap().is_none());
assert_eq!(
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM event_log")
.fetch_one(&mut faults)
.await
.unwrap(),
2
);
sqlx::query("DROP TRIGGER fail_bundle")
.execute(&mut faults)
.await
.unwrap();
faults.close().await.unwrap();
let receipt = log
.import_bundle(
StagedBundle::read(bytes).await.unwrap(),
&binding,
bindings.clone(),
)
.await
.unwrap();
assert_eq!(receipt.root_session_id, root);
assert_eq!(
log.bundle_import_receipt(&receipt.bundle_digest, &binding)
.await
.unwrap(),
Some(receipt.clone())
);
assert!(*log.subscribe_commits().borrow() > 2);
assert!(
log.session_catalog_changes(2, *log.subscribe_commits().borrow(), 128)
.await
.unwrap()
.is_empty(),
"historical import must not replay live catalog notifications"
);
assert!(log.session_copy_receipt(&root).await.unwrap().is_none());
assert_eq!(
log.get_session::<Value>(&root)
.await
.unwrap()
.unwrap()
.configuration,
bindings[&root]
);
assert_eq!(
log.import_bundle(
StagedBundle::read(bytes).await.unwrap(),
&binding,
bindings.clone()
)
.await
.unwrap(),
receipt
);
let mut changed = bindings.clone();
changed.insert(root.clone(), json!({"workspace":"another policy"}));
assert_eq!(
log.import_bundle(
StagedBundle::read(bytes).await.unwrap(),
&binding,
changed.clone()
)
.await
.unwrap(),
receipt,
"an accepted binding is not re-resolved against changed defaults"
);
assert!(matches!(
log.import_bundle(
StagedBundle::read(bytes).await.unwrap(),
&maka_agent_facts::artifact::content_digest(b"other destination"),
changed
)
.await,
Err(BundleError::Store(StoreError::SessionConflict))
));
reexport(&log, &root).await;
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.import_bundle(
StagedBundle::read(bytes).await.unwrap(),
&binding,
bindings.clone()
)
.await
.unwrap(),
receipt
);
let imported = log.prefix(1024, 8 * 1024 * 1024).await.unwrap();
let imported_bytes = serde_json::to_vec(&imported.events).unwrap();
let foreign = imported
.events
.iter()
.find(|stored| {
stored.event.invocation.session_id != "destination"
&& matches!(stored.event.fact, Fact::InvocationOpened { .. })
})
.unwrap()
.event
.invocation
.clone();
assert!(matches!(
log.append(
&EventWrite::plain(RuntimeEvent::new(
foreign,
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
}
))
.unwrap()
)
.await,
Err(CommitError::Rejected(reason))
if reason == "imported invocation is historical evidence, not locally accepted work"
));
let opening_a = local_event(
&root,
"local-a",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Code {
source: "first local invocation".into(),
},
},
);
let opening_b = local_event(
&root,
"local-b",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Code {
source: "second local invocation".into(),
},
},
);
let sequence_a = log.append(&opening_a).await.unwrap();
let active_prefix = log.prefix(1024, 8 * 1024 * 1024).await.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
serde_json::to_vec(&log.prefix(1024, 8 * 1024 * 1024).await.unwrap()).unwrap(),
serde_json::to_vec(&active_prefix).unwrap()
);
assert_eq!(log.append(&opening_a).await.unwrap(), sequence_a);
assert!(matches!(
log.append(&opening_b).await,
Err(CommitError::Rejected(reason))
if reason == "invalid event transition: session already has an unsealed invocation"
));
assert!(matches!(
log.set_session_archived::<Value>(&root, true, 4).await,
Err(StoreError::SessionBusy)
));
log.append(&local_event(
&root,
"local-a",
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
))
.await
.unwrap();
let sealed_prefix = log.prefix(1024, 8 * 1024 * 1024).await.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
serde_json::to_vec(&log.prefix(1024, 8 * 1024 * 1024).await.unwrap()).unwrap(),
serde_json::to_vec(&sealed_prefix).unwrap()
);
log.append(&opening_b).await.unwrap();
let before_replay = log.prefix(1024, 8 * 1024 * 1024).await.unwrap();
assert_eq!(
log.import_bundle(
StagedBundle::read(bytes).await.unwrap(),
&binding,
bindings.clone()
)
.await
.unwrap(),
receipt
);
assert_eq!(
serde_json::to_vec(&log.prefix(1024, 8 * 1024 * 1024).await.unwrap()).unwrap(),
serde_json::to_vec(&before_replay).unwrap()
);
assert_eq!(
serde_json::to_vec(&before_replay.events[..imported.events.len()]).unwrap(),
imported_bytes
);
log.append(&local_event(
&root,
"local-b",
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
))
.await
.unwrap();
// A new publication cannot append imported history after a canonical local owner.
let collision = EventLog::open(&dir.path().join("collision.sqlite"))
.await
.unwrap();
collision
.create_session(&root, "local-owner", &json!({}), 1)
.await
.unwrap();
collision
.append(&local_event(
&root,
"collision-local",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Code {
source: "existing local owner".into(),
},
},
))
.await
.unwrap();
let before_collision = collision.prefix(10, 16_384).await.unwrap();
assert!(matches!(
collision
.import_bundle(
StagedBundle::read(bytes).await.unwrap(),
&binding,
bindings.clone()
)
.await,
Err(BundleError::Store(StoreError::SessionConflict))
));
assert_eq!(
serde_json::to_vec(&collision.prefix(10, 16_384).await.unwrap()).unwrap(),
serde_json::to_vec(&before_collision).unwrap()
);
collision.close().await.unwrap();
let source = log.get_session::<Value>(&root).await.unwrap().unwrap();
log.copy_session(
SessionCopy {
source_session_id: root.clone(),
target_session_id: "after-import".into(),
expected_source_revision: source.revision,
purpose: CopyPurpose::Branch {
turn_id: None,
side_conversation: false,
},
},
&json!({}),
3,
)
.await
.unwrap();
log.begin_session_removal(&root, source.revision)
.await
.unwrap();
log.finish_session_retirement(&root).await.unwrap();
collect(&log).await;
// The original owner is gone, but the later native copy still owns every proof.
reexport(&log, "after-import").await;
assert_eq!(
log.import_bundle(StagedBundle::read(bytes).await.unwrap(), &binding, bindings)
.await
.unwrap(),
receipt
);
assert!(log.get_session::<Value>(&root).await.unwrap().is_none());
let descendant = log
.get_session::<Value>("after-import")
.await
.unwrap()
.unwrap();
log.begin_session_removal("after-import", descendant.revision)
.await
.unwrap();
log.finish_session_retirement("after-import").await.unwrap();
collect(&log).await;
let mut read = sqlx::SqliteConnection::connect_with(
&sqlx::sqlite::SqliteConnectOptions::new()
.filename(&path)
.read_only(true),
)
.await
.unwrap();
assert_eq!(sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM event_log e JOIN imported_invocations i USING(invocation_id) WHERE e.event_json IS NOT NULL",
).fetch_one(&mut read).await.unwrap(), 0);
read.close().await.unwrap();
log.close().await.unwrap();
}
async fn reexport(log: &EventLog, root: &str) {
let inventory = log.preview_bundle(root).await.unwrap();
let (bytes, _) = log
.export_bundle(root, &inventory.subtree_digest, Vec::new())
.await
.unwrap();
let mut staged = StagedBundle::read(bytes.as_slice()).await.unwrap();
staged.validate_history().await.unwrap();
staged.close().await.unwrap();
log.read_model_context(root, None).await.unwrap();
}
async fn collect(log: &EventLog) {
let mut cursor = None;
for _ in 0..256 {
match log
.collect_session_material(cursor.as_deref())
.await
.unwrap()
{
MaterialCollection::Done => return,
MaterialCollection::Retained(id) => cursor = Some(id),
MaterialCollection::Collected => {}
}
}
panic!("fixture material did not converge");
}
fn local_event(session: &str, id: &str, fact: Fact) -> EventWrite {
EventWrite::plain(RuntimeEvent::new(
Invocation {
session_id: session.into(),
turn_id: id.into(),
run_id: id.into(),
invocation_id: id.into(),
},
fact,
))
.unwrap()
}