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.
199 lines
8.0 KiB
Go
199 lines
8.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const tb = testHome + "/" + toolboxDir
|
|
|
|
// newToolbox is a Toolbox as the desktop machine keeps it: three IDEs, one on the EAP channel, its
|
|
// launch-at-login switch at its default, its own autostart entry, an account no answer may name.
|
|
func newToolbox(t *testing.T) *fake {
|
|
f := newFake(t)
|
|
f.write(tb+"/bin/jetbrains-toolbox", "\x7fELF")
|
|
if err := chmodX(f, tb+"/bin/jetbrains-toolbox"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.write(tb+"/bin/build.txt", "3.2.0.65851")
|
|
f.write(tb+"/.settings.json", `{"advanced":{"build_for_installed":true},"channel_rollback_max_history":1,
|
|
"shell_scripts":{"location":"/home/operator/.local/share/JetBrains/Toolbox/scripts"},"ui":{"theme":"dark"},
|
|
"tools":{"update_all_automatically":true},"jetbrains_account":{"active":"1838624"}}`)
|
|
f.write(tb+"/accounts.json", `{"accounts":[{"email":"operator@example.test","token":"secret-token"}]}`)
|
|
f.write(tb+"/state.json", `{"version":1,"appVersion":"3.2.0.65851","tools":[
|
|
{"channelId":"RustRover-dd65","toolId":"RustRover","displayName":"RustRover","displayVersion":"2026.1 EAP","buildNumber":"261.21525.29",
|
|
"installLocation":"/home/operator/.local/share/JetBrains/Toolbox/apps/rustrover","launchCommand":"x"},
|
|
{"channelId":"Rider-8c11","toolId":"Rider","displayName":"Rider","displayVersion":"2025.3.2","buildNumber":"253.30387.148",
|
|
"installLocation":"/home/operator/.local/share/JetBrains/Toolbox/apps/rider"},
|
|
{"channelId":"../../escape","toolId":"Fleet","displayName":"Fleet","displayVersion":"1.48.261 Public Preview",
|
|
"installLocation":"/home/operator/.local/share/JetBrains/Toolbox/apps/fleet"}]}`)
|
|
f.write(tb+"/channels/RustRover-dd65.json", `{"channel":{"updateFilter":{"application_type":"RustRover",
|
|
"quality_filter":{"name":"Early Access Program","order_value":40000}},"autoUpdate":true}}`)
|
|
f.write(tb+"/channels/Rider-8c11.json", `{"channel":{"updateFilter":{"quality_filter":{"name":"Release"}}}}`)
|
|
for _, d := range []string{"rustrover", "rider", "fleet", "webstorm-old"} {
|
|
f.write(tb+"/apps/"+d+"/build.txt", "x")
|
|
}
|
|
f.write(testHome+"/.config/autostart/"+entryName, "[Desktop Entry]\nExec=/home/operator/.local/share/JetBrains/Toolbox/bin/jetbrains-toolbox --minimize\n"+
|
|
"X-GNOME-Autostart-enabled=true\n")
|
|
f.answer = func(name string, args []string) Output { return Output{} }
|
|
return f
|
|
}
|
|
|
|
func noSecrets(t *testing.T, v any) string {
|
|
t.Helper()
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := string(raw)
|
|
for _, never := range []string{"1838624", "example.test", "secret-token", "/home/operator"} {
|
|
if strings.Contains(s, never) {
|
|
t.Errorf("the answer carries %q: %s", never, s)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestStatusListsTheIDEsWithTheirChannelsAndNoAccount(t *testing.T) {
|
|
f := newToolbox(t)
|
|
f.proc(4100, 1000, toolboxComm, []string{"/home/operator/.local/share/JetBrains/Toolbox/bin/jetbrains-toolbox", "--minimize"}, "session-4.scope")
|
|
s, err := f.Status()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
noSecrets(t, s)
|
|
if !s.Installed || s.Version != "3.2.0.65851" || len(s.Running) != 1 || !s.StartedBy.Starts {
|
|
t.Fatalf("%+v", s)
|
|
}
|
|
set := s.Settings
|
|
if !set.Found || !set.LaunchAtLogin || !set.LaunchDefault || !*set.UpdateTools || set.ShellScripts != "~/"+toolboxDir+"/scripts" || *set.RollbackKeeps != 1 {
|
|
t.Fatalf("%+v", set)
|
|
}
|
|
st := s.State
|
|
if st.AppVersion != "3.2.0.65851" || len(st.Tools) != 3 {
|
|
t.Fatalf("%+v", st)
|
|
}
|
|
fleet, rider, rr := st.Tools[0], st.Tools[1], st.Tools[2]
|
|
if rr.Name != "RustRover" || rr.Version != "2026.1 EAP" || rr.Channel != "Early Access Program" || rr.AutoUpdate == nil || !*rr.AutoUpdate ||
|
|
rr.Location != "~/"+toolboxDir+"/apps/rustrover" || !rr.Present {
|
|
t.Fatalf("%+v", rr)
|
|
}
|
|
if rider.Channel != "Release" || rider.AutoUpdate != nil {
|
|
t.Fatalf("%+v", rider)
|
|
}
|
|
// A channel id that would leave the channels directory is not read.
|
|
if fleet.Name != "Fleet" || fleet.Channel != "" {
|
|
t.Fatalf("%+v", fleet)
|
|
}
|
|
if len(st.Untracked) != 1 || st.Untracked[0] != "~/"+toolboxDir+"/apps/webstorm-old" {
|
|
t.Fatalf("%q", st.Untracked)
|
|
}
|
|
}
|
|
|
|
func TestOnlyToolboxsLauncherIsToolbox(t *testing.T) {
|
|
f := newToolbox(t)
|
|
f.proc(4100, 1000, toolboxComm, []string{"/home/operator/.local/share/JetBrains/Toolbox/bin/jetbrains-toolbox"}, "s.scope")
|
|
// A program whose name the kernel cuts to the same comm.
|
|
f.proc(4200, 1000, toolboxComm, []string{"/usr/lib/mesh/bundles/jetbrains-toolbox-tools"}, "s.scope")
|
|
if r := f.running(); len(r) != 1 || r[0].PID != 4100 {
|
|
t.Fatalf("%+v", r)
|
|
}
|
|
}
|
|
|
|
func TestTheLaunchSwitchIsReadAndAbsentMeansOn(t *testing.T) {
|
|
f := newToolbox(t)
|
|
f.write(tb+"/.settings.json", `{"autostart":false}`)
|
|
s, _ := f.settings()
|
|
if s.LaunchAtLogin || s.LaunchDefault {
|
|
t.Fatalf("%+v", s)
|
|
}
|
|
none := newFake(t)
|
|
if s, err := none.settings(); err != nil || s.Found || !s.LaunchAtLogin {
|
|
t.Fatalf("%+v %v", s, err)
|
|
}
|
|
if st, err := none.Status(); err != nil || st.Installed || len(st.State.Tools) != 0 {
|
|
t.Fatalf("%+v %v", st, err)
|
|
}
|
|
}
|
|
|
|
func TestRestartStartsToolboxMinimisedUnderTheServiceManager(t *testing.T) {
|
|
f := newToolbox(t)
|
|
if _, err := f.Restart(); err == nil || !strings.Contains(err.Error(), "no graphical session") {
|
|
t.Fatalf("without a desktop: %v", err)
|
|
}
|
|
f.desktopSession()
|
|
f.proc(4100, 1000, toolboxComm, []string{"jetbrains-toolbox"}, "session-4.scope")
|
|
f.onStart = func(argv []string) { f.proc(9100, 1000, toolboxComm, argv, "app.slice/"+restartAs+".service") }
|
|
a, err := f.Restart()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(a.Ended) != 1 || a.Ended[0] != 4100 || len(a.Running) != 1 || a.Running[0].PID != 9100 ||
|
|
a.Running[0].Command != "~/"+toolboxDir+"/bin/jetbrains-toolbox --minimize" {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
if !f.called("systemd-run --user --collect --quiet --unit=" + restartAs) {
|
|
t.Fatalf("%q", f.calls)
|
|
}
|
|
gone := newFake(t)
|
|
gone.desktopSession()
|
|
if _, err := gone.Restart(); err == nil || !strings.Contains(err.Error(), "not installed") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckHoldsOneStartInAgreementWithTheSwitch(t *testing.T) {
|
|
f := newToolbox(t)
|
|
f.desktopSession()
|
|
f.proc(4100, 1000, toolboxComm, []string{"jetbrains-toolbox"}, "session-4.scope")
|
|
c, err := f.Check()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
noSecrets(t, c)
|
|
if !c.OK || len(c.Starts) != 1 || len(c.Notes) != 1 {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
// The laptop's way: the switch off, no entry, not running: no finding, a note.
|
|
f.write(tb+"/.settings.json", `{"autostart":false}`)
|
|
removeAll(f, testHome+"/.config/autostart/"+entryName)
|
|
removeAll(f, "/proc/4100")
|
|
if c, _ = f.Check(); !c.OK || len(c.Starts) != 0 || !strings.Contains(strings.Join(c.Notes, " "), "its setting is off") {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
// Everything wrong at once.
|
|
f.write(tb+"/.settings.json", `{}`)
|
|
f.write(testHome+"/.config/i3/config.d/70-ide.conf", "exec --no-startup-id ~/.local/share/JetBrains/Toolbox/bin/jetbrains-toolbox\n")
|
|
removeAll(f, tb+"/apps/rider")
|
|
f.proc(4100, 1000, toolboxComm, []string{"jetbrains-toolbox"}, "s.scope")
|
|
f.proc(4101, 1000, toolboxComm, []string{"jetbrains-toolbox"}, "s.scope")
|
|
c, _ = f.Check()
|
|
all := noSecrets(t, c)
|
|
for _, want := range []string{"does not start with the session", "a second start: ~/.config/i3/config.d/70-ide.conf:1",
|
|
"2 Toolbox processes run", "Rider 2025.3.2 is in Toolbox's list but not on disk"} {
|
|
if !strings.Contains(all, want) {
|
|
t.Errorf("no finding %q in %s", want, all)
|
|
}
|
|
}
|
|
f.write(tb+"/.settings.json", `{"autostart":false}`)
|
|
f.write(testHome+"/.config/autostart/"+entryName, "[Desktop Entry]\nExec=something-else\n")
|
|
f.answer = func(name string, _ []string) Output {
|
|
if name == "dex" {
|
|
return Output{Code: 127, Err: ErrNotInstalled}
|
|
}
|
|
return Output{}
|
|
}
|
|
c, _ = f.Check()
|
|
all = noSecrets(t, c)
|
|
for _, want := range []string{"runs something-else, not Toolbox", "says not to launch at login, but its autostart entry is there", "dex"} {
|
|
if !strings.Contains(all, want) {
|
|
t.Errorf("no finding %q in %s", want, all)
|
|
}
|
|
}
|
|
removeAll(f, tb+"/bin/jetbrains-toolbox")
|
|
if c, _ = f.Check(); c.OK || len(c.Findings) != 1 || !strings.Contains(c.Findings[0].Do, "does not install it") {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
}
|