返回 DeepSeek-Reasonix
registry_test.go
根目录 / desktop / internal / hostrpc / registry_test.go
1 package hostrpc
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "testing"
9 )
10
11 type fixtureItem struct {
12 ID string `json:"id"`
13 Note string `json:"note,omitempty"`
14 Next *fixtureItem `json:"next"`
15 Count int `json:"-"`
16 }
17
18 type fixtureTarget struct {
19 calls []string
20 fail bool
21 empty bool
22 }
23
24 func (f *fixtureTarget) Void() { f.calls = append(f.calls, "Void") }
25 func (f *fixtureTarget) Platform() string { return "test-os" }
26 func (f *fixtureTarget) Fail() error { return errors.New("boom") }
27 func (f *fixtureTarget) Ping(id string, n int) (fixtureItem, error) {
28 f.calls = append(f.calls, "Ping:"+id)
29 if f.fail {
30 return fixtureItem{}, errors.New("ping failed: " + id)
31 }
32 return fixtureItem{ID: id, Count: n}, nil
33 }
34 func (f *fixtureTarget) Items() []fixtureItem {
35 if f.empty {
36 return []fixtureItem{}
37 }
38 return nil
39 }
40 func (f *fixtureTarget) Explode() { panic("kaboom") }
41 func (f *fixtureTarget) Hidden(map[string]any) {}
42 func (f *fixtureTarget) Update(item fixtureItem) fixtureItem { return item }
43
44 type badResults struct{}
45
46 func (badResults) Three() (int, int, error) { return 0, 0, nil }
47
48 type badOrder struct{}
49
50 func (badOrder) Pair() (int, string) { return 0, "" }
51
52 type badParam struct{}
53
54 func (badParam) Ctx(string, context.Context) {}
55
56 type badVariadic struct{}
57
58 func (badVariadic) Many(...string) {}
59
60 type badFunc struct{}
61
62 func (badFunc) Callback(func()) {}
63
64 type notStruct int
65
66 func (notStruct) M() {}
67
68 func mustRegistry(t *testing.T, target any, skip Skip) *Registry {
69 t.Helper()
70 r, err := NewRegistry(target, skip)
71 if err != nil {
72 t.Fatal(err)
73 }
74 return r
75 }
76
77 func TestRegistryDescribesAcceptedSignatures(t *testing.T) {
78 r := mustRegistry(t, &fixtureTarget{}, Skip{"Hidden": true})
79 got := map[string]Command{}
80 for _, cmd := range r.Commands() {
81 got[cmd.Name] = cmd
82 }
83 if _, ok := got["Hidden"]; ok {
84 t.Fatal("skipped method appeared in the contract")
85 }
86 if cmd := got["Void"]; cmd.Result != nil || cmd.ReturnsError || len(cmd.Params) != 0 {
87 t.Fatalf("Void = %+v", cmd)
88 }
89 if cmd := got["Platform"]; cmd.Result == nil || cmd.Result.Kind != KindString || cmd.ReturnsError {
90 t.Fatalf("Platform = %+v", cmd)
91 }
92 if cmd := got["Fail"]; cmd.Result != nil || !cmd.ReturnsError {
93 t.Fatalf("Fail = %+v", cmd)
94 }
95 ping := got["Ping"]
96 if !ping.ReturnsError || ping.Result == nil || ping.Result.Kind != KindObject || ping.Result.Ref != "hostrpc.fixtureItem" {
97 t.Fatalf("Ping = %+v", ping)
98 }
99 if len(ping.Params) != 2 || ping.Params[0].Kind != KindString || ping.Params[1].Kind != KindInteger {
100 t.Fatalf("Ping params = %+v", ping.Params)
101 }
102 if items := got["Items"]; items.Result.Kind != KindArray || items.Result.Elem.Ref != "hostrpc.fixtureItem" {
103 t.Fatalf("Items = %+v", items)
104 }
105 names := make([]string, 0, len(r.Commands()))
106 for _, cmd := range r.Commands() {
107 names = append(names, cmd.Name)
108 }
109 if strings.Join(names, ",") != "Explode,Fail,Items,Ping,Platform,Update,Void" {
110 t.Fatalf("commands not sorted by name: %v", names)
111 }
112 }
113
114 func TestRegistryRejectsUnsupportedSignatures(t *testing.T) {
115 cases := []struct {
116 target any
117 want string
118 }{
119 {&badResults{}, "badResults.Three: 3 results"},
120 {&badOrder{}, "badOrder.Pair: results must be (T, error), got (int, string)"},
121 {&badParam{}, "badParam.Ctx: parameter 1: context.Context cannot be decoded"},
122 {&badVariadic{}, "badVariadic.Many: variadic"},
123 {&badFunc{}, "badFunc.Callback: parameter 0: func() is not JSON-serialisable"},
124 {new(notStruct), "must be a pointer to a struct"},
125 {fixtureTarget{}, "must be a pointer to a struct"},
126 {nil, "nil target"},
127 }
128 for _, tc := range cases {
129 _, err := NewRegistry(tc.target, nil)
130 if err == nil || !strings.Contains(err.Error(), tc.want) {
131 t.Errorf("NewRegistry(%T) error = %v, want substring %q", tc.target, err, tc.want)
132 }
133 }
134 }
135
136 func raw(values ...string) []json.RawMessage {
137 out := make([]json.RawMessage, 0, len(values))
138 for _, v := range values {
139 out = append(out, json.RawMessage(v))
140 }
141 return out
142 }
143
144 func TestRegistryInvokeMarshalsArgumentsAndResults(t *testing.T) {
145 target := &fixtureTarget{}
146 r := mustRegistry(t, target, nil)
147 ctx := context.Background()
148
149 result, err := r.Invoke(ctx, "Void", nil)
150 if err != nil || result != nil {
151 t.Fatalf("Void = %v, %v", result, err)
152 }
153 result, err = r.Invoke(ctx, "Ping", raw(`"alpha"`, `7`))
154 if err != nil {
155 t.Fatal(err)
156 }
157 if item, ok := result.(fixtureItem); !ok || item.ID != "alpha" || item.Count != 7 {
158 t.Fatalf("Ping result = %#v", result)
159 }
160 if _, err := r.Invoke(ctx, "Ping", raw(`"missing"`)); err != nil {
161 t.Fatalf("missing trailing argument must decode as zero: %v", err)
162 }
163 if _, err := r.Invoke(ctx, "Ping", raw(`"a"`, `1`, `2`)); err == nil {
164 t.Fatal("extra argument must be rejected")
165 }
166 var invalid *InvalidArgsError
167 if _, err := r.Invoke(ctx, "Ping", raw(`42`)); !errors.As(err, &invalid) {
168 t.Fatalf("type mismatch error = %v", err)
169 }
170 updated, err := r.Invoke(ctx, "Update", raw(`{"id":"x","next":{"id":"y","next":null}}`))
171 if err != nil || updated.(fixtureItem).Next == nil || updated.(fixtureItem).Next.ID != "y" {
172 t.Fatalf("Update = %#v, %v", updated, err)
173 }
174 if strings.Join(target.calls, " ") != "Void Ping:alpha Ping:missing" {
175 t.Fatalf("calls = %v", target.calls)
176 }
177 }
178
179 func TestRegistryInvokeReportsErrorsAndUnknownMethods(t *testing.T) {
180 target := &fixtureTarget{fail: true}
181 r := mustRegistry(t, target, Skip{"Hidden": true})
182 ctx := context.Background()
183
184 if _, err := r.Invoke(ctx, "Fail", nil); err == nil || err.Error() != "boom" {
185 t.Fatalf("Fail error = %v", err)
186 }
187 if _, err := r.Invoke(ctx, "Ping", raw(`"z"`)); err == nil || err.Error() != "ping failed: z" {
188 t.Fatalf("Ping error = %v", err)
189 }
190 var unknown *UnknownMethodError
191 if _, err := r.Invoke(ctx, "Nope", nil); !errors.As(err, &unknown) || unknown.Method != "Nope" {
192 t.Fatalf("unknown method error = %v", err)
193 }
194 if _, err := r.Invoke(ctx, "Hidden", raw(`{}`)); !errors.As(err, &unknown) {
195 t.Fatalf("skipped method must be unknown at invoke: %v", err)
196 }
197 var panicked *PanicError
198 if _, err := r.Invoke(ctx, "Explode", nil); !errors.As(err, &panicked) || panicked.Method != "Explode" {
199 t.Fatalf("panic error = %v", err)
200 }
201 cancelled, cancel := context.WithCancel(ctx)
202 cancel()
203 if _, err := r.Invoke(cancelled, "Void", nil); !errors.Is(err, context.Canceled) {
204 t.Fatalf("cancelled context error = %v", err)
205 }
206 }
207
208 func TestRegistryInvokeKeepsNilAndEmptySlicesDistinct(t *testing.T) {
209 ctx := context.Background()
210 for _, tc := range []struct {
211 empty bool
212 want string
213 }{{false, "null"}, {true, "[]"}} {
214 r := mustRegistry(t, &fixtureTarget{empty: tc.empty}, nil)
215 result, err := r.Invoke(ctx, "Items", nil)
216 if err != nil {
217 t.Fatal(err)
218 }
219 encoded, _ := json.Marshal(result)
220 if string(encoded) != tc.want {
221 t.Fatalf("Items(empty=%v) encodes as %s, want %s", tc.empty, encoded, tc.want)
222 }
223 }
224 }
225
226 func TestRegistryWithoutInstanceDescribesButCannotInvoke(t *testing.T) {
227 r := mustRegistry(t, (*fixtureTarget)(nil), nil)
228 if len(r.Commands()) == 0 {
229 t.Fatal("typed nil target must still describe the contract")
230 }
231 if _, err := r.Invoke(context.Background(), "Void", nil); err == nil {
232 t.Fatal("invoke on a nil target must fail")
233 }
234 }
235
235 lines GO