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 }