/ internal / agent / normalize_test.go
normalize_test.go
 1  package agent
 2  
 3  import (
 4  	"encoding/json"
 5  	"strings"
 6  	"testing"
 7  
 8  	"github.com/Kocoro-lab/ShanClaw/internal/client"
 9  )
10  
11  func TestNormalizeJSON_IdenticalArgumentsAreCanonicalized(t *testing.T) {
12  	a := normalizeJSON(json.RawMessage(`{"command":"date","path":"/tmp"}`))
13  	b := normalizeJSON(json.RawMessage(`{ "path": "/tmp", "command": "date" }`))
14  
15  	if a != b {
16  		t.Fatalf("expected canonical JSON to match, got %q and %q", a, b)
17  	}
18  	if a != `{"command":"date","path":"/tmp"}` {
19  		t.Fatalf("expected deterministic key order, got %q", a)
20  	}
21  }
22  
23  func TestNormalizeJSON_EmptyAndWhitespaceInputs(t *testing.T) {
24  	tests := []json.RawMessage{
25  		nil,
26  		{},
27  		[]byte(""),
28  		[]byte("   \n\t"),
29  	}
30  
31  	for i, tc := range tests {
32  		got := normalizeJSON(tc)
33  		if got != "{}" {
34  			t.Fatalf("case %d: expected {}, got %q", i, got)
35  		}
36  	}
37  }
38  
39  // TestNormalizeJSON_NullBecomesEmptyObject verifies that literal `null`
40  // arguments (emitted by providers when a tool is called with no args) are
41  // canonicalized to `{}` so dedup/cache keys don't diverge between null and
42  // empty-object representations of the same semantic "no arguments". See
43  // issue #45.
44  func TestNormalizeJSON_NullBecomesEmptyObject(t *testing.T) {
45  	cases := []json.RawMessage{
46  		json.RawMessage("null"),
47  		json.RawMessage(" null "),
48  		json.RawMessage("\tnull\n"),
49  	}
50  	for i, tc := range cases {
51  		got := normalizeJSON(tc)
52  		if got != "{}" {
53  			t.Fatalf("case %d: expected {}, got %q", i, got)
54  		}
55  	}
56  }
57  
58  func TestNormalizeJSON_InvalidJSONFallsBackToTrimmedRaw(t *testing.T) {
59  	raw := json.RawMessage(`{ "command": "date",`)
60  	expected := strings.TrimSpace(string(raw))
61  	got := normalizeJSON(raw)
62  	if got != expected {
63  		t.Fatalf("expected trimmed fallback %q, got %q", expected, got)
64  	}
65  }
66  
67  func TestNormalizeWebQuery_BrowserURL(t *testing.T) {
68  	result := normalizeWebQuery(`{"action":"navigate","url":"https://jd.com/search?q=huawei"}`)
69  	if result == "" {
70  		t.Error("normalizeWebQuery should extract URL from browser args")
71  	}
72  }
73  
74  func TestNormalizeStructuredToolCallPreamble_StripsDuplicateSerializedCalls(t *testing.T) {
75  	text := "Tool calls:\nTool: browser_click, Args: {\"ref\":\"e12\"}\nTool: browser_type, Args: {\"ref\":\"e13\",\"text\":\"hello\"}"
76  	toolCalls := []client.FunctionCall{
77  		{Name: "browser_click", Arguments: json.RawMessage(`{"ref":"e12"}`)},
78  		{Name: "browser_type", Arguments: json.RawMessage(`{"text":"hello","ref":"e13"}`)},
79  	}
80  
81  	if got := normalizeStructuredToolCallPreamble(text, toolCalls); got != "" {
82  		t.Fatalf("expected duplicate serialized tool-call text to be stripped, got %q", got)
83  	}
84  }
85  
86  func TestNormalizeStructuredToolCallPreamble_PreservesMeaningfulText(t *testing.T) {
87  	text := "Let me check that file."
88  	toolCalls := []client.FunctionCall{
89  		{Name: "mock_tool", Arguments: json.RawMessage(`{}`)},
90  	}
91  
92  	if got := normalizeStructuredToolCallPreamble(text, toolCalls); got != text {
93  		t.Fatalf("expected meaningful text to be preserved, got %q", got)
94  	}
95  }