blob: 3e1cafec69d09566e375ace65e2635ed241917d1 [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_agent_facts::attachment::AttachmentKind;
use maka_agent_facts::attachment::AttachmentRef;
use maka_agent_facts::attachment::StorageRef;
use maka_agent_facts::composition::SourceKind;
use maka_agent_facts::composition::SourceRevision;
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::InputReceipt;
use maka_agent_facts::input::MessageInput;
use maka_agent_facts::input::QuoteRef;
use maka_agent_facts::input::SelectionSource;
use maka_agent_facts::message::MessageDisposition;
use maka_agent_facts::message::Placement;
use maka_agent_facts::message::RootSourceMessage;
use maka_agent_facts::message::SubmittedTurnIntent;
use maka_agent_facts::session::CopyPurpose;
use maka_event_log::{
EventLog,
message_admissions::PendingMessageAdmission,
message_queue::{QueueCommandKind, QueueEdit},
sessions::{SessionCopy, SessionCopyResult},
};
use serde_json::{Value, json};
#[path = "../support/message_queue.rs"]
mod queue;
fn resource(
invocation: &Invocation,
id: &str,
disposition: MessageDisposition,
) -> PendingMessageAdmission {
let mut pending = queue::admission(invocation, id, disposition);
pending.source.unprepared_content.quotes = Some(vec![QuoteRef {
text: format!("captured immutable {id}"),
label: Some(id.into()),
source_turn_id: None,
source: None,
}]);
pending.source.submitted_intent = Some(SubmittedTurnIntent {
input_selections: [("example.resources".into(), vec![format!("{id}:revision-1")])].into(),
input_selection_sources: vec![SelectionSource {
provider: "example.resources".into(),
package_id: "example".into(),
entry_id: "entry".into(),
activation: "original-activation".into(),
registration: uuid::Uuid::parse_str("12345678-1234-4234-8234-123456789abc").unwrap(),
session_id: invocation.session_id.clone(),
}],
turn_orchestration: None,
});
pending.source.message.content = prepared(&pending.source.unprepared_content);
pending
}
fn prepared(original: &MessageInput) -> MessageInput {
let mut content: MessageInput = format!("prepared {}", original.text).into();
content.quotes = original.quotes.clone();
content.preparation.push(InputReceipt {
source: SourceRevision {
kind: SourceKind::Input,
name: "example.resources".into(),
package_id: "example".into(),
entry_id: "entry".into(),
activation: "original-activation".into(),
revision: "original-revision".into(),
},
receipt: json!({"prepared":original.text}),
});
content
}
fn delivery(invocation: &Invocation, source: RootSourceMessage) -> RuntimeEvent {
RuntimeEvent::new(
invocation.clone(),
Fact::MessageSteered {
message: Box::new(source.message.clone()),
source: Some(Box::new(source)),
},
)
}
#[tokio::test]
async fn steering_source_validates_identity_provenance_and_preserves_large_receipts() {
let owner = queue::invocation("turn");
let pending = resource(&owner, "message", MessageDisposition::Steering);
EventWrite::plain(delivery(&owner, pending.source.clone())).unwrap();
let mut foreign = pending.source.clone();
foreign
.submitted_intent
.as_mut()
.unwrap()
.input_selection_sources[0]
.session_id = "child".into();
assert!(EventWrite::plain(delivery(&owner, foreign)).is_err());
let mut mismatch = delivery(&owner, pending.source.clone());
if let Fact::MessageSteered { message, .. } = &mut mismatch.fact {
message.content.text = "different delivered content".into();
}
assert!(EventWrite::plain(mismatch).is_err());
let mut wire = serde_json::to_value(delivery(&owner, pending.source.clone())).unwrap();
wire["fact"].as_object_mut().unwrap().remove("source");
let without_source: RuntimeEvent = serde_json::from_value(wire).unwrap();
assert!(matches!(
without_source.fact,
Fact::MessageSteered { source: None, .. }
));
EventWrite::plain(without_source).unwrap();
let mut large = pending.source.clone();
large.message.content.preparation[0].receipt = json!("x".repeat(540_000));
large.validate().unwrap();
large.message.validate().unwrap();
assert!(serde_json::to_vec(&large).unwrap().len() < 1024 * 1024);
let event = delivery(&owner, large.clone());
assert!(serde_json::to_vec(&event).unwrap().len() > 1024 * 1024);
let receipt_bytes = serde_json::to_vec(&large.message.content.preparation).unwrap();
let write = EventWrite::plain(event.clone()).unwrap();
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("large-source.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
queue::append(&log, &owner, queue::opening()).await;
log.admit_message(PendingMessageAdmission {
source: large.clone(),
..pending
})
.await
.unwrap();
let sequence = log.append(&write).await.unwrap();
assert_eq!(log.append(&write).await.unwrap(), sequence);
let proof = log
.steering_message("session", "message")
.await
.unwrap()
.unwrap();
assert_eq!(proof.sequence, sequence);
assert_eq!(proof.event, event);
let Fact::MessageSteered {
message,
source: Some(source),
} = &proof.event.fact
else {
unreachable!()
};
assert_eq!(source.as_ref(), &large);
assert!(serde_json::to_vec(&message.content.preparation).unwrap() == receipt_bytes);
assert!(log.pending_messages("session").await.unwrap().is_empty());
queue::append(
&log,
&owner,
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
)
.await;
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(log.append(&write).await.unwrap(), sequence);
let reopened = log
.steering_message("session", "message")
.await
.unwrap()
.unwrap();
assert_eq!(reopened.sequence, proof.sequence);
assert_eq!(reopened.event, proof.event);
log.close().await.unwrap();
}
#[tokio::test]
async fn edited_and_promoted_resource_sources_are_atomic_and_survive_restart_and_copies() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("source.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
let owner = queue::invocation("turn");
queue::append(&log, &owner, queue::opening()).await;
log.commit_artifact(
Artifact {
id: "upload".into(),
session_id: "session".into(),
turn_id: "upload".into(),
created_at: 1,
name: "original.txt".into(),
kind: ArtifactKind::File,
size_bytes: 5,
mime_type: Some("text/plain".into()),
source: ArtifactSource::UserUpload,
summary: Some(content_digest(b"hello")),
},
b"hello",
)
.await
.unwrap();
let mut first = resource(&owner, "first", MessageDisposition::Followup);
first.source.unprepared_content.attachments = Some(vec![AttachmentRef {
kind: AttachmentKind::Other,
name: "original.txt".into(),
mime_type: "text/plain".into(),
bytes: 5,
storage_ref: StorageRef::SessionFile {
session_id: "session".into(),
relative_path: "upload".into(),
},
}]);
let second = resource(&owner, "second", MessageDisposition::Steering);
for pending in [first.clone(), second.clone()] {
log.admit_message(pending).await.unwrap();
}
let mut edited = first.source.unprepared_content.clone();
edited.text = "edited first text".into();
let current = log.message_queue("session").await.unwrap();
let changed = log
.edit_message_queue(
"session",
current.revision,
QueueEdit::Update {
message_id: "first".into(),
content: Box::new(prepared(&edited)),
unprepared_content: Box::new(edited.clone()),
required_tools: Default::default(),
},
queue::command("edit", QueueCommandKind::Update),
)
.await
.unwrap();
log.edit_message_queue(
"session",
changed.revision,
QueueEdit::Promote {
message_id: "first".into(),
invocation: owner.clone(),
},
queue::command("promote", QueueCommandKind::Promote),
)
.await
.unwrap();
let pending = log.message_queue("session").await.unwrap();
let expected = pending
.entries
.iter()
.find(|p| p.source.message.message_id == "first")
.unwrap()
.source
.clone();
assert_eq!(expected.disposition, MessageDisposition::Steering);
assert_eq!(expected.submitted_placement, Placement::NextTurn);
assert_eq!(expected.submitted_intent, first.source.submitted_intent);
let mut stale = expected.clone();
stale.unprepared_content = first.source.unprepared_content.clone();
assert!(
log.append(&EventWrite::plain(delivery(&owner, stale)).unwrap())
.await
.is_err(),
"old unprepared input cannot replace the accepted edit"
);
let omitted = RuntimeEvent::new(
owner.clone(),
Fact::MessageSteered {
message: Box::new(expected.message.clone()),
source: None,
},
);
assert!(
log.append(&EventWrite::plain(omitted).unwrap())
.await
.is_err(),
"resource intent cannot be consumed without its canonical source"
);
assert_eq!(log.message_queue("session").await.unwrap(), pending);
let sql = rusqlite::Connection::open(&path).unwrap();
sql.execute_batch("CREATE TRIGGER refuse_source_consumption BEFORE DELETE ON message_admissions BEGIN SELECT RAISE(ABORT, 'source rollback'); END;").unwrap();
assert!(log.commit_pending_steering(&owner).await.is_err());
assert_eq!(log.message_queue("session").await.unwrap(), pending);
assert!(
log.root_message("session", "first")
.await
.unwrap()
.is_none()
);
sql.execute_batch("DROP TRIGGER refuse_source_consumption")
.unwrap();
assert_eq!(log.commit_pending_steering(&owner).await.unwrap(), 2);
assert_eq!(log.commit_pending_steering(&owner).await.unwrap(), 0);
assert!(log.pending_messages("session").await.unwrap().is_empty());
for source in [&expected, &second.source] {
assert_eq!(
log.root_message("session", &source.message.message_id)
.await
.unwrap()
.unwrap()
.source(),
source
);
}
queue::append(
&log,
&owner,
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
)
.await;
let revision = log
.get_session::<Value>("session")
.await
.unwrap()
.unwrap()
.revision;
for (target, purpose) in [
(
"branch",
CopyPurpose::Branch {
turn_id: Some("turn".into()),
side_conversation: false,
},
),
(
"revision",
CopyPurpose::Revision {
turn_id: "turn".into(),
},
),
] {
assert!(matches!(
log.copy_session(
SessionCopy {
source_session_id: "session".into(),
target_session_id: target.into(),
expected_source_revision: revision,
purpose,
},
&json!({}),
2
)
.await
.unwrap(),
SessionCopyResult::Committed(_)
));
}
let inventory = log.preview_bundle("revision").await.unwrap();
let (bytes, _) = log
.export_bundle("revision", &inventory.subtree_digest, Vec::new())
.await
.unwrap();
let mut staged = maka_event_log::bundle::StagedBundle::read(bytes.as_slice())
.await
.unwrap();
staged.validate_history().await.unwrap();
staged.close().await.unwrap();
log.delete_user_artifact("session", "upload").await.unwrap();
log.close().await.unwrap();
sql.execute_batch("DELETE FROM message_sources;").unwrap();
drop(sql);
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.root_message("session", "first")
.await
.unwrap()
.unwrap()
.source(),
&expected
);
for target in ["branch", "revision"] {
let first = log
.editable_message(target, "turn", "first")
.await
.unwrap()
.unwrap();
assert_eq!(first.content.text, edited.text);
assert_eq!(first.content.quotes, edited.quotes);
assert!(first.content.preparation.is_empty());
assert_eq!(
first.intent, expected.submitted_intent,
"viewing a child Session cannot rebind the original source identity"
);
assert!(
matches!(&first.content.attachments.as_ref().unwrap()[0].storage_ref,
StorageRef::SessionFile { session_id, .. } if session_id == target)
);
let second_input = log
.editable_message(target, "turn", "second")
.await
.unwrap()
.unwrap();
assert_eq!(second_input.content, second.source.unprepared_content);
assert_eq!(second_input.intent, second.source.submitted_intent);
assert!(log.root_message(target, "first").await.unwrap().is_none());
}
log.close().await.unwrap();
}