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