use std::io;
use serde_json::{Value, json, value::RawValue};
use super::{ServiceRefusal, dispatch, writes_tests::Fixture};
use crate::mcp::{rpc, types};
use crate::serve::protocol;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn review(
fixture: &mut Fixture,
version: Option<types::Version>,
cursors: &types::ReviewCursors,
) -> Result<types::ReviewBranchOutput, Box<dyn std::error::Error>> {
let call = rpc::ToolCall {
metadata: None,
name: "review_branch".to_owned(),
arguments: serde_json::value::to_raw_value(
&json!({"selection":{"branch":"feature","version":version},"page":{"rows":64,"bytes":65536},"after":cursors}),
)?,
};
let before = fixture.requests();
let result = dispatch(&mut fixture.sender, &call)?;
assert_eq!(fixture.requests(), before + 4);
assert!(!result.is_error, "{}", result.structured_content.get());
let schema = crate::mcp::schema::tools()
.into_iter()
.find(|tool| tool.name == "review_branch")
.ok_or_else(|| io::Error::other("review schema missing"))?;
let validator = jsonschema::validator_for(schema.output_schema.as_value())?;
let body: Value = serde_json::from_str(result.structured_content.get())?;
validator
.validate(&body)
.map_err(|error| io::Error::other(error.to_string()))?;
Ok(serde_json::from_str(result.structured_content.get())?)
}
fn put(fixture: &Fixture, branch: &str, request: &str, number: u64) -> TestResult {
fixture.mutate(
branch,
request,
protocol::Change::PutMany(protocol::DocumentImport {
collection: "records".to_owned(),
documents: vec![protocol::ImportedDocument {
document_id: "a".to_owned(),
document: RawValue::from_string(format!("{{\"n\":{number}}}"))?,
}],
}),
)?;
Ok(())
}
#[test]
fn review_branch_preserves_relation_owners_and_native_presence() -> TestResult {
let mut fixture = Fixture::start()?;
fixture.seed()?;
fixture.fork("feature")?;
fixture.mutate(
"feature",
"new-relation",
protocol::Change::RelationPut(protocol::Relation {
relation: "more_links".to_owned(),
from_collection: "records".to_owned(),
to_collection: "records".to_owned(),
cardinality: protocol::Cardinality::ManyToMany,
}),
)?;
let output = review(&mut fixture, None, &types::ReviewCursors::default())?;
assert!(output.diff.entries.iter().any(|line| matches!(&line.owner, types::ReviewOwner::Relation { relation } if relation == "more_links")));
assert!(output.conflicts.entries.iter().any(|line| matches!(&line.owner, types::ReviewOwner::Relation { relation } if relation == "more_links") && matches!(line.state, types::ConflictState::SourceOnly)));
assert!(
serde_json::to_value(&output.counts.partial)?
.get("dangling")
.is_none()
);
assert_eq!(output.target.branch, "main");
fixture.finish()
}
#[test]
fn review_branch_keeps_all_conflict_values_state_token_and_distance() -> TestResult {
let mut fixture = Fixture::start()?;
fixture.seed()?;
fixture.fork("feature")?;
put(&fixture, "feature", "source-put", 2)?;
put(&fixture, "main", "target-put", 3)?;
let output = review(&mut fixture, None, &types::ReviewCursors::default())?;
let conflict = output
.conflicts
.entries
.iter()
.find(|line| matches!(line.state, types::ConflictState::Conflict))
.ok_or_else(|| io::Error::other("conflict missing"))?;
assert_eq!(
serde_json::to_value(&conflict.ancestor)?,
json!({"kind":"value","value":{"n":1}})
);
assert_eq!(
serde_json::to_value(&conflict.target)?,
json!({"kind":"value","value":{"n":3}})
);
assert_eq!(
serde_json::to_value(&conflict.source)?,
json!({"kind":"value","value":{"n":2}})
);
assert!(conflict.choice_token.is_some());
assert!(
matches!(&conflict.owner, types::ReviewOwner::Document { record } if record.collection == "records" && record.document_id == "a")
);
assert_eq!(output.counts.partial.conflicts, 1);
assert_eq!(output.base_moved, Some(1));
fixture.finish()
}
#[test]
fn review_branch_retains_conditional_dangling_findings() -> TestResult {
let mut fixture = Fixture::start()?;
fixture.seed()?;
fixture.fork("feature")?;
fixture.mutate(
"feature",
"source-delete",
protocol::Change::Delete(protocol::DocumentDelete {
collection: "records".to_owned(),
document_id: "a".to_owned(),
}),
)?;
put(&fixture, "main", "target-put", 3)?;
fixture.mutate(
"main",
"target-link",
protocol::Change::EdgePut(protocol::Edge {
relation: "links".to_owned(),
from: protocol::Node {
collection: "records".to_owned(),
document_id: "a".to_owned(),
},
to: protocol::Node {
collection: "records".to_owned(),
document_id: "b".to_owned(),
},
properties: json!({}),
}),
)?;
let first = review(&mut fixture, None, &types::ReviewCursors::default())?;
let selected = first.selection.version;
let mut findings = first.dangling.entries;
let mut after = first.dangling.next_after;
let mut pages = 1;
while let Some(cursor) = after {
let output = review(
&mut fixture,
Some(selected),
&types::ReviewCursors {
dangling: Some(cursor),
..types::ReviewCursors::default()
},
)?;
after = output.dangling.next_after;
findings.extend(output.dangling.entries);
pages += 1;
assert!(
pages <= 8,
"bounded fixture did not complete its served cursor"
);
}
let finding = findings
.iter()
.find(|line| matches!(line.state, types::DanglingState::DependsOnChoice))
.ok_or_else(|| io::Error::other("conditional dangling finding missing"))?;
assert_eq!(finding.relation, "links");
assert_eq!(finding.from.document_id, "a");
assert_eq!(finding.to.document_id, "b");
assert!(matches!(finding.missing_end, types::MissingEnd::From));
assert!(matches!(finding.removed_by, types::RemovedBy::Source));
assert!(!finding.choice_token.is_empty());
fixture.finish()
}
#[test]
fn every_registered_tool_reaches_a_served_method_without_not_ready() -> TestResult {
let mut fixture = Fixture::start()?;
fixture.seed()?;
fixture.fork("feature")?;
let inputs = types::tests::inputs();
assert_eq!(inputs.len(), 11);
for (name, arguments) in inputs {
let call = rpc::ToolCall {
metadata: None,
name: name.to_owned(),
arguments: RawValue::from_string(arguments)?,
};
let before = fixture.requests();
let result = dispatch(&mut fixture.sender, &call)?;
assert!(
fixture.requests() > before,
"{name} did not reach a served method"
);
if result.is_error {
let error: ServiceRefusal = serde_json::from_str(result.structured_content.get())?;
assert!(
![
"mcp_tool_not_ready",
"mcp_tool_unknown",
"mcp_arguments_invalid"
]
.contains(&error.code.as_str()),
"{name}: {}",
error.code
);
}
}
fixture.finish()
}