136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBoundArrayPayloadsAreNonNilBeforeStartup(t *testing.T) {
|
|
isolateDesktopUserDirs(t)
|
|
|
|
app := NewApp()
|
|
cases := []struct {
|
|
name string
|
|
got any
|
|
}{
|
|
{"Checkpoints", app.Checkpoints()},
|
|
{"ListSessions", app.ListSessions()},
|
|
{"ListTrashedSessions", app.ListTrashedSessions()},
|
|
{"ListWorkspaces", app.ListWorkspaces()},
|
|
{"History", app.History()},
|
|
{"Jobs", app.Jobs()},
|
|
{"Commands", app.Commands()},
|
|
{"Models", app.Models()},
|
|
{"ListDir", app.ListDir("__missing__")},
|
|
{"ListDirForTab", app.ListDirForTab("missing", "")},
|
|
{"SearchFileRefsForTab", app.SearchFileRefsForTab("missing", "file")},
|
|
{"ListTabs", app.ListTabs()},
|
|
{"ListProjectTree", app.ListProjectTree()},
|
|
{"AvailableSubagentTools", app.AvailableSubagentTools()},
|
|
{"AutoResearchList", app.AutoResearchList("missing")},
|
|
{"AutoResearchFindings", app.AutoResearchFindings("missing", 10)},
|
|
{"MCPServers", app.MCPServers()},
|
|
{"Plugins", app.Plugins()},
|
|
{"HeartbeatListTasks", app.HeartbeatListTasks()},
|
|
{"HeartbeatReloadTasks", app.HeartbeatReloadTasks()},
|
|
}
|
|
for _, tc := range cases {
|
|
assertNonNilSliceJSON(t, tc.name, tc.got)
|
|
}
|
|
|
|
if got := app.SlashArgs("/skill "); got.Items == nil {
|
|
t.Fatal("SlashArgs().Items is nil; frontend expects []")
|
|
}
|
|
if got := app.WorkspaceChanges(""); got.Files == nil {
|
|
t.Fatal(`WorkspaceChanges("").Files is nil; frontend expects []`)
|
|
}
|
|
if got := app.ContextPanel("missing"); got.ReadFiles == nil || got.ChangedFiles == nil {
|
|
t.Fatalf("ContextPanel(missing) arrays = read:%v changed:%v, want non-nil", got.ReadFiles, got.ChangedFiles)
|
|
}
|
|
if got := app.HooksSettings("global"); got.Hooks == nil || got.Events == nil {
|
|
t.Fatalf("HooksSettings(global) arrays = hooks:%v events:%v, want non-nil", got.Hooks, got.Events)
|
|
}
|
|
if got := app.Settings(); got.Providers == nil || got.OfficialProviders == nil || got.ProviderPresets == nil || got.ProviderKinds == nil ||
|
|
got.Permissions.Allow == nil || got.Permissions.Ask == nil || got.Permissions.Deny == nil ||
|
|
got.Sandbox.AllowWrite == nil || got.Sandbox.EffectiveWriteRoots == nil ||
|
|
got.Bot.Allowlist.QQUsers == nil || got.Bot.Allowlist.FeishuUsers == nil || got.Bot.Allowlist.WeixinUsers == nil ||
|
|
got.Bot.Allowlist.QQGroups == nil || got.Bot.Allowlist.FeishuGroups == nil || got.Bot.Allowlist.WeixinGroups == nil {
|
|
t.Fatalf("Settings() contains nil array fields: %+v", got)
|
|
}
|
|
if got := app.DesktopStartupSettings(); got.StatusBarItems == nil ||
|
|
got.Bot.Allowlist.QQUsers == nil || got.Bot.Allowlist.FeishuUsers == nil || got.Bot.Allowlist.WeixinUsers == nil ||
|
|
got.Bot.Allowlist.QQGroups == nil || got.Bot.Allowlist.FeishuGroups == nil || got.Bot.Allowlist.WeixinGroups == nil {
|
|
t.Fatalf("DesktopStartupSettings() contains nil array fields: %+v", got)
|
|
}
|
|
|
|
boundPayloads := []struct {
|
|
name string
|
|
got any
|
|
}{
|
|
{"CapabilityDiagnostics", app.CapabilityDiagnostics(false)},
|
|
{"Capabilities", app.Capabilities()},
|
|
{"SkillsSettings", app.SkillsSettings()},
|
|
{"HistoryPage", app.HistoryPage(0, 20)},
|
|
{"Effort", app.Effort()},
|
|
{"Memory", app.Memory()},
|
|
{"MemorySuggestions", app.MemorySuggestions()},
|
|
}
|
|
for _, tc := range boundPayloads {
|
|
assertRequiredJSONSlicesNonNil(t, tc.name, reflect.ValueOf(tc.got))
|
|
}
|
|
}
|
|
|
|
func assertNonNilSliceJSON(t *testing.T, name string, got any) {
|
|
t.Helper()
|
|
v := reflect.ValueOf(got)
|
|
if v.Kind() != reflect.Slice {
|
|
t.Fatalf("%s returned %T, want slice", name, got)
|
|
}
|
|
if v.IsNil() {
|
|
t.Fatalf("%s returned nil slice; frontend expects []", name)
|
|
}
|
|
raw, err := json.Marshal(got)
|
|
if err != nil {
|
|
t.Fatalf("%s JSON marshal: %v", name, err)
|
|
}
|
|
if string(raw) == "null" {
|
|
t.Fatalf("%s JSON encoded as null; frontend expects []", name)
|
|
}
|
|
}
|
|
|
|
func assertRequiredJSONSlicesNonNil(t *testing.T, path string, value reflect.Value) {
|
|
t.Helper()
|
|
for value.Kind() == reflect.Interface || value.Kind() == reflect.Pointer {
|
|
if value.IsNil() {
|
|
return
|
|
}
|
|
value = value.Elem()
|
|
}
|
|
|
|
switch value.Kind() {
|
|
case reflect.Slice, reflect.Array:
|
|
if value.Kind() == reflect.Slice && value.IsNil() {
|
|
t.Fatalf("%s is a nil slice; JSON contract requires []", path)
|
|
}
|
|
for i := 0; i < value.Len(); i++ {
|
|
assertRequiredJSONSlicesNonNil(t, path, value.Index(i))
|
|
}
|
|
case reflect.Struct:
|
|
typ := value.Type()
|
|
for i := 0; i < value.NumField(); i++ {
|
|
fieldType := typ.Field(i)
|
|
if fieldType.PkgPath != "" {
|
|
continue
|
|
}
|
|
jsonTag := fieldType.Tag.Get("json")
|
|
if jsonTag == "-" || strings.Contains(jsonTag, ",omitempty") {
|
|
continue
|
|
}
|
|
fieldPath := path + "." + fieldType.Name
|
|
assertRequiredJSONSlicesNonNil(t, fieldPath, value.Field(i))
|
|
}
|
|
}
|
|
}
|