- Client struct sends JSON requests to daemon over Unix socket - EnsureDaemon auto-starts the daemon if not running (retry 50ms x 20) - Switch handler uses FuzzyMatch + SwitchToWorkspace via i3 IPC - InitWorkspaceResolver wires BuildTerminalWorkspaceMap + ResolveWorkspace - sysattr_linux.go for Setsid detach on daemon spawn
208 lines
4.7 KiB
Go
208 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
i3 "go.i3wm.org/i3/v4"
|
|
)
|
|
|
|
func TestClientSendReceive(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
if err := d.Start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
defer d.Stop()
|
|
|
|
// Populate a session
|
|
d.registry.Update(SessionInfo{
|
|
PID: 42,
|
|
SessionID: "test-sess",
|
|
State: "Working",
|
|
Cwd: "/tmp/test",
|
|
})
|
|
|
|
client := NewClient(d.sockPath)
|
|
resp, err := client.Send(Request{Action: "list"})
|
|
if err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
if !resp.OK {
|
|
t.Fatalf("resp.OK = false, error = %q", resp.Error)
|
|
}
|
|
if len(resp.Sessions) != 1 {
|
|
t.Fatalf("sessions len = %d, want 1", len(resp.Sessions))
|
|
}
|
|
if resp.Sessions[0].SessionID != "test-sess" {
|
|
t.Errorf("session_id = %q, want %q", resp.Sessions[0].SessionID, "test-sess")
|
|
}
|
|
}
|
|
|
|
func TestEnsureDaemonAlreadyRunning(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
if err := d.Start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
defer d.Stop()
|
|
|
|
// EnsureDaemon should return nil immediately since daemon is already listening
|
|
err := EnsureDaemon(d.sockPath)
|
|
if err != nil {
|
|
t.Errorf("EnsureDaemon should succeed when daemon is running: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDaemonNoSocket(t *testing.T) {
|
|
dir := t.TempDir()
|
|
sockPath := filepath.Join(dir, "nonexistent.sock")
|
|
|
|
// EnsureDaemon with no real daemon and a fake executable will fail,
|
|
// but we can verify it doesn't panic and returns an error
|
|
err := EnsureDaemon(sockPath)
|
|
if err == nil {
|
|
t.Error("EnsureDaemon should fail when no daemon can be started")
|
|
}
|
|
}
|
|
|
|
func TestClientSendSwitchNoMatch(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
if err := d.Start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
defer d.Stop()
|
|
|
|
client := NewClient(d.sockPath)
|
|
args := mustMarshalJSON(t, SwitchArgs{Query: "nonexistent"})
|
|
resp, err := client.Send(Request{Action: "switch", Args: args})
|
|
if err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
if resp.OK {
|
|
t.Error("expected OK=false for no matching session")
|
|
}
|
|
if resp.Error == "" {
|
|
t.Error("expected error message")
|
|
}
|
|
}
|
|
|
|
func TestClientSendSwitchNoWorkspace(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
if err := d.Start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
defer d.Stop()
|
|
|
|
d.registry.Update(SessionInfo{
|
|
PID: 42,
|
|
SessionID: "sess-1",
|
|
State: "Working",
|
|
Cwd: "/tmp/test",
|
|
GitBranch: "feat-auth",
|
|
Workspace: "", // no workspace
|
|
})
|
|
|
|
client := NewClient(d.sockPath)
|
|
args := mustMarshalJSON(t, SwitchArgs{Query: "auth"})
|
|
resp, err := client.Send(Request{Action: "switch", Args: args})
|
|
if err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
if resp.OK {
|
|
t.Error("expected OK=false when session has no workspace")
|
|
}
|
|
}
|
|
|
|
func TestClientConnectionRefused(t *testing.T) {
|
|
dir := t.TempDir()
|
|
sockPath := filepath.Join(dir, "dead.sock")
|
|
|
|
client := NewClient(sockPath)
|
|
_, err := client.Send(Request{Action: "list"})
|
|
if err == nil {
|
|
t.Error("expected error when daemon is not running")
|
|
}
|
|
}
|
|
|
|
func TestClientSendSwitchWithMockI3(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
d.i3commander = &mockI3Commander{results: []i3.CommandResult{{Success: true}}}
|
|
|
|
if err := d.Start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
defer d.Stop()
|
|
|
|
d.registry.Update(SessionInfo{
|
|
PID: 42,
|
|
SessionID: "sess-1",
|
|
State: "Working",
|
|
Cwd: "/tmp/vmux-project",
|
|
GitBranch: "feat-auth",
|
|
Workspace: "3",
|
|
})
|
|
|
|
client := NewClient(d.sockPath)
|
|
args := mustMarshalJSON(t, SwitchArgs{Query: "auth"})
|
|
resp, err := client.Send(Request{Action: "switch", Args: args})
|
|
if err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
if !resp.OK {
|
|
t.Errorf("expected OK=true, got error: %q", resp.Error)
|
|
}
|
|
|
|
// Verify workspace was switched (SwitchToWorkspace sends "workspace number 3")
|
|
mock := d.i3commander.(*mockI3Commander)
|
|
want := "workspace number 3"
|
|
if mock.lastCommand != want {
|
|
t.Errorf("lastCommand = %q, want %q", mock.lastCommand, want)
|
|
}
|
|
}
|
|
|
|
// mockI3Commander is defined in i3bridge_test.go (same package).
|
|
|
|
func mustMarshalJSON(t *testing.T, v any) []byte {
|
|
t.Helper()
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// Verify Client respects timeouts on dead connections
|
|
func TestClientTimeout(t *testing.T) {
|
|
dir := t.TempDir()
|
|
sockPath := filepath.Join(dir, "slow.sock")
|
|
|
|
// Create a listener that accepts but never responds
|
|
ln, err := net.Listen("unix", sockPath)
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
go func() {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Hold the connection open without responding
|
|
time.Sleep(10 * time.Second)
|
|
conn.Close()
|
|
}()
|
|
|
|
client := NewClient(sockPath)
|
|
_, err = client.Send(Request{Action: "list"})
|
|
if err == nil {
|
|
t.Error("expected timeout error")
|
|
}
|
|
}
|