Files
streamdeck-go/internal/config/edit_test.go
Levi Woodard 8cff4f8418 Add streamdeck-ctl JSON control CLI
streamdeck-ctl reports device/daemon/key status as JSON and edits the
config (brightness, key set/clear/press, daemon control) so front-ends
don't have to reimplement YAML handling or device probing.

- internal/config/edit.go: comment-preserving, node-level YAML edits with
  atomic writes; key indices written as !!int so config.Load accepts them.
- internal/device: add model names, Lookup() and Present() for probing
  without opening the HID handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjuZBBfzXqZgxRhxzJvkkC
2026-09-27 18:21:33 -06:00

127 lines
3.2 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func writeConfig(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
// Regression: a new key's index must be written as an int scalar. A !!str key
// ("25":) makes config.Load fail — yaml.v3 refuses string keys for map[int] —
// which bricked the config on the daemon's next hot reload.
func TestApplyKeyEditNewSlotLoadsBack(t *testing.T) {
path := writeConfig(t, "icons_dir: ./icons\nbrightness: 70\nkeys: {}\n")
err := ApplyKeyEdit(path, 25, KeyEdit{Set: map[string]any{
"icon": "test.png",
"command": "echo hi",
}})
if err != nil {
t.Fatalf("ApplyKeyEdit: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load after edit: %v", err)
}
key, ok := cfg.Keys[25]
if !ok {
t.Fatalf("key 25 missing after edit; keys=%v", cfg.Keys)
}
if key.Icon != "test.png" || key.Command != "echo hi" {
t.Fatalf("key 25 = %+v", key)
}
raw, _ := os.ReadFile(path)
if strings.Contains(string(raw), `"25"`) {
t.Fatalf("index written as a quoted string:\n%s", raw)
}
}
// Editing an existing key must merge, and comments/blank lines must survive.
func TestApplyKeyEditMergePreservesFormatting(t *testing.T) {
path := writeConfig(t, `icons_dir: ./icons
# the deck
keys:
0:
icon: a.png # keep me
command: run-a
`)
if err := ApplyKeyEdit(path, 0, KeyEdit{Set: map[string]any{"text": "Hi"}}); err != nil {
t.Fatalf("ApplyKeyEdit: %v", err)
}
raw, _ := os.ReadFile(path)
text := string(raw)
for _, want := range []string{"# the deck", "# keep me", "command: run-a", "text: Hi", "\n\n"} {
if !strings.Contains(text, want) {
t.Fatalf("output missing %q:\n%s", want, text)
}
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Keys[0].Icon != "a.png" || cfg.Keys[0].Text != "Hi" {
t.Fatalf("merge lost fields: %+v", cfg.Keys[0])
}
}
// Unset is applied before Set, so a field named in both ends up set.
func TestApplyKeyEditSetWinsOverUnset(t *testing.T) {
path := writeConfig(t, "keys:\n 0:\n command: old\n")
err := ApplyKeyEdit(path, 0, KeyEdit{
Set: map[string]any{"command": "new"},
Unset: []string{"command"},
})
if err != nil {
t.Fatalf("ApplyKeyEdit: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Keys[0].Command != "new" {
t.Fatalf("command = %q, want %q", cfg.Keys[0].Command, "new")
}
}
func TestClearKeyAndSetBrightness(t *testing.T) {
path := writeConfig(t, "brightness: 70\nkeys:\n 3:\n command: x\n")
if err := ClearKey(path, 3); err != nil {
t.Fatalf("ClearKey: %v", err)
}
if err := ClearKey(path, 99); err != nil { // absent key is not an error
t.Fatalf("ClearKey absent: %v", err)
}
if err := SetBrightness(path, 140); err != nil { // clamped
t.Fatalf("SetBrightness: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(cfg.Keys) != 0 {
t.Fatalf("keys not cleared: %v", cfg.Keys)
}
if cfg.Brightness != 100 {
t.Fatalf("brightness = %d, want 100", cfg.Brightness)
}
}