#915: get_node refuses a layer too large for maxBytes on its own; a window only an offset made says it is paging

This commit is contained in:
bflatastic
2026-10-24 13:01:33 +07:00
parent b6f5c29d19
commit b00d12b623
2 changed files with 68 additions and 8 deletions
+35 -8
View File
@@ -742,7 +742,7 @@ fn get_node(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<ToolResult, S
}
}
if used_wire + MARKER_RESERVE <= budget {
return Ok(ToolResult::json(&with_marker(value, depth, used, wire, used_wire, max_bytes, None)));
return Ok(ToolResult::json(&with_marker(value, depth, used, wire, used_wire, max_bytes, true, None)));
}
}
@@ -755,6 +755,16 @@ fn get_node(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<ToolResult, S
let after = offset + shown.max(1);
let next = (after < total).then_some(after);
let used_wire = wire_size(&value);
if used_wire + MARKER_RESERVE > budget {
// Even with every child left out, the layer's own fields (a very long name, say) pass the
// limit: an answer would be larger than the caller can read, so there is none.
return Err(format!(
"layer {id} takes {used_wire} bytes on the wire without its children, more than fit in maxBytes {max_bytes}; ask again with a larger `maxBytes`"
));
}
// Children left out after the window were cut by size; a window that only `offset` moved, of a
// subtree within the limit, is paging.
let over = wire > budget || next.is_some();
let mut out = with_marker(
value,
depth,
@@ -762,6 +772,7 @@ fn get_node(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<ToolResult, S
wire,
used_wire,
max_bytes,
over,
Some(json!({
"from": offset,
"shown": shown,
@@ -769,7 +780,7 @@ fn get_node(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<ToolResult, S
"nextOffset": next,
})),
);
if window && shown == total && offset == 0 && used == depth && used_wire + MARKER_RESERVE <= budget {
if window && shown == total && offset == 0 && used == depth {
// Nothing was left out after all (an offset of zero that fitted whole).
out.as_object_mut().map(|o| o.remove("truncated"));
}
@@ -801,9 +812,19 @@ fn children_window(value: &mut Value, offset: usize, budget: usize) -> bool {
true
}
/// `value` with the `truncated` object saying how it was cut. `children`: the window, when the
/// `value` with the `truncated` object saying how it was cut. `over`: the subtree asked for passed
/// the limit (else the window is only the `offset` asked for). `children`: the window, when the
/// children were cut as well as (or instead of) the depth.
fn with_marker(mut value: Value, asked: u64, used: u64, asked_wire: usize, used_wire: usize, max_bytes: usize, children: Option<Value>) -> Value {
fn with_marker(
mut value: Value,
asked: u64,
used: u64,
asked_wire: usize,
used_wire: usize,
max_bytes: usize,
over: bool,
children: Option<Value>,
) -> Value {
if let Some(obj) = value.as_object_mut() {
let mut marker = json!({
"requestedDepth": asked,
@@ -815,10 +836,16 @@ fn with_marker(mut value: Value, asked: u64, used: u64, asked_wire: usize, used_
match &children {
Some(window) => {
marker["children"] = window.clone();
marker["reason"] = json!(format!(
"the subtree at depth {asked} would be {asked_wire} bytes on the wire, over the {max_bytes}-byte limit a client can read in one message; its children are reported from {} instead",
window.get("from").and_then(Value::as_u64).unwrap_or(0)
));
let from = window.get("from").and_then(Value::as_u64).unwrap_or(0);
marker["reason"] = json!(if over {
format!(
"the subtree at depth {asked} would be {asked_wire} bytes on the wire, over the {max_bytes}-byte limit a client can read in one message; its children are reported from {from} instead"
)
} else {
format!(
"its children are reported from {from}, the `offset` asked for; the subtree at depth {asked} ({asked_wire} bytes) is within the {max_bytes}-byte limit"
)
});
marker["hint"] = json!("ask again with the `offset` in `children.nextOffset` for the rest, or get_node({id, depth}) on one child");
}
None => {
@@ -127,6 +127,7 @@ fn a_node_with_more_children_than_fit_is_windowed() {
assert_eq!(window["total"], json!(total), "{window}");
assert!(shown > 0 && shown < total, "some children, not all: {shown} of {total}");
assert_eq!(window["nextOffset"], json!(shown), "the offset to ask for next: {window}");
assert!(cut["truncated"]["reason"].as_str().unwrap_or("").contains("over the"), "cut by size: {}", cut["truncated"]);
// The layer itself is still there, not just a marker.
assert_eq!(cut["id"], whole["id"]);
@@ -183,3 +184,35 @@ fn children_too_large_to_fit_page_one_at_a_time() {
assert_eq!(page["children"].as_array().map_or(0, Vec::len), 0);
assert_eq!(page["truncated"]["children"]["total"], json!(1), "{}", page["truncated"]);
}
/// A layer whose own fields pass the limit with every child left out is refused: an answer would
/// be over maxBytes, and the marker would only make it larger.
#[test]
fn a_layer_too_large_on_its_own_is_refused() {
let mut c = Client::new();
let rect = c.create(json!({"type": "rectangle", "x": 0, "y": 0, "width": 20, "height": 20, "name": "n".repeat(3000)}));
let r = c.call("get_node", json!({"id": rect, "maxBytes": 2500}));
assert_eq!(r["isError"], true, "{r}");
assert!(r["content"][0]["text"].as_str().unwrap_or("").contains("maxBytes"), "{r}");
// A layer of ordinary size answers within the same limit.
let small = c.create(json!({"type": "rectangle", "x": 0, "y": 0, "width": 20, "height": 20}));
let (v, wire) = get_node(&mut c, json!({"id": small, "maxBytes": 2500}));
assert!(wire <= 2500 && v["truncated"].is_null(), "{wire}: {v}");
}
/// A window that only an `offset` made, of a subtree within the limit, is paging: the marker
/// doesn't say the limit was passed.
#[test]
fn an_offset_alone_pages_without_claiming_the_limit() {
let mut c = Client::new();
let frame = c.create(json!({"type": "frame", "x": 0, "y": 0, "width": 400, "height": 400}));
for i in 0..3 {
c.create(json!({"type": "rectangle", "parent": frame, "x": i * 5, "y": 5, "width": 20, "height": 20}));
}
let (page, _) = get_node(&mut c, json!({"id": frame, "depth": 1, "offset": 1}));
let t = &page["truncated"];
assert_eq!(page["children"].as_array().map_or(0, Vec::len), 2, "{t}");
assert!(t["children"]["nextOffset"].is_null(), "{t}");
let reason = t["reason"].as_str().unwrap_or("");
assert!(reason.contains("offset") && !reason.contains("over the"), "{reason}");
}