Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

677 lines
20 KiB
Go

package server_test
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"unicode/utf16"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
)
func canonicalTestPath(path string) string {
if path == "" {
return ""
}
clean := filepath.Clean(path)
if resolved, err := filepath.EvalSymlinks(clean); err == nil {
clean = filepath.Clean(resolved)
}
if runtime.GOOS == "darwin" && strings.HasPrefix(clean, "/private/") {
publicPath := filepath.Clean(strings.TrimPrefix(clean, "/private"))
if info, err := os.Stat(publicPath); err == nil && info.IsDir() {
return publicPath
}
}
return clean
}
func assertSamePath(t *testing.T, label, got, want string) {
t.Helper()
got = canonicalTestPath(got)
want = canonicalTestPath(want)
if got == want {
return
}
gotInfo, gotErr := os.Stat(got)
wantInfo, wantErr := os.Stat(want)
if gotErr == nil && wantErr == nil && os.SameFile(gotInfo, wantInfo) {
return
}
assert.Fail(t, "path mismatch", "%s = %q, want %q", label, got, want)
}
func messagePointPromptGlob(t *testing.T, sessionID string, ordinal int) string {
t.Helper()
cacheDir, err := os.UserCacheDir()
require.NoError(t, err)
return filepath.Join(
cacheDir,
"agentsview",
"claude-message-points",
fmt.Sprintf("%s-ordinal-%d-*.txt", sessionID, ordinal),
)
}
func removeMessagePointPrompts(t *testing.T, sessionID string, ordinal int) {
t.Helper()
matches, err := filepath.Glob(
messagePointPromptGlob(t, sessionID, ordinal),
)
require.NoError(t, err)
for _, match := range matches {
_ = os.Remove(match)
}
}
func findSingleMessagePointPrompt(
t *testing.T, sessionID string, ordinal int,
) string {
t.Helper()
matches, err := filepath.Glob(
messagePointPromptGlob(t, sessionID, ordinal),
)
require.NoError(t, err)
require.Len(t, matches, 1)
return matches[0]
}
func assertNoMessagePointPrompts(
t *testing.T, sessionID string, ordinal int,
) {
t.Helper()
matches, err := filepath.Glob(
messagePointPromptGlob(t, sessionID, ordinal),
)
require.NoError(t, err)
assert.Empty(t, matches)
}
func assertMessagePointCommandForRuntime(
t *testing.T, command string, promptPath string,
) {
t.Helper()
if runtime.GOOS == "windows" {
script := decodeMessagePointPowerShellCommandForTest(t, command)
quotedPromptPath := powerShellSingleQuoteForTest(promptPath)
assert.Contains(t, script,
"Get-Content -Raw -Encoding UTF8 -LiteralPath "+
quotedPromptPath)
assert.Contains(t, script,
"Remove-Item -LiteralPath "+quotedPromptPath+
" -Force -ErrorAction SilentlyContinue")
assert.NotContains(t, command, " < ")
assert.NotContains(t, command, "rm -f --")
assert.NotContains(t, script, " < ")
assert.NotContains(t, script, "rm -f --")
return
}
assert.Contains(t, command, "claude <")
assert.Contains(t, command, "rm -f --")
}
func decodeMessagePointPowerShellCommandForTest(
t *testing.T, command string,
) string {
t.Helper()
const prefix = "powershell.exe -NoProfile -EncodedCommand "
require.True(t, strings.HasPrefix(command, prefix), "command = %q", command)
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(command, prefix))
require.NoError(t, err)
require.Zero(t, len(raw)%2, "UTF-16LE byte length must be even")
codeUnits := make([]uint16, len(raw)/2)
for i := range codeUnits {
codeUnits[i] = binary.LittleEndian.Uint16(raw[i*2:])
}
return string(utf16.Decode(codeUnits))
}
func powerShellSingleQuoteForTest(s string) string {
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}
func TestResumeSession(t *testing.T) {
te := setup(t)
// Seed a claude session with an absolute project path.
projectDir := t.TempDir()
te.seedSession(t, "sess-1", projectDir, 5, func(s *db.Session) {
s.Agent = "claude"
})
t.Run("command only", func(t *testing.T) {
w := te.post(t,
"/api/v1/sessions/sess-1/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
Cwd string `json:"cwd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
assert.NotEmpty(t, resp.Command)
assertSamePath(t, "cwd", resp.Cwd, projectDir)
})
t.Run("fork session command only", func(t *testing.T) {
w := te.post(t,
"/api/v1/sessions/sess-1/resume",
`{"command_only":true,"fork_session":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
Cwd string `json:"cwd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
assert.Contains(t, resp.Command, "claude --resume sess-1 --fork-session")
assertSamePath(t, "cwd", resp.Cwd, projectDir)
})
t.Run("not found", func(t *testing.T) {
w := te.post(t,
"/api/v1/sessions/nonexistent/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusNotFound)
})
t.Run("copilot command only", func(t *testing.T) {
projectDir := t.TempDir()
// Use a prefixed ID to exercise the agent-prefix stripping
// logic (e.g. "copilot:abc123" → raw ID "abc123").
te.seedSession(t, "copilot:abc123", projectDir, 3, func(s *db.Session) {
s.Agent = "copilot"
})
w := te.post(t,
"/api/v1/sessions/copilot:abc123/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
assert.Equal(t, "copilot --resume=abc123", resp.Command)
})
t.Run("kiro current-store command only", func(t *testing.T) {
projectDir := t.TempDir()
te.seedSession(t, "kiro:sqlite-chat", "kiro_app", 3, func(s *db.Session) {
s.Agent = "kiro"
s.Cwd = projectDir
})
w := te.post(t,
"/api/v1/sessions/kiro:sqlite-chat/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
Cwd string `json:"cwd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
const cmdSuffix = "' && kiro-cli chat --resume-id sqlite-chat"
if !strings.HasPrefix(resp.Command, "cd '") ||
!strings.HasSuffix(resp.Command, cmdSuffix) {
assert.Fail(t, "command shape mismatch",
"command = %q, want cd command ending with %q",
resp.Command, cmdSuffix)
} else {
commandCwd := strings.TrimSuffix(
strings.TrimPrefix(resp.Command, "cd '"),
cmdSuffix,
)
assertSamePath(t, "command cwd", commandCwd, projectDir)
}
assertSamePath(t, "cwd", resp.Cwd, projectDir)
})
t.Run("claude desktop rejects non-claude agent", func(t *testing.T) {
te.seedSession(t, "codex-desk", t.TempDir(), 3, func(s *db.Session) {
s.Agent = "codex"
})
w := te.post(t,
"/api/v1/sessions/codex-desk/resume",
`{"opener_id":"claude-desktop"}`,
)
assertStatus(t, w, http.StatusBadRequest)
})
t.Run("cursor command only", func(t *testing.T) {
projectDir := t.TempDir()
runDir := filepath.Join(projectDir, "frontend")
require.NoError(t, os.MkdirAll(runDir, 0o755))
runDirJSON, _ := json.Marshal(runDir)
sessionFile := filepath.Join(t.TempDir(), "cursor.jsonl")
content := `{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Shell","input":{"command":"pwd","working_directory":` +
string(runDirJSON) + `}}]}}` + "\n"
require.NoError(t, os.WriteFile(sessionFile, []byte(content), 0o644))
te.seedSession(t, "cursor:chat-1", projectDir, 3, func(s *db.Session) {
s.Agent = "cursor"
s.FilePath = &sessionFile
})
w := te.post(t,
"/api/v1/sessions/cursor:chat-1/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
Cwd string `json:"cwd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
wantProjectDir := canonicalTestPath(projectDir)
assert.Equal(t,
"cursor agent --resume chat-1 --workspace '"+wantProjectDir+"'",
resp.Command)
assertSamePath(t, "cwd", resp.Cwd, runDir)
})
t.Run("cursor command only falls back workspace to cwd", func(t *testing.T) {
runDir := filepath.Join(t.TempDir(), "frontend")
require.NoError(t, os.MkdirAll(runDir, 0o755))
runDirJSON, _ := json.Marshal(runDir)
sessionFile := filepath.Join(t.TempDir(), "cursor.jsonl")
content := `{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Shell","input":{"command":"pwd","working_directory":` +
string(runDirJSON) + `}}]}}` + "\n"
require.NoError(t, os.WriteFile(sessionFile, []byte(content), 0o644))
te.seedSession(t, "cursor:chat-2", "li_tools", 3, func(s *db.Session) {
s.Agent = "cursor"
s.FilePath = &sessionFile
})
w := te.post(t,
"/api/v1/sessions/cursor:chat-2/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
Cwd string `json:"cwd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
wantRunDir := canonicalTestPath(runDir)
assert.Equal(t,
"cursor agent --resume chat-2 --workspace '"+wantRunDir+"'",
resp.Command)
assertSamePath(t, "cwd", resp.Cwd, runDir)
})
t.Run("unsupported agent", func(t *testing.T) {
te.seedSession(t, "vscode-1", "/tmp", 3, func(s *db.Session) {
s.Agent = "vscode-copilot"
})
w := te.post(t,
"/api/v1/sessions/vscode-1/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusBadRequest)
})
t.Run("message point command only", func(t *testing.T) {
removeMessagePointPrompts(t, "sess-2", 1)
te.seedSession(t, "sess-2", projectDir, 3, func(s *db.Session) {
s.Agent = "claude"
})
te.seedMessages(t, "sess-2", 3)
w := te.post(t,
"/api/v1/sessions/sess-2/resume",
`{"command_only":true,"from_ordinal":1,"fork_session":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
Cwd string `json:"cwd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
promptPath := findSingleMessagePointPrompt(t, "sess-2", 1)
assertMessagePointCommandForRuntime(t, resp.Command, promptPath)
if runtime.GOOS != "windows" {
assert.Contains(t, resp.Command, "< '")
}
assertSamePath(t, "cwd", resp.Cwd, projectDir)
if runtime.GOOS != "windows" {
idx := strings.LastIndex(resp.Command, "< ")
require.Greater(t, idx, 0, "command = %q", resp.Command)
extracted := strings.TrimSpace(resp.Command[idx+2:])
if semi := strings.Index(extracted, ";"); semi >= 0 {
extracted = strings.TrimSpace(extracted[:semi])
}
extracted = strings.TrimPrefix(extracted, "'")
extracted = strings.TrimSuffix(extracted, "'")
assertSamePath(t, "prompt file", extracted, promptPath)
}
data, err := os.ReadFile(promptPath)
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(promptPath) })
text := string(data)
assert.Contains(t, text, "Message A")
assert.Contains(t, text, "Message B")
assert.NotContains(t, text, "Message C")
})
t.Run("message point command only finds sparse ordinals", func(t *testing.T) {
removeMessagePointPrompts(t, "sess-sparse", 3)
te.seedSession(t, "sess-sparse", projectDir, 3, func(s *db.Session) {
s.Agent = "claude"
})
te.seedMessages(t, "sess-sparse", 3, func(i int, m *db.Message) {
if i == 2 {
m.Ordinal = 3
m.Content = "Message D"
}
})
w := te.post(t,
"/api/v1/sessions/sess-sparse/resume",
`{"command_only":true,"from_ordinal":3,"fork_session":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
Cwd string `json:"cwd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched, "expected launched=false for command_only")
promptPath := findSingleMessagePointPrompt(t, "sess-sparse", 3)
assertMessagePointCommandForRuntime(t, resp.Command, promptPath)
assertSamePath(t, "cwd", resp.Cwd, projectDir)
t.Cleanup(func() { _ = os.Remove(promptPath) })
data, err := os.ReadFile(promptPath)
require.NoError(t, err)
text := string(data)
assert.Contains(t, text, "Message A")
assert.Contains(t, text, "Message B")
assert.Contains(t, text, "Message D")
})
t.Run("message point rejects unsupported agents", func(t *testing.T) {
removeMessagePointPrompts(t, "codex-desk", 0)
te.seedSession(t, "codex-desk", t.TempDir(), 3, func(s *db.Session) {
s.Agent = "codex"
})
w := te.post(t,
"/api/v1/sessions/codex-desk/resume",
`{"command_only":true,"from_ordinal":0,"fork_session":true}`,
)
assertStatus(t, w, http.StatusBadRequest)
assertNoMessagePointPrompts(t, "codex-desk", 0)
})
t.Run("message point requires fork session", func(t *testing.T) {
removeMessagePointPrompts(t, "sess-need-fork", 0)
te.seedSession(t, "sess-need-fork", projectDir, 3, func(s *db.Session) {
s.Agent = "claude"
})
te.seedMessages(t, "sess-need-fork", 3)
w := te.post(t,
"/api/v1/sessions/sess-need-fork/resume",
`{"command_only":true,"from_ordinal":0}`,
)
assertStatus(t, w, http.StatusBadRequest)
assertNoMessagePointPrompts(t, "sess-need-fork", 0)
})
t.Run("message point rejects opener id", func(t *testing.T) {
removeMessagePointPrompts(t, "sess-opener", 0)
te.seedSession(t, "sess-opener", projectDir, 3, func(s *db.Session) {
s.Agent = "claude"
})
te.seedMessages(t, "sess-opener", 3)
w := te.post(t,
"/api/v1/sessions/sess-opener/resume",
`{"command_only":true,"from_ordinal":0,"fork_session":true,"opener_id":"claude-desktop"}`,
)
assertStatus(t, w, http.StatusBadRequest)
assertNoMessagePointPrompts(t, "sess-opener", 0)
})
t.Run("message point rejects missing ordinals", func(t *testing.T) {
removeMessagePointPrompts(t, "sess-3", 99)
te.seedSession(t, "sess-3", projectDir, 3, func(s *db.Session) {
s.Agent = "claude"
})
te.seedMessages(t, "sess-3", 3)
w := te.post(t,
"/api/v1/sessions/sess-3/resume",
`{"command_only":true,"from_ordinal":99,"fork_session":true}`,
)
assertStatus(t, w, http.StatusNotFound)
assertNoMessagePointPrompts(t, "sess-3", 99)
})
t.Run("message point remote launch rejects before writing prompt", func(t *testing.T) {
te := setupPGMode(t)
removeMessagePointPrompts(t, "sess-remote", 1)
te.seedSession(t, "sess-remote", projectDir, 3, func(s *db.Session) {
s.Agent = "claude"
})
te.seedMessages(t, "sess-remote", 3)
w := te.post(t,
"/api/v1/sessions/sess-remote/resume",
`{"from_ordinal":1,"fork_session":true}`,
)
assertStatus(t, w, http.StatusNotImplemented)
assertNoMessagePointPrompts(t, "sess-remote", 1)
})
t.Run("message point command only works in read only mode", func(t *testing.T) {
te := setupPGMode(t)
removeMessagePointPrompts(t, "sess-remote-copy", 1)
te.seedSession(t, "sess-remote-copy", projectDir, 3, func(s *db.Session) {
s.Agent = "claude"
})
te.seedMessages(t, "sess-remote-copy", 3)
w := te.post(t,
"/api/v1/sessions/sess-remote-copy/resume",
`{"command_only":true,"from_ordinal":1,"fork_session":true}`,
)
assertStatus(t, w, http.StatusOK)
var resp struct {
Launched bool `json:"launched"`
Command string `json:"command"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.False(t, resp.Launched)
promptPath := findSingleMessagePointPrompt(t, "sess-remote-copy", 1)
assertMessagePointCommandForRuntime(t, resp.Command, promptPath)
t.Cleanup(func() { _ = os.Remove(promptPath) })
})
t.Run("deleted session rejected", func(t *testing.T) {
te.seedSession(t, "del-1", "/tmp", 3, func(s *db.Session) {
s.Agent = "claude"
})
require.NoError(t, te.db.SoftDeleteSession("del-1"))
w := te.post(t,
"/api/v1/sessions/del-1/resume",
`{"command_only":true}`,
)
assertStatus(t, w, http.StatusNotFound)
})
}
func TestGetSessionDirectory(t *testing.T) {
te := setup(t)
projectDir := t.TempDir()
te.seedSession(t, "dir-1", projectDir, 3)
t.Run("returns resolved directory", func(t *testing.T) {
w := te.get(t, "/api/v1/sessions/dir-1/directory")
assertStatus(t, w, http.StatusOK)
var resp struct {
Path string `json:"path"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assertSamePath(t, "path", resp.Path, projectDir)
})
t.Run("empty path for relative project", func(t *testing.T) {
te.seedSession(t, "dir-2", "my-repo", 3)
w := te.get(t, "/api/v1/sessions/dir-2/directory")
assertStatus(t, w, http.StatusOK)
var resp struct {
Path string `json:"path"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Empty(t, resp.Path)
})
t.Run("not found", func(t *testing.T) {
w := te.get(t, "/api/v1/sessions/nonexistent/directory")
assertStatus(t, w, http.StatusNotFound)
})
t.Run("prefers session file cwd", func(t *testing.T) {
cwdDir := filepath.Join(t.TempDir(), "nested")
require.NoError(t, os.Mkdir(cwdDir, 0o755))
sessionFile := filepath.Join(t.TempDir(), "session.jsonl")
cwdJSON, _ := json.Marshal(cwdDir)
content := `{"cwd":` + string(cwdJSON) + "}\n"
require.NoError(t, os.WriteFile(sessionFile, []byte(content), 0o644))
te.seedSession(t, "dir-3", projectDir, 3, func(s *db.Session) {
s.FilePath = &sessionFile
})
w := te.get(t, "/api/v1/sessions/dir-3/directory")
assertStatus(t, w, http.StatusOK)
var resp struct {
Path string `json:"path"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assertSamePath(t, "path", resp.Path, cwdDir)
})
t.Run("cursor directory returns workspace root", func(t *testing.T) {
projectDir := t.TempDir()
runDir := filepath.Join(projectDir, "frontend")
require.NoError(t, os.MkdirAll(runDir, 0o755))
runDirJSON, _ := json.Marshal(runDir)
sessionFile := filepath.Join(t.TempDir(), "cursor.jsonl")
content := `{"role":"assistant","message":{"content":[{"type":"tool_use","name":"Shell","input":{"command":"pwd","working_directory":` +
string(runDirJSON) + `}}]}}` + "\n"
require.NoError(t, os.WriteFile(sessionFile, []byte(content), 0o644))
te.seedSession(t, "dir-cursor", projectDir, 3, func(s *db.Session) {
s.Agent = "cursor"
s.FilePath = &sessionFile
})
w := te.get(t, "/api/v1/sessions/dir-cursor/directory")
assertStatus(t, w, http.StatusOK)
var resp struct {
Path string `json:"path"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assertSamePath(t, "path", resp.Path, projectDir)
})
}
func TestListOpeners(t *testing.T) {
te := setup(t)
w := te.get(t, "/api/v1/openers")
assertStatus(t, w, http.StatusOK)
var resp struct {
Openers []struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
Bin string `json:"bin"`
} `json:"openers"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
// The response should always be an array (possibly empty),
// never null.
assert.NotNil(t, resp.Openers, "openers should be [] not null")
}
func TestGetTerminalConfig(t *testing.T) {
te := setup(t)
t.Run("default config", func(t *testing.T) {
w := te.get(t, "/api/v1/config/terminal")
assertStatus(t, w, http.StatusOK)
var resp struct {
Mode string `json:"mode"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(t, "auto", resp.Mode)
})
t.Run("set and get", func(t *testing.T) {
w := te.post(t,
"/api/v1/config/terminal",
`{"mode":"clipboard"}`,
)
assertStatus(t, w, http.StatusOK)
w = te.get(t, "/api/v1/config/terminal")
assertStatus(t, w, http.StatusOK)
var resp struct {
Mode string `json:"mode"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(t, "clipboard", resp.Mode)
})
t.Run("invalid mode", func(t *testing.T) {
w := te.post(t,
"/api/v1/config/terminal",
`{"mode":"invalid"}`,
)
assertStatus(t, w, http.StatusBadRequest)
})
t.Run("custom requires bin", func(t *testing.T) {
w := te.post(t,
"/api/v1/config/terminal",
`{"mode":"custom","custom_bin":""}`,
)
assertStatus(t, w, http.StatusBadRequest)
})
}