返回 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 {"AutoResearchList", app.AutoResearchList("missing")},
33 {"AutoResearchFindings", app.AutoResearchFindings("missing", 10)},
34 {"MCPServers", app.MCPServers()},
35 {"Plugins", app.Plugins()},
36 {"HeartbeatListTasks", app.HeartbeatListTasks()},
37 {"HeartbeatReloadTasks", app.HeartbeatReloadTasks()},
38 }
39 for _, tc := range cases {
40 assertNonNilSliceJSON(t, tc.name, tc.got)
41 }
42
43 if got := app.SlashArgs("/skill "); got.Items == nil {
44 t.Fatal("SlashArgs().Items is nil; frontend expects []")
45 }
46 if got := app.WorkspaceChanges(""); got.Files == nil {
47 t.Fatal(`WorkspaceChanges("").Files is nil; frontend expects []`)
48 }
49 if got := app.ContextPanel("missing"); got.ReadFiles == nil || got.ChangedFiles == nil {
50 t.Fatalf("ContextPanel(missing) arrays = read:%v changed:%v, want non-nil", got.ReadFiles, got.ChangedFiles)
51 }
52 if got := app.HooksSettings("global"); got.Hooks == nil || got.Events == nil {
53 t.Fatalf("HooksSettings(global) arrays = hooks:%v events:%v, want non-nil", got.Hooks, got.Events)
54 }
55 if got := app.Settings(); got.Providers == nil || got.OfficialProviders == nil || got.ProviderPresets == nil || got.ProviderKinds == nil ||
56 got.Permissions.Allow == nil || got.Permissions.Ask == nil || got.Permissions.Deny == nil ||
57 got.Sandbox.AllowWrite == nil || got.Sandbox.EffectiveWriteRoots == nil ||
58 got.Bot.Allowlist.QQUsers == nil || got.Bot.Allowlist.FeishuUsers == nil || got.Bot.Allowlist.WeixinUsers == nil ||
59 got.Bot.Allowlist.QQGroups == nil || got.Bot.Allowlist.FeishuGroups == nil || got.Bot.Allowlist.WeixinGroups == nil {
60 t.Fatalf("Settings() contains nil array fields: %+v", got)
61 }
62 if got := app.DesktopStartupSettings(); got.StatusBarItems == 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("DesktopStartupSettings() contains nil array fields: %+v", got)
66 }
67
68 boundPayloads := []struct {
69 name string
70 got any
71 }{
72 {"CapabilityDiagnostics", app.CapabilityDiagnostics(false)},
73 {"Capabilities", app.Capabilities()},
74 {"SkillsSettings", app.SkillsSettings()},
75 {"HistoryPage", app.HistoryPage(0, 20)},
76 {"Effort", app.Effort()},
77 {"Memory", app.Memory()},
78 {"MemorySuggestions", app.MemorySuggestions()},
79 }
80 for _, tc := range boundPayloads {
81 assertRequiredJSONSlicesNonNil(t, tc.name, reflect.ValueOf(tc.got))
82 }
83 }
84
85 func assertNonNilSliceJSON(t *testing.T, name string, got any) {
86 t.Helper()
87 v := reflect.ValueOf(got)
88 if v.Kind() != reflect.Slice {
89 t.Fatalf("%s returned %T, want slice", name, got)
90 }
91 if v.IsNil() {
92 t.Fatalf("%s returned nil slice; frontend expects []", name)
93 }
94 raw, err := json.Marshal(got)
95 if err != nil {
96 t.Fatalf("%s JSON marshal: %v", name, err)
97 }
98 if string(raw) == "null" {
99 t.Fatalf("%s JSON encoded as null; frontend expects []", name)
100 }
101 }
102
103 func assertRequiredJSONSlicesNonNil(t *testing.T, path string, value reflect.Value) {
104 t.Helper()
105 for value.Kind() == reflect.Interface || value.Kind() == reflect.Pointer {
106 if value.IsNil() {
107 return
108 }
109 value = value.Elem()
110 }
111
112 switch value.Kind() {
113 case reflect.Slice, reflect.Array:
114 if value.Kind() == reflect.Slice && value.IsNil() {
115 t.Fatalf("%s is a nil slice; JSON contract requires []", path)
116 }
117 for i := 0; i < value.Len(); i++ {
118 assertRequiredJSONSlicesNonNil(t, path, value.Index(i))
119 }
120 case reflect.Struct:
121 typ := value.Type()
122 for i := 0; i < value.NumField(); i++ {
123 fieldType := typ.Field(i)
124 if fieldType.PkgPath != "" {
125 continue
126 }
127 jsonTag := fieldType.Tag.Get("json")
128 if jsonTag == "-" || strings.Contains(jsonTag, ",omitempty") {
129 continue
130 }
131 fieldPath := path + "." + fieldType.Name
132 assertRequiredJSONSlicesNonNil(t, fieldPath, value.Field(i))
133 }
134 }
135 }
136
136 lines GO