Files
vmux/client_test.go
Pierre Martin a79a0e154c feat(02-03): client socket, autostart daemon, switch handler, workspace wiring
- 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
2026-03-23 17:51:31 +01:00

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")
}
}