blob: 18f57049e11b04938c2220e7bb18a10b7c8cf619 [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::artifact::Artifact;
use maka_agent_facts::artifact::ArtifactKind;
use maka_agent_facts::artifact::ArtifactSource;
use maka_agent_facts::artifact::content_digest;
use maka_event_log::{
EventLog, StoreError,
artifacts::{ArtifactDeletion, ArtifactReadScope},
};
use serde_json::json;
use sqlx::{Connection, SqliteConnection, sqlite::SqliteConnectOptions};
fn artifact(id: &str, bytes: &[u8]) -> Artifact {
Artifact {
id: id.into(),
session_id: "session".into(),
turn_id: "upload".into(),
created_at: 10,
name: "file.bin".into(),
kind: ArtifactKind::File,
size_bytes: bytes.len() as u64,
mime_type: Some("application/octet-stream".into()),
source: ArtifactSource::UserUpload,
summary: Some(content_digest(bytes)),
}
}
#[tokio::test]
async fn large_upload_copy_reopens_with_complete_immutable_bytes() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("runtime.sqlite");
let log = EventLog::open(&path).await.unwrap();
for session in ["session", "copied"] {
log.create_session(session, session, &json!({}), 1)
.await
.unwrap();
}
let bytes = vec![0x5a; 50 * 1024 * 1024 + 1];
let record = artifact("large", &bytes);
let completed = async {
let committed = log.commit_artifact(record.clone(), bytes.clone()).await?;
let copied = log
.copy_plugin_attachment(
&maka_capabilities::storage::Namespace::new(
"upload.fixture",
maka_plugin_core::scope::Scope::Profile,
)
.unwrap(),
maka_agent_ports::execution::CopyAttachment {
target_session_id: "copied".into(),
attachment: maka_agent_facts::attachment::AttachmentRef {
kind: maka_agent_facts::attachment::AttachmentKind::Other,
name: "file.bin".into(),
mime_type: "application/octet-stream".into(),
bytes: bytes.len() as u64,
storage_ref: maka_agent_facts::attachment::StorageRef::SessionFile {
session_id: "session".into(),
relative_path: "large".into(),
},
},
},
)
.await?;
Ok::<_, StoreError>((committed, copied))
}
.await;
log.close().await.unwrap();
let (committed, copied) =
completed.expect("complete upload and immutable copy must be admitted");
assert_eq!(committed, record);
let maka_agent_facts::attachment::StorageRef::SessionFile {
session_id,
relative_path,
} = copied.storage_ref
else {
panic!("copy lost Session ownership")
};
let reopened = EventLog::open(&path).await.unwrap();
for (session, id) in [
("session", "large"),
(session_id.as_str(), relative_path.as_str()),
] {
let mut restored = Vec::new();
while restored.len() < bytes.len() {
let chunk = reopened
.read_artifact_chunk(session, id, restored.len() as u64, 64 * 1024)
.await
.unwrap()
.unwrap();
assert_eq!(chunk.total_bytes, 50 * 1024 * 1024 + 1);
restored.extend(chunk.bytes);
}
assert_eq!(restored, bytes);
assert_eq!(
reopened
.get_artifact(session, id)
.await
.unwrap()
.record
.unwrap()
.summary,
record.summary
);
}
reopened.close().await.unwrap();
}
#[tokio::test]
async fn immutable_uploads_are_scoped_bounded_and_reopen_with_atomic_catalog_changes() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("runtime.sqlite");
let log = EventLog::open(&path).await.unwrap();
for session in ["session", "other"] {
log.create_session(session, session, &json!({}), 1)
.await
.unwrap();
}
let initial = log.list_artifacts("session", 0, 128).await.unwrap();
let bytes: Vec<u8> = (0..100_000).map(|i| (i % 251) as u8).collect();
let first = artifact("first", &bytes);
assert_eq!(
log.commit_artifact(first.clone(), bytes.clone())
.await
.unwrap(),
first
);
let page = log.list_artifacts("session", 0, 128).await.unwrap();
assert_ne!(page.revision, initial.revision);
assert_eq!(page.records, std::slice::from_ref(&first));
let mut retry = first.clone();
retry.created_at = 99;
assert_eq!(
log.commit_artifact(retry, bytes.clone()).await.unwrap(),
first
);
assert_eq!(
log.list_artifacts("session", 0, 128)
.await
.unwrap()
.revision,
page.revision
);
let mut different = bytes.clone();
different[17] ^= 1;
assert!(matches!(
log.commit_artifact(first.clone(), different).await,
Err(StoreError::ArtifactConflict)
));
let mut renamed = first.clone();
renamed.name = "different".into();
assert!(matches!(
log.commit_artifact(renamed, bytes.clone()).await,
Err(StoreError::ArtifactConflict)
));
let read = log
.read_artifact_chunk("session", "first", 123, 32_768)
.await
.unwrap()
.unwrap();
assert_eq!(read.total_bytes, bytes.len() as u64);
assert_eq!(read.bytes, bytes[123..123 + 32_768]);
let mut invocation = maka_agent_facts::event::Invocation {
session_id: "session".into(),
turn_id: "upload".into(),
run_id: "run".into(),
invocation_id: "invocation".into(),
};
let scoped = log
.with_artifact(
ArtifactReadScope::Execution(&invocation),
"first",
|record, reader| {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).unwrap();
(record.clone(), bytes)
},
)
.await
.unwrap()
.unwrap();
assert_eq!(scoped.0, first);
assert_eq!(scoped.1, bytes);
invocation.turn_id = "different-turn".into();
assert!(
log.with_artifact(
ArtifactReadScope::Execution(&invocation),
"first",
|_, _| panic!("foreign Turn read")
)
.await
.unwrap()
.is_none()
);
invocation.turn_id = "upload".into();
invocation.session_id = "other".into();
assert!(
log.with_artifact(
ArtifactReadScope::Execution(&invocation),
"first",
|_, _| panic!("foreign Session read")
)
.await
.unwrap()
.is_none()
);
assert!(
log.read_artifact_chunk("session", "first", bytes.len() as u64, 1)
.await
.unwrap()
.unwrap()
.bytes
.is_empty()
);
assert!(
log.read_artifact_chunk("session", "first", bytes.len() as u64 + 1, 1)
.await
.is_err()
);
assert!(
log.get_artifact("other", "first")
.await
.unwrap()
.record
.is_none()
);
assert!(
log.read_artifact_chunk("other", "first", 0, 1)
.await
.unwrap()
.is_none()
);
assert_eq!(
log.delete_user_artifact("other", "first").await.unwrap(),
ArtifactDeletion::NotFound
);
let mut protected = artifact("protected", b"proof");
protected.source = ArtifactSource::ToolResultArchive;
protected.created_at = 11;
log.commit_artifact(protected.clone(), b"proof".to_vec())
.await
.unwrap();
let newer = log.list_artifacts("session", 0, 1).await.unwrap();
assert_eq!(newer.total, 2);
assert_eq!(newer.records, [protected.clone()]);
assert_eq!(
log.list_artifacts("session", 1, 1).await.unwrap().records,
std::slice::from_ref(&first)
);
assert_eq!(
log.delete_user_artifact("session", "protected")
.await
.unwrap(),
ArtifactDeletion::Protected
);
assert_eq!(
log.list_artifacts("session", 0, 128)
.await
.unwrap()
.revision,
newer.revision
);
// This control authority must not fabricate invocation facts.
assert_eq!(log.prefix(1, 1024).await.unwrap().high_water, 0);
log.close().await.unwrap();
let mut observer = SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&path))
.await
.unwrap();
let stored: (String, Vec<u8>) =
sqlx::query_as("SELECT record_json, payload FROM artifacts WHERE id = 'first'")
.fetch_one(&mut observer)
.await
.unwrap();
assert_eq!(serde_json::from_str::<Artifact>(&stored.0).unwrap(), first);
assert_eq!(stored.1, bytes);
observer.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.list_artifacts("session", 0, 128)
.await
.unwrap()
.revision,
newer.revision
);
assert_eq!(
log.delete_user_artifact("session", "first").await.unwrap(),
ArtifactDeletion::Deleted
);
let remaining = log.list_artifacts("session", 0, 128).await.unwrap();
assert_ne!(remaining.revision, newer.revision);
assert_eq!(remaining.records, [protected]);
assert!(
log.read_artifact_chunk("session", "first", 0, 1)
.await
.unwrap()
.is_none()
);
assert_eq!(
log.delete_user_artifact("session", "first").await.unwrap(),
ArtifactDeletion::NotFound
);
log.close().await.unwrap();
}
#[tokio::test]
async fn catalog_fault_rolls_back_payload_metadata_and_delete_as_one_commit() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("runtime.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
let before = log.list_artifacts("session", 0, 128).await.unwrap();
let mut observer = SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&path))
.await
.unwrap();
sqlx::raw_sql(
"CREATE TRIGGER reject_artifact_revision BEFORE INSERT ON artifact_catalog
BEGIN SELECT RAISE(ABORT, 'injected revision fault'); END;",
)
.execute(&mut observer)
.await
.unwrap();
let record = artifact("artifact", b"content");
assert!(
log.commit_artifact(record.clone(), b"content".to_vec())
.await
.is_err()
);
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM artifacts")
.fetch_one(&mut observer)
.await
.unwrap();
assert_eq!(
count, 0,
"metadata/payload insertion rolled back when revision failed"
);
assert_eq!(
log.list_artifacts("session", 0, 128)
.await
.unwrap()
.revision,
before.revision
);
sqlx::raw_sql("DROP TRIGGER reject_artifact_revision")
.execute(&mut observer)
.await
.unwrap();
log.commit_artifact(record.clone(), b"content".to_vec())
.await
.unwrap();
let committed = log.list_artifacts("session", 0, 128).await.unwrap();
sqlx::raw_sql(
"CREATE TRIGGER reject_artifact_revision BEFORE INSERT ON artifact_catalog
BEGIN SELECT RAISE(ABORT, 'injected deletion revision fault'); END;",
)
.execute(&mut observer)
.await
.unwrap();
assert!(
log.delete_user_artifact("session", "artifact")
.await
.is_err()
);
assert_eq!(
log.get_artifact("session", "artifact")
.await
.unwrap()
.record,
Some(record.clone())
);
assert_eq!(
log.read_artifact_chunk("session", "artifact", 0, 100)
.await
.unwrap()
.unwrap()
.bytes,
b"content"
);
assert_eq!(
log.list_artifacts("session", 0, 128)
.await
.unwrap()
.revision,
committed.revision
);
observer.close().await.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.get_artifact("session", "artifact")
.await
.unwrap()
.record,
Some(record)
);
assert_eq!(
log.list_artifacts("session", 0, 128)
.await
.unwrap()
.revision,
committed.revision
);
log.close().await.unwrap();
}
#[tokio::test]
async fn read_prefixes_are_literal_session_scoped_and_follow_artifact_visibility() {
use maka_agent_facts::read::ResourceAddress::Attachment;
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("prefix.sqlite");
let log = EventLog::open(&path).await.unwrap();
for session in ["session", "other"] {
log.create_session(session, session, &json!({}), 1)
.await
.unwrap();
}
for id in ["part_a", "part_ab", "partX", "Case"] {
log.commit_artifact(artifact(id, b"data"), b"data")
.await
.unwrap();
}
let mut hidden = artifact("part_hidden", b"hidden");
hidden.source = ArtifactSource::ToolResult;
log.commit_artifact(hidden, b"hidden").await.unwrap();
for (prefix, expected) in [
("part_a", Some("part_a")),
("part_ab", Some("part_ab")),
("part_h", None),
("case", None),
] {
assert_eq!(
log.resolve_read_resource("session", Attachment(prefix.into()))
.await
.unwrap(),
expected.map(|id| Attachment(id.into()))
);
}
assert!(matches!(
log.resolve_read_resource("session", Attachment("part_".into()))
.await,
Err(StoreError::AmbiguousReadResource)
));
assert!(
log.resolve_read_resource("other", Attachment("part_a".into()))
.await
.unwrap()
.is_none()
);
assert_eq!(
log.delete_user_artifact("session", "part_a").await.unwrap(),
ArtifactDeletion::Deleted
);
assert_eq!(
log.resolve_read_resource("session", Attachment("part_".into()))
.await
.unwrap(),
Some(Attachment("part_ab".into()))
);
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.resolve_read_resource("session", Attachment("part_".into()))
.await
.unwrap(),
Some(Attachment("part_ab".into()))
);
log.close().await.unwrap();
}
#[path = "artifacts/reads.rs"]
mod reads;