blob: 756e117702707e269ce24e3e025caec30af30153 [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 super::*;
use maka_agent_facts::archive::projection_digest;
use sha2::{Digest, Sha256};
#[tokio::test]
async fn effective_digest_covers_archives_beyond_the_first_page() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("digest.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
open(&log, "history", false).await;
let mut archives = Vec::new();
for index in 0..129 {
let result = tool(&log, "history", &format!("step-{index}"), "x".repeat(8192)).await;
archives.push(archive("writer", &result));
}
end(&log, "history").await;
open(&log, "writer", false).await;
for archived in &archives {
log.append(archived).await.unwrap();
}
let after = log
.read_model_context("session", Some("writer"))
.await
.unwrap();
let archived: Vec<_> = after
.tail
.iter()
.filter_map(|entry| match entry {
ContextEvent::Archived(entry) => Some(entry),
_ => None,
})
.collect();
assert_eq!(archived.len(), 129);
// Check the durable digest contract against the exact append inputs and
// exposed replacements, including the record past the old page boundary.
let mut hash = Sha256::new();
hash.update(b"maka.effective-context.v1\0");
hash.update(after.source_evidence.digest.as_bytes());
for (actual, written) in archived.iter().zip(&archives) {
let Fact::ToolResultArchived { placeholder } = &written.event().fact else {
panic!()
};
assert_eq!(actual.event_id, placeholder.identity.runtime_event_id);
let encoded = serde_json::to_vec(&(
actual.sequence,
&actual.event_id,
placeholder,
projection_digest(&actual.replacement).unwrap(),
))
.unwrap();
hash.update((encoded.len() as u64).to_le_bytes());
hash.update(encoded);
}
assert_eq!(
after.effective_source_digest,
format!("sha256:{}", hex::encode(hash.finalize()))
);
let database = rusqlite::Connection::open(&path).unwrap();
end(&log, "writer").await;
let revision = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap()
.revision;
assert!(matches!(
log.copy_session(
maka_event_log::sessions::SessionCopy {
source_session_id: "session".into(),
target_session_id: "copy".into(),
expected_source_revision: revision,
purpose: maka_agent_facts::session::CopyPurpose::Branch {
turn_id: Some("history".into()),
side_conversation: false,
},
},
&json!({}),
2,
)
.await
.unwrap(),
maka_event_log::sessions::SessionCopyResult::Committed(_)
));
let writer = event(
"copy-writer",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
source_messages: Vec::new(),
content: "question".into(),
request_fingerprint: None,
},
},
);
let mut writer = writer.event().clone();
writer.invocation.session_id = "copy".into();
log.append(&EventWrite::plain(writer.clone()).unwrap())
.await
.unwrap();
log.read_model_context("copy", Some("copy-writer"))
.await
.unwrap();
// A copied target may match its pinned archive and an owner archive. Put
// the duplicate exactly across the 128/129 boundary without dropping the
// per-owner unique index or introducing an invalid archive writer.
assert_eq!(
database
.execute(
"INSERT INTO event_log(event_id,invocation_id,kind,operation_id,event_json)
SELECT 'duplicate-archive','copy-writer',kind,operation_id,
json_set(event_json,'$.id','duplicate-archive','$.invocation',json(?2))
FROM event_log WHERE event_id=?1",
[
&archives[127].event().id,
&serde_json::to_string(&writer.invocation).unwrap()
],
)
.unwrap(),
1
);
assert!(matches!(
log.read_model_context("copy", Some("copy-writer")).await,
Err(StoreError::Archive(
maka_event_log::archive::ArchiveError::Corrupt
))
));
assert_eq!(database.execute(
"UPDATE event_log SET event_json=json_set(event_json,'$.fact.placeholder.page.content','corrupted archive') WHERE event_id=?",
[&archives[128].event().id],
).unwrap(), 1);
assert!(matches!(
log.read_model_context("session", None).await,
Err(StoreError::Archive(
maka_event_log::archive::ArchiveError::Corrupt
))
));
log.close().await.unwrap();
}
#[tokio::test]
async fn copied_archive_rejects_target_with_wrong_event_kind() {
use maka_event_log::sessions::{SessionCopy, SessionCopyResult};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("copy-digest.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
open(&log, "history", false).await;
let result = tool(&log, "history", "step", "x".repeat(8192)).await;
end(&log, "history").await;
open(&log, "writer", false).await;
log.append(&archive("writer", &result)).await.unwrap();
end(&log, "writer").await;
let revision = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap()
.revision;
assert!(matches!(
log.copy_session(
SessionCopy {
source_session_id: "session".into(),
target_session_id: "copy".into(),
expected_source_revision: revision,
purpose: maka_agent_facts::session::CopyPurpose::Branch {
turn_id: Some("history".into()),
side_conversation: false,
},
},
&json!({}),
2
)
.await
.unwrap(),
SessionCopyResult::Committed(_)
));
let context = log.read_model_context("copy", None).await.unwrap();
let archived: Vec<_> = context
.tail
.iter()
.filter_map(|entry| match entry {
ContextEvent::Archived(entry) => Some(entry),
_ => None,
})
.collect();
assert_eq!(archived.len(), 1);
assert_eq!(archived[0].event_id, result.event().id);
let database = rusqlite::Connection::open(&path).unwrap();
assert_eq!(
database
.execute(
"UPDATE event_log SET kind='model_observed' WHERE event_id=?",
[&result.event().id],
)
.unwrap(),
1
);
assert!(matches!(
log.read_model_context("copy", None).await,
Err(StoreError::Archive(
maka_event_log::archive::ArchiveError::Corrupt
))
));
log.close().await.unwrap();
}