@@ -80,6 +80,33 @@ fn unclosed_tag_with_balanced_json_still_recovers() {
8080 assert_eq ! ( calls. len( ) , 1 ) ;
8181}
8282
83+ #[ test]
84+ fn unclosed_tag_recovers_dsml_parameter_calls ( ) {
85+ let response = concat ! (
86+ "<tool_call>\n " ,
87+ r#"{"arguments":{"path":"work/extract.py","content":"t = re.sub(r'<script.*?</script>', ' ', t, flags=re.S)"}}"# ,
88+ "</|DSML| parameter>\n " ,
89+ "<|DSML| parameter name=\" name\" :\" file_write\" }</|DSML| parameter>\n " ,
90+ "</|DSML| invoke>\n " ,
91+ "<|DSML| invoke>\n " ,
92+ r#"{"arguments":{"command":"ls work"}}"# ,
93+ "</|DSML| parameter>\n " ,
94+ "<|DSML| parameter name=\" name\" :\" shell\" }</|DSML| parameter>\n " ,
95+ "</|DSML| invoke>\n " ,
96+ "</|DSML| calls>Done." ,
97+ ) ;
98+ let ( text, calls) = parse ( response) ;
99+ assert_eq ! ( text, "Done." ) ;
100+ assert_eq ! ( calls. len( ) , 2 ) ;
101+ assert_eq ! ( calls[ 0 ] . name, "file_write" ) ;
102+ assert_eq ! (
103+ calls[ 0 ] . arguments[ "content" ] ,
104+ "t = re.sub(r'<script.*?</script>', ' ', t, flags=re.S)"
105+ ) ;
106+ assert_eq ! ( calls[ 1 ] . name, "shell" ) ;
107+ assert_eq ! ( calls[ 1 ] . arguments[ "command" ] , "ls work" ) ;
108+ }
109+
83110#[ test]
84111fn unclosed_tag_recovery_preserves_unrelated_markup_after_the_json ( ) {
85112 // No tag-family marker exists anywhere after this opener, so a `</div>`
@@ -632,12 +659,8 @@ fn a_bare_dsml_invoke_carries_its_name_in_the_body() {
632659/// A block that decodes to nothing must not bury the calls after it.
633660///
634661/// Verbatim from a `deepseek` turn: an unterminated `<tool_call>` whose body is
635- /// `{"arguments":{…}}` with no name — genuinely unrecoverable, since the name
636- /// survived only in a corrupted `<|DSML| parameter name="name":"file_write"}`
637- /// line and inventing one is never right — followed by a complete
638- /// `<|DSML| invoke>`. The failed block used to run to end-of-text and take
639- /// the good call with it, so the whole response parsed as prose and both calls
640- /// were lost.
662+ /// `{"arguments":{…}}` with a name in a corrupted DSML parameter line,
663+ /// followed by a complete `<|DSML| invoke>`. Both calls should survive.
641664#[ test]
642665fn an_undecodable_block_does_not_swallow_the_call_after_it ( ) {
643666 let raw = concat ! (
@@ -656,11 +679,12 @@ fn an_undecodable_block_does_not_swallow_the_call_after_it() {
656679 let outcome = super :: parse_known ( raw, & [ "file_write" , "shell" ] ) ;
657680 assert_eq ! (
658681 outcome. calls. len( ) ,
659- 1 ,
660- "the well-formed call survives its malformed neighbour : {:?}" ,
682+ 2 ,
683+ "both calls survive their mixed framing : {:?}" ,
661684 outcome. calls
662685 ) ;
663- assert_eq ! ( outcome. calls[ 0 ] . name, "shell" ) ;
686+ assert_eq ! ( outcome. calls[ 0 ] . name, "file_write" ) ;
687+ assert_eq ! ( outcome. calls[ 1 ] . name, "shell" ) ;
664688
665689 // Reduced to the essential shape, so a future change that reintroduces the
666690 // swallow fails here with less noise.
@@ -717,6 +741,30 @@ fn recovery_leaves_a_named_invoke_after_a_complete_malformed_body() {
717741 assert_eq ! ( outcome. calls[ 0 ] . name, "shell" ) ;
718742}
719743
744+ #[ test]
745+ fn unclosed_inline_name_does_not_scan_later_json_from_narrative ( ) {
746+ let raw = concat ! (
747+ "<tool_call>{\" name\" :\" first\" ,\" arguments\" :{}} prose " ,
748+ "{\" name\" :\" shell\" ,\" arguments\" :{\" command\" :\" ls\" }}"
749+ ) ;
750+ let ( text, calls) = parse ( raw) ;
751+ assert_eq ! ( calls. len( ) , 1 , "{calls:?}" ) ;
752+ assert_eq ! ( calls[ 0 ] . name, "first" ) ;
753+ assert ! ( text. contains( "prose" ) ) ;
754+ }
755+
756+ #[ test]
757+ fn unclosed_inline_name_decodes_stringified_and_aliased_arguments ( ) {
758+ for raw in [
759+ r#"<tool_call>{"name":"echo","arguments":"{\"value\":\"x\"}"}"# ,
760+ r#"<tool_call>{"name":"echo","args":{"value":"x"}}"# ,
761+ ] {
762+ let ( _, calls) = parse ( raw) ;
763+ assert_eq ! ( calls. len( ) , 1 , "{calls:?}" ) ;
764+ assert_eq ! ( calls[ 0 ] . arguments, serde_json:: json!( { "value" : "x" } ) ) ;
765+ }
766+ }
767+
720768#[ test]
721769fn a_named_invoke_precedes_a_later_bare_invoke_closer ( ) {
722770 let raw = concat ! (
0 commit comments