Slack comes from the AUR and Toolbox from JetBrains' self-updating tarball, so neither is declared: the host installs official packages only, and a pinned archive would fight Toolbox's own updates. Slack's start and the operator's i3 window rules become one node-display-session contribution, because Slack's own launch-on-login is a symlink in ~/.config/autostart. Toolbox keeps its own autostart entry as its one start. The shared desktop.go header now names all four bundles.
157 lines
5.3 KiB
Go
157 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// slack's shape (novox/hq ADR 0208, ADR 0210, ADR 0212, ADR 0182): no package (Slack comes from the
|
|
// AUR, and the host installs from the official repositories only), no seat, no file; the X display on
|
|
// its own machine; one contribution to node-display-session that is both Slack's one start and its
|
|
// window rules; and the Go bundle serving exactly the listed slack_ tools.
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Capabilities []string `json:"capabilities"`
|
|
Requires []string `json:"requires"`
|
|
Tools []string `json:"tools"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Claims []any `json:"claims"`
|
|
Seats []any `json:"seats"`
|
|
Shell []any `json:"shell"`
|
|
Contributions []struct {
|
|
Seat string `json:"seat"`
|
|
Kind string `json:"kind"`
|
|
Content string `json:"content"`
|
|
} `json:"contributions"`
|
|
Environment any `json:"environment"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) (manifest, string) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
|
dec.DisallowUnknownFields()
|
|
var m manifest
|
|
if err := dec.Decode(&m); err != nil {
|
|
t.Fatalf("module.json: %v", err)
|
|
}
|
|
return m, string(raw)
|
|
}
|
|
|
|
func TestItInstallsNothingAndOwnsNoFile(t *testing.T) {
|
|
m, raw := readManifest(t)
|
|
if m.Module != "slack" || !reflect.DeepEqual(m.Requires, []string{"x11-display"}) {
|
|
t.Fatalf("%+v", m)
|
|
}
|
|
// slack-desktop is the AUR's: a package resource would ask the host for what it cannot install.
|
|
if m.Resources != nil || m.Capabilities != nil {
|
|
t.Fatalf("no package, no file: %v %v", m.Resources, m.Capabilities)
|
|
}
|
|
if m.Claims != nil || m.Seats != nil || m.Environment != nil || m.Shell != nil {
|
|
t.Fatal("it holds no seat, sets no environment and adds no session code")
|
|
}
|
|
// Slack's own setting writes ~/.config/autostart/slack.desktop (a link): never the module's.
|
|
for _, never := range []string{".config/autostart", ".config/Slack", "root-state", "Cookies"} {
|
|
if strings.Contains(raw, never) {
|
|
t.Errorf("module.json names %q", never)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheOneContributionIsTheStartAndTheWindowRules(t *testing.T) {
|
|
m, _ := readManifest(t)
|
|
if len(m.Contributions) != 1 {
|
|
t.Fatalf("%+v", m.Contributions)
|
|
}
|
|
c := m.Contributions[0]
|
|
if c.Seat != "node-display-session" || c.Kind != "config" || !strings.HasSuffix(c.Content, "\n") {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
var execs, rules []string
|
|
for _, l := range strings.Split(c.Content, "\n") {
|
|
l = strings.TrimSpace(l)
|
|
switch {
|
|
case strings.HasPrefix(l, "exec"):
|
|
execs = append(execs, l)
|
|
case strings.HasPrefix(l, "for_window"):
|
|
rules = append(rules, l)
|
|
case l == "" || strings.HasPrefix(l, "#"):
|
|
default:
|
|
t.Errorf("a line that is neither the start nor a rule: %q", l)
|
|
}
|
|
}
|
|
want := "exec --no-startup-id " + slackBin + " " + strings.Join(startArgs, " ")
|
|
if len(execs) != 1 || execs[0] != want {
|
|
t.Fatalf("one start, the package entry's arguments, as slack_restart starts it: %q", execs)
|
|
}
|
|
// The operator's rules of 2026-08 (50-slack.conf), verified then against Slack's windows.
|
|
wantRules := []string{
|
|
`for_window [class="(?i)^slack$" window_role="browser-window"] move to workspace $ws3`,
|
|
`for_window [class="(?i)^slack$" window_role="browser-window"] floating disable`,
|
|
`for_window [class="(?i)^slack$" window_role="browser-window"] border pixel 2`,
|
|
}
|
|
if !reflect.DeepEqual(rules, wantRules) {
|
|
t.Fatalf("%q", rules)
|
|
}
|
|
for _, r := range rules {
|
|
if !strings.HasPrefix(r, ruleMark) {
|
|
t.Errorf("slack_check recognises the module's rules by %q: %s", ruleMark, r)
|
|
}
|
|
}
|
|
// The placed lines start Slack once: the fake i3 configuration below holds exactly this content.
|
|
f := newFake(t)
|
|
f.write(testHome+"/"+i3Main, "exec --no-startup-id dex --autostart --environment i3\n# slack\n"+c.Content)
|
|
if s := f.starts(); len(s) != 1 || !s[0].Mine {
|
|
t.Fatalf("%+v", s)
|
|
}
|
|
if n, _ := f.windowRules(); n != 3 {
|
|
t.Fatalf("%d rules", n)
|
|
}
|
|
}
|
|
|
|
func TestTheToolsAgreeWithTheManifest(t *testing.T) {
|
|
m, raw := readManifest(t)
|
|
served := map[string]bool{}
|
|
for _, tool := range tools() {
|
|
served[tool.Name] = true
|
|
if !strings.HasPrefix(tool.Name, "slack_") || strings.TrimSpace(tool.Description) == "" {
|
|
t.Errorf("%s: prefixed slack_ and described", tool.Name)
|
|
}
|
|
}
|
|
for _, name := range m.Tools {
|
|
if !served[name] {
|
|
t.Errorf("module.json lists %s, which the bundle does not serve", name)
|
|
}
|
|
delete(served, name)
|
|
}
|
|
for name := range served {
|
|
t.Errorf("the bundle serves %s, which module.json does not list", name)
|
|
}
|
|
if len(m.Build.Artifacts) != 1 {
|
|
t.Fatalf("%v", m.Build.Artifacts)
|
|
}
|
|
b := m.Build.Artifacts[0]
|
|
if b["kind"] != "bundle" || b["language"] != "go" || b["system"] != "arch" ||
|
|
b["from"] != "cmd/slack-tools" || b["binary"] != "slack-tools" {
|
|
t.Errorf("the Go tools bundle: %v", b)
|
|
}
|
|
s := strings.ToLower(raw)
|
|
for _, never := range []string{"/home/", "jochen", "g14", "shanks", "novox.be", "http", "password", "token"} {
|
|
if strings.Contains(s, never) {
|
|
t.Errorf("module.json names %q", never)
|
|
}
|
|
}
|
|
}
|