返回 DeepSeek-Reasonix
registry_canon_test.go
根目录 / internal / tool / registry_canon_test.go
1 package tool
2
3 import (
4 "context"
5 "encoding/json"
6 "testing"
7 )
8
9 type countingSchemaTool struct {
10 name string
11 calls *int
12 }
13
14 func (c countingSchemaTool) Name() string { return c.name }
15 func (c countingSchemaTool) Description() string { return c.name }
16 func (c countingSchemaTool) Schema() json.RawMessage {
17 *c.calls++
18 return json.RawMessage(`{"type":"object","properties":{"b":{"type":"string"},"a":{"type":"string"}}}`)
19 }
20 func (c countingSchemaTool) Execute(context.Context, json.RawMessage) (string, error) {
21 return "", nil
22 }
23 func (c countingSchemaTool) ReadOnly() bool { return true }
24
25 // TestSchemasCanonicalizesOncePerTool guards the regression where Schemas() — run
26 // every turn — re-canonicalized (unmarshal+sort+marshal) every tool's schema on
27 // each call. Schemas never change after registration, so Schema() must be invoked
28 // exactly once (at Add), no matter how many times Schemas() is called.
29 func TestSchemasCanonicalizesOncePerTool(t *testing.T) {
30 calls := 0
31 r := NewRegistry()
32 r.Add(countingSchemaTool{name: "alpha", calls: &calls})
33
34 if calls != 1 {
35 t.Fatalf("Schema() called %d times at Add, want 1", calls)
36 }
37
38 for i := 0; i < 50; i++ {
39 schemas := r.Schemas()
40 if len(schemas) != 1 {
41 t.Fatalf("Schemas() returned %d entries, want 1", len(schemas))
42 }
43 // Canonicalization sorts object keys, so "a" must precede "b".
44 got := string(schemas[0].Parameters)
45 if ai, bi := indexOf(got, `"a"`), indexOf(got, `"b"`); ai < 0 || bi < 0 || ai > bi {
46 t.Fatalf("schema not canonicalized (keys unsorted): %s", got)
47 }
48 }
49
50 if calls != 1 {
51 t.Fatalf("Schema() called %d times after 50 Schemas() calls, want 1 (caching regressed)", calls)
52 }
53 }
54
55 func indexOf(s, sub string) int {
56 for i := 0; i+len(sub) <= len(s); i++ {
57 if s[i:i+len(sub)] == sub {
58 return i
59 }
60 }
61 return -1
62 }
63
63 lines GO