From d530d257939073e234e7472c8c5fc237d7f8559d Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 00:57:13 +0800 Subject: [PATCH] Expand history-sanitize boundary coverage for stream chunks --- .../openai/tool_history_sanitize_test.go | 61 +++++++++++++++++-- tests/node/stream-tool-sieve.test.js | 16 +++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/internal/adapter/openai/tool_history_sanitize_test.go b/internal/adapter/openai/tool_history_sanitize_test.go index 0236b02..02128c9 100644 --- a/internal/adapter/openai/tool_history_sanitize_test.go +++ b/internal/adapter/openai/tool_history_sanitize_test.go @@ -11,10 +11,35 @@ func TestSanitizeLeakedToolHistoryRemovesMarkerBlocks(t *testing.T) { } func TestSanitizeLeakedToolHistoryPreservesChunkWhitespace(t *testing.T) { - raw := "Hello " - got := sanitizeLeakedToolHistory(raw) - if got != "Hello " { - t.Fatalf("expected trailing whitespace to be preserved, got %q", got) + cases := []struct { + name string + raw string + want string + }{ + { + name: "trailing space kept", + raw: "Hello ", + want: "Hello ", + }, + { + name: "leading newline kept", + raw: "\nworld", + want: "\nworld", + }, + { + name: "surrounding whitespace around marker is preserved", + raw: "A \n[TOOL_RESULT_HISTORY]\nfunction.name: exec\nfunction.arguments: {}\n[/TOOL_RESULT_HISTORY]\n B", + want: "A \n\n B", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := sanitizeLeakedToolHistory(tc.raw) + if got != tc.want { + t.Fatalf("unexpected sanitize result, want %q got %q", tc.want, got) + } + }) } } @@ -43,3 +68,31 @@ func TestFlushToolSieveDropsToolResultHistoryLeak(t *testing.T) { t.Fatalf("expected result history block to be swallowed, got %+v", flushed) } } + +func TestProcessToolSieveChunkSplitsResultHistoryBoundary(t *testing.T) { + var state toolStreamSieveState + parts := []string{ + "Hello ", + "[TOOL_RESULT_HISTORY]\nstatus: already_called\n", + "function.name: exec\nfunction.arguments: {}\n[/TOOL_RESULT_HISTORY]", + "world", + } + var events []toolStreamEvent + for _, p := range parts { + events = append(events, processToolSieveChunk(&state, p, []string{"exec"})...) + } + events = append(events, flushToolSieve(&state, []string{"exec"})...) + + var text string + for _, evt := range events { + if evt.Content != "" { + text += evt.Content + } + if len(evt.ToolCalls) > 0 { + t.Fatalf("did not expect parsed tool calls from history leak: %+v", evt.ToolCalls) + } + } + if text != "Hello world" { + t.Fatalf("expected clean text output preserving boundary spaces, got %q", text) + } +} diff --git a/tests/node/stream-tool-sieve.test.js b/tests/node/stream-tool-sieve.test.js index a4c793c..b24e138 100644 --- a/tests/node/stream-tool-sieve.test.js +++ b/tests/node/stream-tool-sieve.test.js @@ -260,6 +260,22 @@ test('sieve swallows leaked TOOL_RESULT_HISTORY marker blocks', () => { assert.equal(leakedText.includes('[TOOL_RESULT_HISTORY]'), false); }); +test('sieve preserves text spacing when TOOL_RESULT_HISTORY spans chunks', () => { + const events = runSieve( + [ + 'Hello ', + '[TOOL_RESULT_HISTORY]\nstatus: already_called\n', + 'function.name: exec\nfunction.arguments: {}\n[/TOOL_RESULT_HISTORY]', + 'world', + ], + ['exec'], + ); + const leakedText = collectText(events); + const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0); + assert.equal(hasToolCall, false); + assert.equal(leakedText, 'Hello world'); +}); + test('sieve intercepts rejected unknown tool payload (no args) without raw leak', () => { const events = runSieve( ['{"tool_calls":[{"name":"not_in_schema"}]}', '后置正文G。'],