package main import ( "os" "strings" "testing" "github.com/spf13/cobra" ) // The --device flag accepts the 1-based number that `keyboard info` prints. // Its help text is the only place a user learns that, so it must not // describe the HID path the flag rejects. func TestDeviceFlagUsageDescribesANumberNotAPath(t *testing.T) { flag := newRootCommand().PersistentFlags().Lookup("device") if flag == nil { t.Fatal("--device flag not found") } if strings.Contains(flag.Usage, "path") { t.Errorf("--device usage = %q, must not mention a path; the flag rejects HID paths", flag.Usage) } if !strings.Contains(flag.Usage, "number") { t.Errorf("--device usage = %q, want it to say the argument is a number", flag.Usage) } } // Cobra reads a back-quoted word in a flag's usage as the value placeholder // and renders it where the type name would go, turning `--device into // `--device keyboard info`. The help must therefore contain no backticks. func TestFlagUsageHasNoValuePlaceholder(t *testing.T) { root := newRootCommand() for _, name := range []string{"device", "definition", "json"} { flag := root.PersistentFlags().Lookup(name) if flag == nil { t.Errorf("--%s is registered on the root, want it", name) continue } if strings.Contains(flag.Usage, "`") { t.Errorf("--%s usage = %q, backticks make cobra render a bogus value placeholder", name, flag.Usage) } } } // The flag usage is not the only place a user learns what --device takes. The // same promise is repeated in three documentation files, and it was wrong in // all of them at once. Pin the docs to the flag's actual contract by looking // for a value that looks like a HID path, not for the word "path" — prose // about why paths are unstable, and the rule that forbids them, must pass. func TestDocsDoNotOfferAPathValueForDevice(t *testing.T) { pathLike := []string{"/dev/", "hidraw", `\\?\hid`, "IO/HIDDevice"} for _, path := range []string{ "../../README.md", "../../AGENTS.md", "../../.claude/skills/qmk-rgb/SKILL.md", } { data, err := os.ReadFile(path) if err != nil { t.Errorf("read %s: %v", path, err) continue } for _, line := range strings.Split(string(data), "\n") { if !strings.Contains(line, "--device") { continue } for _, needle := range pathLike { if strings.Contains(line, needle) { t.Errorf("%s offers a path value for --device: %q", path, strings.TrimSpace(line)) } } } } } // The docs must not teach a zone name the resolver rejects. The subsystem // names are the vocabulary; a board's own channel names come from its definition file. func TestDocsDoNotTeachAZoneNameTheResolverRejects(t *testing.T) { for _, path := range []string{ "../../README.md", "../../AGENTS.md", "../../.claude/skills/qmk-rgb/SKILL.md", } { data, err := os.ReadFile(path) if err != nil { t.Errorf("read %s: %v", path, err) continue } body := string(data) for _, wrong := range []string{"zone logo|backlight", "zone matrix", "zone=matrix"} { if strings.Contains(body, wrong) { t.Errorf("%s contains %q, want only the canonical zone names", path, wrong) } } } } // A usage line is the one place a command's arity is declared, and the docs // repeat it. The two drifted here: the table spelled `effect ` // with the name required, while the command reads with one argument and writes // with two. Pin the table to the line the command actually prints, and keep every // message that offers the ID form from dropping the zone — `effect 17` alone is // read as a channel named `17`, so advice in that form does not run. func TestDocsSpellTheEffectUsageAsTheCommandDoes(t *testing.T) { used := NewEffectCmd().Use if !strings.Contains(used, "[name|index]") { t.Fatalf("effect Use = %q, want it to mark the name optional", used) } want := "qmk-rgb-tool " + used readme, err := os.ReadFile("../../README.md") if err != nil { t.Fatalf("read README.md: %v", err) } // The reference table lives in a markdown table, where a pipe inside a cell is // escaped, so the line carries a backslash the usage string does not. Compare // against the unescaped form rather than making the table write a broken cell. if !strings.Contains(strings.ReplaceAll(string(readme), `\|`, "|"), want) { t.Errorf("README.md does not carry the usage line %q", want) } // The bare form is the defect, so look for exactly that and not for the // substring it shares with the correct one. for _, path := range []string{ "../../README.md", "../../AGENTS.md", "../../.claude/skills/qmk-rgb/SKILL.md", "effect.go", "enable.go", "profile.go", "../../internal/rgb/catalog.go", } { data, err := os.ReadFile(path) if err != nil { t.Errorf("read %s: %v", path, err) continue } for i, line := range strings.Split(string(data), "\n") { if strings.Contains(line, "effect ") { t.Errorf("%s:%d points at `effect `, which reads the ID as a zone: %q", path, i+1, strings.TrimSpace(line)) } } } } // The zone is a positional argument now, so its help is the Long text the // command carries rather than a flag's usage string. Every subsystem name has to // be discoverable there, because a user reading `brightness --help` has nowhere // else to learn what a zone may be. func TestZoneHelpNamesTheChannelVocabulary(t *testing.T) { cmds := map[string]*cobra.Command{ "effect": NewEffectCmd(), "brightness": NewBrightnessCmd(), "speed": NewSpeedCmd(), "color": NewColorCmd(), "enable": NewEnableCmd(), "disable": NewDisableCmd(), } for name, cmd := range cmds { t.Run(name, func(t *testing.T) { for _, zone := range []string{"backlight", "rgblight", "rgb_matrix", "audio", "led_matrix"} { if !strings.Contains(cmd.Long, zone) { t.Errorf("%s help does not name the subsystem %q", name, zone) } } if !strings.Contains(cmd.Long, "all") { t.Errorf("%s help does not say that all is a zone", name) } if !strings.Contains(cmd.Long, "comma") { t.Errorf("%s help does not say that several zones may be written at once", name) } // The board-supplied names are not a fixed vocabulary, so the help // must not claim they are. if strings.Contains(cmd.Long, "logo") || strings.Contains(cmd.Long, "side") { t.Errorf("%s help names channels a board supplies", name) } }) } }