use std::io;
use serde_json::{Value, json, value::RawValue};
use super::{dispatch, writes_tests::Fixture};
use crate::mcp::{rpc, types};
use crate::serve::protocol;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn history(
fixture: &mut Fixture,
selection: &types::Selection,
after: Option<&str>,
record: Option<&types::RecordRef>,
rows: u64,
) -> Result<types::ReadHistoryOutput, Box<dyn std::error::Error>> {
let request = rpc::ToolCall {
metadata: None,
name: "read_history".to_owned(),
arguments: serde_json::value::to_raw_value(
&json!({"selection":selection,"after":after,"record":record,"page":{"rows":rows,"bytes":65536}}),
)?,
};
let before = fixture.requests();
let answer = dispatch(&mut fixture.sender, &request)?;
assert_eq!(fixture.requests(), before + 2);
assert!(!answer.is_error, "{}", answer.structured_content.get());
let schema = crate::mcp::schema::tools()
.into_iter()
.find(|tool| tool.name == "read_history")
.ok_or_else(|| io::Error::other("history schema missing"))?;
let validator = jsonschema::validator_for(schema.output_schema.as_value())?;
let body: Value = serde_json::from_str(answer.structured_content.get())?;
validator
.validate(&body)
.map_err(|error| io::Error::other(error.to_string()))?;
Ok(serde_json::from_str(answer.structured_content.get())?)
}
#[test]
fn read_history_preserves_served_snapshot_roots_and_raw_record() -> TestResult {
let mut fixture = Fixture::start()?;
fixture.seed()?;
let raw = r#"{"n":1e2,"identity":18446744073709551616001}"#;
fixture.mutate(
"main",
"put-large",
protocol::Change::PutMany(protocol::DocumentImport {
collection: "records".to_owned(),
documents: vec![protocol::ImportedDocument {
document_id: "large".to_owned(),
document: RawValue::from_string(raw.to_owned())?,
}],
}),
)?;
let expected = fixture.current()?;
let output = history(
&mut fixture,
&types::Selection {
branch: "main".to_owned(),
version: Some(types::Version(expected)),
},
None,
Some(&types::RecordRef {
collection: "records".to_owned(),
document_id: "large".to_owned(),
}),
64,
)?;
assert_eq!(output.selection.version.0, expected);
let record = output
.record
.ok_or_else(|| io::Error::other("record missing"))?;
assert_eq!(record.document.get(), raw);
assert_eq!(record.record.document_id, "large");
assert!(!output.history.entries.is_empty());
for entry in output.history.entries {
assert_eq!(entry.branch, "main");
assert_eq!(entry.roots.len(), 1);
assert_eq!(entry.roots[0].shard, 0);
assert!(!entry.roots[0].root.is_empty());
let object = serde_json::to_value(entry)?;
assert_eq!(object.as_object().map(serde_json::Map::len), Some(3));
for absent in ["timestamp", "caller", "request_id", "change"] {
assert!(object.get(absent).is_none());
}
}
fixture.finish()
}
#[test]
fn read_history_continuation_stops_at_the_captured_version() -> TestResult {
let mut fixture = Fixture::start()?;
fixture.seed()?;
let expected = fixture.current()?;
let first = history(
&mut fixture,
&types::Selection {
branch: "main".to_owned(),
version: None,
},
None,
None,
1,
)?;
assert_eq!(first.selection.version.0, expected);
let mut entries = first.history.entries;
let mut after = first.history.next_after;
fixture.mutate(
"main",
"later-collection",
protocol::Change::Collection(protocol::CollectionWrite {
collection: "later".to_owned(),
schema: None,
}),
)?;
let retained = history(
&mut fixture,
&types::Selection {
branch: "main".to_owned(),
version: Some(types::Version(expected)),
},
None,
None,
64,
)?;
assert_eq!(
retained.history.entries.last().map(|entry| entry.version.0),
Some(expected)
);
assert_eq!(
u64::try_from(retained.history.entries.len())?,
expected.sequence + 1
);
while let Some(cursor) = after {
let answer = history(
&mut fixture,
&types::Selection {
branch: "main".to_owned(),
version: Some(types::Version(expected)),
},
Some(&cursor),
None,
1,
)?;
assert_eq!(answer.selection.version.0, expected);
after = answer.history.next_after;
entries.extend(answer.history.entries);
assert!(u64::try_from(entries.len())? <= expected.sequence + 1);
}
assert_eq!(entries.last().map(|entry| entry.version.0), Some(expected));
assert_eq!(u64::try_from(entries.len())?, expected.sequence + 1);
fixture.finish()
}