blob: 4ef7e96103a24b474880be62a130431d2c9f37e8 [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::*;
async fn dispatch(log: &EventLog, step: &str) -> String {
accepted(log, step, "Read").await;
let operation = format!("{step}:reused-provider-id");
log.append(&event(Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: operation.clone(),
call: ToolCallIdentity::provider(step.into(), "reused-provider-id".into()),
name: "Read".into(),
input: json!({}),
}))
.await
.unwrap();
operation
}
async fn open(log: &EventLog) {
log.append(&event(Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
source_messages: Vec::new(),
content: "read".into(),
request_fingerprint: None,
},
}))
.await
.unwrap();
}
#[tokio::test]
async fn ten_mib_raw_fragments_rebuild_after_restart_from_durable_facts() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
open(&log).await;
let operation = dispatch(&log, "read").await;
let raw = json!({"kind":"text","text":"x".repeat(10 * 1024 * 1024)});
let write = success(&operation, raw.clone());
let through = log.append(&write).await.unwrap();
let id = write.event().id.clone();
drop(write);
materialize_all(&log, through).await;
assert!(log.prefix(100, 16 * 1024 * 1024).await.is_ok());
let cache_id = id.clone();
let original: Vec<u8> = crate::test_store::inspect(&log, move |db| {
db.query_row(
"SELECT payload FROM transcript_cache.rows WHERE message_id = ?",
[&cache_id],
|row| row.get(0),
)
.unwrap()
})
.await;
let mut assembled = Vec::new();
while assembled.len() < original.len() {
let length = (original.len() - assembled.len()).min(512 * 1024);
let fragment = log
.transcript_fragment(
"session",
maka_agent_facts::history::RowPosition {
source_sequence: through,
item_index: 0,
},
assembled.len() as u64,
length as u64,
)
.await
.unwrap();
assembled.extend(fragment);
}
assert_eq!(assembled, original);
assert_eq!(
serde_json::from_slice::<Value>(&assembled).unwrap()["content"]["value"],
raw
);
crate::test_store::inspect(&log, move |db| {
db.execute("DELETE FROM transcript_cache.rows", []).unwrap()
})
.await;
crate::test_store::inspect(&log, move |db| {
db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows")
.unwrap()
})
.await;
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.resolve_tool_result("session", &id)
.await
.unwrap()
.to_json(),
raw
);
materialize_all(&log, through).await;
let rebuilt: Vec<u8> = crate::test_store::inspect(&log, move |db| {
db.query_row(
"SELECT payload FROM transcript_cache.rows WHERE message_id = ?",
[&id],
|row| row.get(0),
)
.unwrap()
})
.await;
assert_eq!(rebuilt, original);
}
#[tokio::test]
async fn selected_boundary_does_not_hydrate_an_earlier_sibling_payload() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
open(&log).await;
let operation = dispatch(&log, "sibling").await;
let sibling = success(&operation, json!({"text":"x".repeat(12 * 1024 * 1024)}));
let previous = log.append(&sibling).await.unwrap();
materialize_all(&log, previous).await;
let db = Connection::open(&path).unwrap();
db.execute(
"DELETE FROM tool_result_payloads WHERE event_id = ?",
[&sibling.event().id],
)
.unwrap();
let operation = dispatch(&log, "target").await;
let target = success(&operation, json!({"text":"target"}));
let through = log.append(&target).await.unwrap();
materialize_all(&log, through).await;
assert!(
log.resolve_tool_result("session", &sibling.event().id)
.await
.is_err()
);
let value: Vec<u8> = crate::test_store::inspect(&log, move |db| {
db.query_row(
"SELECT payload FROM transcript_cache.rows WHERE message_id = ?",
[&target.event().id],
|row| row.get(0),
)
.unwrap()
})
.await;
assert_eq!(
serde_json::from_slice::<Value>(&value).unwrap()["content"]["value"]["text"],
"target"
);
}
#[tokio::test]
async fn escaped_raw_and_complete_ui_row_survive_reopen_and_require_original_body() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
open(&log).await;
let operation = dispatch(&log, "control").await;
let raw = json!({"kind":"text","text":"\u{0000}".repeat(3 * 1024 * 1024)});
let write = success(&operation, raw.clone());
let through = log.append(&write).await.unwrap();
materialize_all(&log, through).await;
let cache_id = write.event().id.clone();
let projected: Vec<u8> = crate::test_store::inspect(&log, move |db| {
db.query_row(
"SELECT payload FROM transcript_cache.rows WHERE message_id = ?",
[&cache_id],
|row| row.get(0),
)
.unwrap()
})
.await;
let row: Value = serde_json::from_slice(&projected).unwrap();
assert!(
row["content"]["value"] == raw,
"escaped raw must be projected completely"
);
crate::test_store::inspect(&log, move |db| {
db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows")
.unwrap()
})
.await;
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert!(
log.resolve_tool_result("session", &write.event().id)
.await
.unwrap()
.to_json()
== raw,
"reopened raw result must retain its complete original value"
);
assert!(log.prefix(100, 32 * 1024 * 1024).await.is_ok());
materialize_all(&log, through).await;
let rebuilt: Vec<u8> = crate::test_store::inspect(&log, {
let cache_id = write.event().id.clone();
move |db| {
db.query_row(
"SELECT payload FROM transcript_cache.rows WHERE message_id = ?",
[&cache_id],
|row| row.get(0),
)
.unwrap()
}
})
.await;
assert!(
rebuilt == projected,
"reopened projection must retain exact escaped bytes"
);
// Remove the derived cache as well so the next read must hydrate original bytes.
let db = Connection::open(&path).unwrap();
db.execute(
"DELETE FROM tool_result_payloads WHERE event_id = ?",
[&write.event().id],
)
.unwrap();
crate::test_store::inspect(&log, move |db| {
db.execute_batch("DELETE FROM transcript_cache.text; DELETE FROM transcript_cache.rows")
.unwrap()
})
.await;
let error = crate::transcript_cache::materialize_all_rows(&log, "session", through)
.await
.expect_err(
"missing original body must fail hydration despite formerly valid derived rows",
);
assert!(
matches!(
&error,
maka_event_log::StoreError::InvalidTransition(message)
if message == "missing or inconsistent durable tool payload"
),
"unexpected hydration error: {error}"
);
assert!(
log.resolve_tool_result("session", &write.event().id)
.await
.is_err()
);
log.close().await.unwrap();
}
#[tokio::test]
async fn multiple_large_results_keep_conversation_metadata_readable() {
use maka_agent_ports::session::history::{HistoryView, Page, Position, Role};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("large-history.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
open(&log).await;
let mut ids = Vec::new();
let mut through = 0;
for index in 0..4 {
let operation = dispatch(&log, &format!("read-{index}")).await;
let result = success(&operation, json!({"text":"x".repeat(5 * 1024 * 1024)}));
through = log.append(&result).await.unwrap();
ids.push(result.event().id.clone());
}
assert_eq!(
crate::test_store::inspect(&log, move |db| {
db.query_row("SELECT count(*) FROM transcript_cache.rows", [], |row| {
row.get::<_, i64>(0)
})
.unwrap()
})
.await,
0
);
let Page::Ready { chunks, .. } = log
.history_view(
"session",
through,
Position::Start,
false,
HistoryView::Conversation,
128 * 1024,
None,
1024 * 1024,
)
.await
.unwrap();
let results: Vec<_> = chunks
.iter()
.filter(|chunk| chunk.role == Role::ToolResult)
.collect();
assert_eq!(results.len(), 4);
assert_eq!(
results
.iter()
.map(|chunk| chunk.message_id.clone())
.collect::<Vec<_>>(),
ids
);
assert!(
results
.iter()
.all(|chunk| chunk.tool.as_ref().unwrap().result.is_some() && chunk.text.is_empty())
);
assert_eq!(crate::test_store::inspect(&log, move |db| { db.query_row("SELECT count(*) FROM transcript_cache.rows body JOIN transcript_directory row USING(session_id,source_sequence,item_index) WHERE row.role IN ('tool_call','tool_result')",[],|row|row.get::<_,i64>(0)).unwrap() }).await,0,"conversation metadata must not hydrate large tool bodies");
log.close().await.unwrap();
}