返回 DeepSeek-Reasonix
bound_array_contract_test.go
根目录 / desktop / bound_array_contract_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "reflect"
6 "strings"
7 "testing"
8 )
9
10 func TestBoundArrayPayloadsAreNonNilBeforeStartup(t *testing.T) {
11 isolateDesktopUserDirs(t)
12
13 app := NewApp()
14 cases := []struct {
15 name string
16 got any
17 }{
18 {"Checkpoints", app.Checkpoints()},
19 {"ListSessions", app.ListSessions()},
20 {"ListTrashedSessions", app.ListTrashedSessions()},
21 {"ListWorkspaces", app.ListWorkspaces()},
22 {"History", app.History()},
23 {"Jobs", app.Jobs()},
24 {"Commands", app.Commands()},
25 {"Models", app.Models()},
26 {"ListDir", app.ListDir("__missing__")},
27 {"ListDirForTab", app.ListDirForTab("missing", "")},
28 {"SearchFileRefsForTab", app.SearchFileRefsForTab("missing", "file")},
29 {"ListTabs", app.ListTabs()},
30 {"ListProjectTree", app.ListProjectTree()},
31 {"AvailableSubagentTools", app.AvailableSubagentTools()},
32 {"MCPServers", app.MCPServers()},
33 {"Plugins", app.Plugins()},
34 {"HeartbeatListTasks", app.HeartbeatListTasks()},
35 {"HeartbeatReloadTasks", app.HeartbeatReloadTasks()},
36 }
37 for _, tc := range cases {
38 assertNonNilSliceJSON(t, tc.name, tc.got)
39 }
40
41 if got := app.SlashArgs("/skill "); got.Items == nil {
42 t.Fatal("SlashArgs().Items is nil; frontend expects []")
43 }
44 if got := app.WorkspaceChanges(""); got.Files == nil {
45 t.Fatal(`WorkspaceChanges("").Files is nil; frontend expects []`)
46 }
47 if got := app.ContextPanel("missing"); got.ReadFiles == nil || got.ChangedFiles == nil {
48 t.Fatalf("ContextPanel(missing) arrays = read:%v changed:%v, want non-nil", got.ReadFiles, got.ChangedFiles)
49 }
50 if got := app.HooksSettings("global"); got.Hooks == nil || got.Events == nil {
51 t.Fatalf("HooksSettings(global) arrays = hooks:%v events:%v, want non-nil", got.Hooks, got.Events)
52 }
53 forkTargets, forkTargetsErr := app.ForkTargetsForTab("__missing__")
54 if forkTargetsErr != nil {
55 t.Fatalf("ForkTargetsForTab(__missing__): %v", forkTargetsErr)
56 }
57 if forkTargets.Targets == nil {
58 t.Fatal("ForkTargetsForTab(__missing__).Targets is nil; frontend expects []")
59 }
60 if got := app.Settings(); got.Providers == nil || got.OfficialProviders == nil || got.ProviderPresets == nil || got.ProviderKinds == nil ||
61 got.Permissions.Allow == nil || got.Permissions.Ask == nil || got.Permissions.Deny == nil ||
62 got.Sandbox.AllowWrite == nil || got.Sandbox.EffectiveWriteRoots == nil ||
63 got.Bot.Allowlist.QQUsers == nil || got.Bot.Allowlist.FeishuUsers == nil || got.Bot.Allowlist.WeixinUsers == nil ||
64 got.Bot.Allowlist.QQGroups == nil || got.Bot.Allowlist.FeishuGroups == nil || got.Bot.Allowlist.WeixinGroups == nil {
65 t.Fatalf("Settings() contains nil array fields: %+v", got)
66 }
67 if got := app.DesktopStartupSettings(); got.StatusBarItems == nil ||
68 got.Bot.Allowlist.QQUsers == nil || got.Bot.Allowlist.FeishuUsers == nil || got.Bot.Allowlist.WeixinUsers == nil ||
69 got.Bot.Allowlist.QQGroups == nil || got.Bot.Allowlist.FeishuGroups == nil || got.Bot.Allowlist.WeixinGroups == nil {
70 t.Fatalf("DesktopStartupSettings() contains nil array fields: %+v", got)
71 }
72
73 boundPayloads := []struct {
74 name string
75 got any
76 }{
77 {"CapabilityDiagnostics", app.CapabilityDiagnostics(false)},
78 {"Capabilities", app.Capabilities()},
79 {"SkillsSettings", app.SkillsSettings()},
80 {"HistoryPage", app.HistoryPage(0, 20)},
81 {"Effort", app.Effort()},
82 {"Memory", app.Memory()},
83 {"MemorySuggestions", app.MemorySuggestions()},
84 }
85 for _, tc := range boundPayloads {
86 assertRequiredJSONSlicesNonNil(t, tc.name, reflect.ValueOf(tc.got))
87 }
88 }
89
90 func assertNonNilSliceJSON(t *testing.T, name string, got any) {
91 t.Helper()
92 v := reflect.ValueOf(got)
93 if v.Kind() != reflect.Slice {
94 t.Fatalf("%s returned %T, want slice", name, got)
95 }
96 if v.IsNil() {
97 t.Fatalf("%s returned nil slice; frontend expects []", name)
98 }
99 raw, err := json.Marshal(got)
100 if err != nil {
101 t.Fatalf("%s JSON marshal: %v", name, err)
102 }
103 if string(raw) == "null" {
104 t.Fatalf("%s JSON encoded as null; frontend expects []", name)
105 }
106 }
107
108 func assertRequiredJSONSlicesNonNil(t *testing.T, path string, value reflect.Value) {
109 t.Helper()
110 for value.Kind() == reflect.Interface || value.Kind() == reflect.Pointer {
111 if value.IsNil() {
112 return
113 }
114 value = value.Elem()
115 }
116
117 switch value.Kind() {
118 case reflect.Slice, reflect.Array:
119 if value.Kind() == reflect.Slice && value.IsNil() {
120 t.Fatalf("%s is a nil slice; JSON contract requires []", path)
121 }
122 for i := range value.Len() {
123 assertRequiredJSONSlicesNonNil(t, path, value.Index(i))
124 }
125 case reflect.Struct:
126 typ := value.Type()
127 for i := range value.NumField() {
128 fieldType := typ.Field(i)
129 if fieldType.PkgPath != "" {
130 continue
131 }
132 jsonTag := fieldType.Tag.Get("json")
133 if jsonTag == "-" || strings.Contains(jsonTag, ",omitempty") {
134 continue
135 }
136 fieldPath := path + "." + fieldType.Name
137 assertRequiredJSONSlicesNonNil(t, fieldPath, value.Field(i))
138 }
139 }
140 }
141
141 lines GO