From fe48c01facb37d333accfcb585212f93ace1beba Mon Sep 17 00:00:00 2001 From: wsp Date: Wed, 7 Oct 2026 14:10:59 +0800 Subject: [PATCH] fix(tool-call-jsonrepair): sync fork with upstream 0.2.5 Refresh the local parser and non-CLI regression coverage to the upstream 0.2.5 baseline while preserving OpenBitFun's tool-call behavior: - keep comment markers as argument content in the tool-call profile - include parser fixes for truncated URLs, invalid numbers, Unicode, control characters, recursion, and separator rollback - omit the upstream schema-guided correction API Update the README and add dedicated profile regression coverage. --- .../execution/tool-call-jsonrepair/README.md | 16 ++-- .../execution/tool-call-jsonrepair/src/lib.rs | 32 ++++---- .../tool-call-jsonrepair/src/parser/array.rs | 3 +- .../tool-call-jsonrepair/src/parser/mod.rs | 30 ++----- .../tool-call-jsonrepair/src/parser/number.rs | 14 +++- .../tool-call-jsonrepair/src/parser/object.rs | 3 +- .../tool-call-jsonrepair/src/parser/string.rs | 81 +++++++++++++++++-- .../src/parser/toplevel.rs | 10 ++- .../tests/number_regression_tests.rs | 10 +++ .../tests/repair_tests.rs | 74 +++++++++++++++++ .../tests/streaming_tests.rs | 31 +++++-- .../tests/tool_call_profile_tests.rs | 20 +++++ .../tests/writer_tests.rs | 2 +- 13 files changed, 263 insertions(+), 63 deletions(-) create mode 100644 src/crates/execution/tool-call-jsonrepair/tests/tool_call_profile_tests.rs diff --git a/src/crates/execution/tool-call-jsonrepair/README.md b/src/crates/execution/tool-call-jsonrepair/README.md index ecfbf74637..f843337c33 100644 --- a/src/crates/execution/tool-call-jsonrepair/README.md +++ b/src/crates/execution/tool-call-jsonrepair/README.md @@ -1,7 +1,7 @@ # Tool-call JSON repair This crate is a local fork of -[`jsonrepair-rs` 0.2.1](https://github.com/majiayu000/jsonrepair-rs), licensed +[`jsonrepair-rs` 0.2.5](https://github.com/majiayu000/jsonrepair-rs), licensed under MIT. The upstream license is retained in [`LICENSE`](LICENSE). It retains the upstream generic repair API for compatibility, and adds @@ -14,9 +14,15 @@ The profile still supports bounded syntax recovery needed for malformed model tool arguments, including missing string quotes, commas, and closing delimiters. The caller must parse and schema-validate the result before use. +The fork keeps the upstream 0.2.5 parser fixes and regression coverage, while +retaining the separate comment-free tool-call profile. Schema-guided correction +from upstream 0.2.5 is intentionally not included; tool arguments are parsed +and validated by the owning tool pipeline. + ## Upstream regression coverage -The non-CLI regression tests and parity fixture from `jsonrepair-rs` 0.2.1 are -vendored under `tests/`. They differ only in the local crate import path. The -upstream CLI tests are intentionally excluded because this internal library -sets `autobins = false` and does not ship the upstream command-line program. +The non-CLI parser, streaming, and parity regression tests from +`jsonrepair-rs` 0.2.5 are vendored under `tests/`, with the local crate import +path adjusted. Schema-specific tests and the upstream CLI tests are omitted: +this internal library does not expose the schema helper, sets `autobins = false`, +and does not ship the upstream command-line program. diff --git a/src/crates/execution/tool-call-jsonrepair/src/lib.rs b/src/crates/execution/tool-call-jsonrepair/src/lib.rs index 8449c9276b..2ad2601889 100644 --- a/src/crates/execution/tool-call-jsonrepair/src/lib.rs +++ b/src/crates/execution/tool-call-jsonrepair/src/lib.rs @@ -1,6 +1,6 @@ //! # OpenBitFun tool-call JSON repair //! -//! A local MIT-licensed fork of `jsonrepair-rs` 0.2.1 for streamed tool +//! A local MIT-licensed fork of `jsonrepair-rs` 0.2.5 for streamed tool //! arguments. The tool-call profile preserves Markdown-like `#` content rather //! than treating it as a configuration-file comment. //! @@ -304,6 +304,21 @@ where serde_json::from_str(&repaired).map_err(JsonRepairParseError::from) } +#[cfg(test)] +mod tests { + use super::repair_tool_call_json; + + #[test] + fn tool_call_profile_preserves_hash_prefixed_unquoted_string_values() { + let repaired = repair_tool_call_json(r##"{"plan": # Markdown heading"}"##) + .expect("repair should succeed"); + let value: serde_json::Value = + serde_json::from_str(&repaired).expect("repaired output should be JSON"); + + assert_eq!(value["plan"], "# Markdown heading"); + } +} + fn reject_if_changed(input: &str, repaired: &str) -> Result<(), JsonRepairError> { if input == repaired { return Ok(()); @@ -347,18 +362,3 @@ fn line_column(input: &str, position: usize) -> (usize, usize) { (line, column) } - -#[cfg(test)] -mod tests { - use super::repair_tool_call_json; - - #[test] - fn tool_call_profile_preserves_hash_prefixed_unquoted_string_values() { - let repaired = repair_tool_call_json(r##"{"plan": # Markdown heading"}"##) - .expect("repair should succeed"); - let value: serde_json::Value = - serde_json::from_str(&repaired).expect("repaired output should be JSON"); - - assert_eq!(value["plan"], "# Markdown heading"); - } -} diff --git a/src/crates/execution/tool-call-jsonrepair/src/parser/array.rs b/src/crates/execution/tool-call-jsonrepair/src/parser/array.rs index 5f66658456..9c195bbb13 100644 --- a/src/crates/execution/tool-call-jsonrepair/src/parser/array.rs +++ b/src/crates/execution/tool-call-jsonrepair/src/parser/array.rs @@ -11,6 +11,7 @@ impl JsonRepairer { self.enter_container()?; self.output.push('['); + let frame_start = self.output.len(); self.pos += 1; self.parse_whitespace_and_comments(); @@ -41,7 +42,7 @@ impl JsonRepairer { let processed_value = self.parse_value()?; if !processed_value { // Trailing comma or truncated input. - self.strip_trailing_comma(); + self.strip_trailing_comma(frame_start); break; } } diff --git a/src/crates/execution/tool-call-jsonrepair/src/parser/mod.rs b/src/crates/execution/tool-call-jsonrepair/src/parser/mod.rs index 1c13135b2b..e0fced4c95 100644 --- a/src/crates/execution/tool-call-jsonrepair/src/parser/mod.rs +++ b/src/crates/execution/tool-call-jsonrepair/src/parser/mod.rs @@ -284,39 +284,23 @@ impl JsonRepairer { true } - /// Remove last occurrence of `c` from output. - pub(super) fn strip_last_occurrence(&mut self, c: char) { - if let Some(idx) = self.output.rfind(c) { - self.output.remove(idx); - } - } - /// Fast path for the common trailing-comma rollback case. - /// Removes a comma only when it's the last non-whitespace output char. - pub(super) fn strip_trailing_comma(&mut self) { + /// Never remove a separator before the current container's output. + pub(super) fn strip_trailing_comma(&mut self, min_index: usize) { let bytes = self.output.as_bytes(); - if let Some(&last) = bytes.last() { - if last == b',' { - self.output.pop(); - return; - } - if !matches!(last, b' ' | b'\n' | b'\r' | b'\t') { - self.strip_last_occurrence(','); - return; - } - } - let mut idx = bytes.len(); - while idx > 0 && matches!(bytes[idx - 1], b' ' | b'\n' | b'\r' | b'\t') { + while idx > min_index && matches!(bytes[idx - 1], b' ' | b'\n' | b'\r' | b'\t') { idx -= 1; } - if idx > 0 && bytes[idx - 1] == b',' { + if idx > min_index && bytes[idx - 1] == b',' { self.output.remove(idx - 1); return; } - self.strip_last_occurrence(','); + if let Some(relative_index) = self.output[min_index..].rfind(',') { + self.output.remove(min_index + relative_index); + } } /// Insert `text` before any trailing whitespace in the output buffer. diff --git a/src/crates/execution/tool-call-jsonrepair/src/parser/number.rs b/src/crates/execution/tool-call-jsonrepair/src/parser/number.rs index 57dec6de46..cfebf4579b 100644 --- a/src/crates/execution/tool-call-jsonrepair/src/parser/number.rs +++ b/src/crates/execution/tool-call-jsonrepair/src/parser/number.rs @@ -30,6 +30,10 @@ impl JsonRepairer { let mut append_trailing_zero = false; let mut has_leading_dot = false; let mut has_invalid_leading_zero = false; + // Exponent (`e`/`E`) is only valid after at least one integer or fraction + // digit. Without this guard, plus-prefixed forms like `+e` / `+e1` (after + // `parse_plus_number` strips `+`) would emit invalid JSON such as `e0`. + let mut has_digit = false; if self.peek() == Some('-') { self.pos += 1; @@ -55,6 +59,7 @@ impl JsonRepairer { return Ok(false); } + has_digit = true; while self.pos < len && chars::is_digit(self.chars[self.pos]) { self.pos += 1; } @@ -75,6 +80,9 @@ impl JsonRepairer { integer_digits += 1; self.pos += 1; } + if integer_digits > 0 { + has_digit = true; + } if self.pos < len && self.chars[self.pos] == '.' { self.pos += 1; @@ -84,6 +92,7 @@ impl JsonRepairer { self.pos = start; return Ok(false); } else { + has_digit = true; while self.pos < len && chars::is_digit(self.chars[self.pos]) { self.pos += 1; } @@ -91,7 +100,10 @@ impl JsonRepairer { } } - if !append_trailing_zero && self.pos < len && matches!(self.chars[self.pos], 'e' | 'E') + if !append_trailing_zero + && has_digit + && self.pos < len + && matches!(self.chars[self.pos], 'e' | 'E') { self.pos += 1; if self.pos < len && matches!(self.chars[self.pos], '-' | '+') { diff --git a/src/crates/execution/tool-call-jsonrepair/src/parser/object.rs b/src/crates/execution/tool-call-jsonrepair/src/parser/object.rs index 7e73715028..27ebcb6da6 100644 --- a/src/crates/execution/tool-call-jsonrepair/src/parser/object.rs +++ b/src/crates/execution/tool-call-jsonrepair/src/parser/object.rs @@ -12,6 +12,7 @@ impl JsonRepairer { self.enter_container()?; self.output.push('{'); + let frame_start = self.output.len(); self.pos += 1; self.parse_whitespace_and_comments(); @@ -43,7 +44,7 @@ impl JsonRepairer { || matches!(self.peek(), Some('}') | Some('{') | Some(']') | Some('[')); if near_end { // Trailing comma. - self.strip_trailing_comma(); + self.strip_trailing_comma(frame_start); } else { return Err(self.error_kind( "Object key expected", diff --git a/src/crates/execution/tool-call-jsonrepair/src/parser/string.rs b/src/crates/execution/tool-call-jsonrepair/src/parser/string.rs index 93de754c80..0b9a48c68d 100644 --- a/src/crates/execution/tool-call-jsonrepair/src/parser/string.rs +++ b/src/crates/execution/tool-call-jsonrepair/src/parser/string.rs @@ -96,7 +96,7 @@ impl JsonRepairer { .and_then(|idx| self.prev_non_whitespace_index(idx)); let prev_char = prev_non_ws.and_then(|idx| self.peek_at(idx)); - if prev_char == Some(',') { + if prev_char == Some(',') && stop_at_index != prev_non_ws { // {"a":"b,c,"d":"e"} -> stop at comma before quote. self.pos = input_start; self.output.truncate(output_start); @@ -116,9 +116,12 @@ impl JsonRepairer { self.pos = quote_pos + 1; } else if stop_at_delimiter && chars::is_unquoted_string_delimiter(c) { // URL like "https://..." should not stop at '/'. - if self.pos > input_start + 1 - && self.peek_at(self.pos.saturating_sub(1)) == Some(':') - && self.looks_like_url_start(input_start + 1, self.pos) + if c == '/' + && self.pos > input_start + 1 + && self.peek_at(self.pos - 1) == Some(':') + && (self.looks_like_url_start(input_start + 1, self.pos) + || (self.pos + 1 == self.chars.len() + && self.ends_with_url_scheme(input_start + 1, self.pos))) { while self.peek().is_some_and(chars::is_url_char) { self.output.push(self.chars[self.pos]); @@ -173,6 +176,47 @@ impl JsonRepairer { } if digits == 4 { + let code_unit = self.hex_quad(self.pos + 1).ok_or_else(|| { + self.error_at_kind( + "Invalid unicode escape", + backslash_pos, + JsonRepairErrorKind::InvalidUnicode, + ) + })?; + if (0xD800..=0xDBFF).contains(&code_unit) { + let next = self.pos + 5; + let low = if self.peek_at(next) == Some('\\') + && self.peek_at(next + 1) == Some('u') + { + self.hex_quad(next + 2) + } else { + None + }; + if !low.is_some_and(|unit| (0xDC00..=0xDFFF).contains(&unit)) { + return Err(self.error_at_kind( + "Invalid unicode surrogate pair", + backslash_pos, + JsonRepairErrorKind::InvalidUnicode, + )); + } + self.output.push_str("\\u"); + for i in 0..4 { + self.output.push(self.chars[self.pos + 1 + i]); + } + self.output.push_str("\\u"); + for i in 0..4 { + self.output.push(self.chars[next + 2 + i]); + } + self.pos += 11; + return Ok(()); + } + if (0xDC00..=0xDFFF).contains(&code_unit) { + return Err(self.error_at_kind( + "Invalid unicode surrogate pair", + backslash_pos, + JsonRepairErrorKind::InvalidUnicode, + )); + } self.output.push_str("\\u"); for i in 0..4 { self.output.push(self.chars[self.pos + 1 + i]); @@ -202,13 +246,22 @@ impl JsonRepairer { } _ => { // Invalid escape: drop '\' and keep char. - self.output.push(esc); + self.push_string_char(esc); self.pos += 1; } } Ok(()) } + fn hex_quad(&self, start: usize) -> Option { + let mut value = 0u16; + for offset in 0..4 { + let digit = self.peek_at(start + offset)?.to_digit(16)? as u16; + value = value * 16 + digit; + } + Some(value) + } + fn parse_string_char(&mut self, c: char) -> Result<()> { if c >= '\u{0020}' && c != '"' && c != '\\' { self.output.push(c); @@ -324,6 +377,13 @@ impl JsonRepairer { self.pos -= 1; } + // A whitespace-only token has no value. Form feed is trimmed here but + // is not consumed by parse_whitespace_and_comments. Returning false + // prevents the array parser from looping at an unchanged position. + if self.pos == start { + return Ok(false); + } + // Compare directly on char slice — no String allocation. if !is_key && self.slice_eq(start, self.pos, "undefined") { self.output.push_str("null"); @@ -395,6 +455,17 @@ impl JsonRepairer { || self.matches_at(start, "irc://") } + fn ends_with_url_scheme(&self, start: usize, slash_idx: usize) -> bool { + [ + "http:", "https:", "ftp:", "mailto:", "file:", "data:", "irc:", + ] + .iter() + .any(|scheme| { + slash_idx >= start + scheme.len() + && self.slice_eq(slash_idx - scheme.len(), slash_idx, scheme) + }) + } + fn is_known_wrapper_function(&self, start: usize, end: usize) -> bool { self.slice_starts_with_ignore_ascii_case(start, end, "callback") || self.slice_eq_ignore_ascii_case(start, end, "cb") diff --git a/src/crates/execution/tool-call-jsonrepair/src/parser/toplevel.rs b/src/crates/execution/tool-call-jsonrepair/src/parser/toplevel.rs index 024433913c..0743a86426 100644 --- a/src/crates/execution/tool-call-jsonrepair/src/parser/toplevel.rs +++ b/src/crates/execution/tool-call-jsonrepair/src/parser/toplevel.rs @@ -39,7 +39,7 @@ impl JsonRepairer { self.parse_ndjson()?; } else if processed_comma { // Remove trailing comma after a single root value. - self.strip_trailing_comma(); + self.strip_trailing_comma(0); } // Repair redundant closing brackets at the root level. @@ -67,12 +67,16 @@ impl JsonRepairer { fn parse_ndjson(&mut self) -> Result<()> { let mut initial = true; let mut processed_value = true; + // repair() has already inserted or consumed the separator before the + // first additional value. + let mut pending_comma = self.output.rfind(','); while processed_value { if !initial { let processed_comma = self.parse_char(','); if !processed_comma { self.insert_before_last_whitespace(","); } + pending_comma = self.output.rfind(','); } else { initial = false; } @@ -81,7 +85,9 @@ impl JsonRepairer { } if !processed_value { - self.strip_trailing_comma(); + if let Some(index) = pending_comma { + self.output.remove(index); + } } self.output.insert_str(0, "[\n"); diff --git a/src/crates/execution/tool-call-jsonrepair/tests/number_regression_tests.rs b/src/crates/execution/tool-call-jsonrepair/tests/number_regression_tests.rs index 63db3168df..1a8b520896 100644 --- a/src/crates/execution/tool-call-jsonrepair/tests/number_regression_tests.rs +++ b/src/crates/execution/tool-call-jsonrepair/tests/number_regression_tests.rs @@ -28,3 +28,13 @@ fn plus_prefixed_leading_dot_numbers_remain_parseable() { assert_repaired_json("[+.5]"); assert_repaired_json("{value:+.5}"); } + +#[test] +fn plus_prefixed_exponent_only_tokens_are_rejected() { + for input in ["+e", "+e1", "+E", "+e+1", "[+e]", "{v:+e1}"] { + assert!( + jsonrepair(input).is_err(), + "plus-prefixed exponent-only token {input:?} must not repair to invalid JSON" + ); + } +} diff --git a/src/crates/execution/tool-call-jsonrepair/tests/repair_tests.rs b/src/crates/execution/tool-call-jsonrepair/tests/repair_tests.rs index d2bf824e0b..ba77f9a3d3 100644 --- a/src/crates/execution/tool-call-jsonrepair/tests/repair_tests.rs +++ b/src/crates/execution/tool-call-jsonrepair/tests/repair_tests.rs @@ -321,6 +321,80 @@ fn truncated_nested() { ok(r#"{"a": [1, 2, {"b": 3"#, r#"{"a": [1, 2, {"b": 3}]}"#); } +#[test] +fn llm_stream_prefix_ending_at_first_url_slash() { + ok( + r##"{"content":"# Heading\nhttps:/"##, + r##"{"content":"# Heading\nhttps:/"}"##, + ); +} + +#[test] +fn llm_stream_string_prefixes_remain_repairable() { + let full = + r##"{"content":"# Heading\nhttps://example.com/path","items":[{"id":1,"name":"Ada"}]}"##; + for end in full + .char_indices() + .map(|(index, _)| index) + .skip(1) + .chain(std::iter::once(full.len())) + { + let prefix = &full[..end]; + let repaired = openbitfun_tool_call_jsonrepair::jsonrepair(prefix) + .unwrap_or_else(|error| panic!("prefix {prefix:?} failed: {error}")); + serde_json::from_str::(&repaired) + .unwrap_or_else(|error| panic!("prefix {prefix:?} produced invalid JSON: {error}")); + } +} + +#[test] +fn unsupported_whitespace_only_array_value_does_not_loop() { + for input in ["[\u{000C}", "[1,\u{000C}"] { + assert!( + jsonrepair(input).is_err(), + "input {input:?} must return an error" + ); + } +} + +#[test] +fn root_list_trailing_comma_does_not_remove_earlier_separator() { + let repaired = jsonrepair("R ,[{[").unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&repaired).unwrap(); + assert_eq!(parsed.as_array().map(Vec::len), Some(2)); +} + +#[test] +fn invalid_escape_before_control_character_is_escaped_in_output() { + ok("'z\\\u{0010}'", r#""z\u0010""#); +} + +#[test] +fn isolated_unicode_surrogates_return_invalid_unicode_error() { + use openbitfun_tool_call_jsonrepair::JsonRepairErrorKind; + + for input in [r#""\udfff""#, r#""\ud83d""#, r#""\ud83d\u0041""#] { + let error = jsonrepair(input).expect_err("isolated surrogate must fail"); + assert_eq!(error.kind, JsonRepairErrorKind::InvalidUnicode); + } + ok(r#""\ud83d\ude00""#, r#""\ud83d\ude00""#); +} + +#[test] +fn escaped_comma_does_not_cause_recursive_string_retry() { + let input = "'\\,'?"; + if let Ok(repaired) = jsonrepair(input) { + serde_json::from_str::(&repaired).unwrap(); + } +} + +#[test] +fn empty_fenced_root_value_does_not_leave_trailing_comma() { + let repaired = jsonrepair("\0\n``````").unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&repaired).unwrap(); + assert_eq!(parsed.as_array().map(Vec::len), Some(1)); +} + // ── 13. Markdown code fences ───────────────────────────────── #[test] diff --git a/src/crates/execution/tool-call-jsonrepair/tests/streaming_tests.rs b/src/crates/execution/tool-call-jsonrepair/tests/streaming_tests.rs index c39e70ce4e..75c2613426 100644 --- a/src/crates/execution/tool-call-jsonrepair/tests/streaming_tests.rs +++ b/src/crates/execution/tool-call-jsonrepair/tests/streaming_tests.rs @@ -64,14 +64,29 @@ fn chunk_boundary_cases_match_string_api() { } #[test] -fn preserves_repair_errors_without_partial_output() { - let mut input = Cursor::new(br#""\u00""#); - let mut output = Vec::new(); +fn repairs_truncated_llm_url_across_reader_chunks() { + let input = r##"{"content":"# Heading\nhttps:/"##; + let expected = r##"{"content":"# Heading\nhttps:/"}"##; + for chunk_size in [1, 2, 3, 5] { + let mut output = Vec::new(); + jsonrepair_reader_to_writer( + ChunkedReader::new(input.as_bytes(), chunk_size), + &mut output, + ) + .unwrap(); + assert_eq!(output, expected.as_bytes(), "chunk size {chunk_size}"); + } +} - let err = jsonrepair_reader_to_writer(&mut input, &mut output).unwrap_err(); +#[test] +fn preserves_repair_errors_without_partial_output() { + for input in [&br#""\u00""#[..], &b"[\x0c"[..], &br#""\udfff""#[..]] { + let mut output = Vec::new(); + let err = jsonrepair_reader_to_writer(Cursor::new(input), &mut output).unwrap_err(); - assert!(matches!(err, JsonRepairStreamError::Repair(_))); - assert!(output.is_empty()); + assert!(matches!(err, JsonRepairStreamError::Repair(_))); + assert!(output.is_empty()); + } } #[test] @@ -129,7 +144,7 @@ struct FailingReader; impl Read for FailingReader { fn read(&mut self, _buf: &mut [u8]) -> io::Result { - Err(io::Error::other("source closed")) + Err(io::Error::new(io::ErrorKind::Other, "source closed")) } } @@ -137,7 +152,7 @@ struct FailingWriter; impl Write for FailingWriter { fn write(&mut self, _buf: &[u8]) -> io::Result { - Err(io::Error::other("destination closed")) + Err(io::Error::new(io::ErrorKind::Other, "destination closed")) } fn flush(&mut self) -> io::Result<()> { diff --git a/src/crates/execution/tool-call-jsonrepair/tests/tool_call_profile_tests.rs b/src/crates/execution/tool-call-jsonrepair/tests/tool_call_profile_tests.rs new file mode 100644 index 0000000000..c0780924ba --- /dev/null +++ b/src/crates/execution/tool-call-jsonrepair/tests/tool_call_profile_tests.rs @@ -0,0 +1,20 @@ +use openbitfun_tool_call_jsonrepair::repair_tool_call_json; + +#[test] +fn markdown_comment_markers_are_preserved_as_tool_argument_content() { + for (input, marker) in [ + (r##"{"content": # Markdown heading"}"##, "#"), + (r##"{"content": //}"##, "//"), + (r##"{"content": /* Markdown block */}"##, "/*"), + ] { + let repaired = repair_tool_call_json(input) + .unwrap_or_else(|error| panic!("repair failed for {input:?}: {error}")); + + assert!( + repaired.contains(marker), + "comment-like marker was discarded for {input:?}: {repaired:?}" + ); + serde_json::from_str::(&repaired) + .unwrap_or_else(|error| panic!("repaired output was invalid JSON: {error}")); + } +} diff --git a/src/crates/execution/tool-call-jsonrepair/tests/writer_tests.rs b/src/crates/execution/tool-call-jsonrepair/tests/writer_tests.rs index 186feea19f..fc2c400d05 100644 --- a/src/crates/execution/tool-call-jsonrepair/tests/writer_tests.rs +++ b/src/crates/execution/tool-call-jsonrepair/tests/writer_tests.rs @@ -27,7 +27,7 @@ struct FailingWriter; impl Write for FailingWriter { fn write(&mut self, _buf: &[u8]) -> io::Result { - Err(io::Error::other("destination closed")) + Err(io::Error::new(io::ErrorKind::Other, "destination closed")) } fn flush(&mut self) -> io::Result<()> {