// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package i3ipc import ( "context" "net" "path/filepath" "testing" "time" ) func listener(t *testing.T) net.Listener { t.Helper() l, err := net.Listen("unix", filepath.Join(t.TempDir(), "ipc")) if err != nil { t.Fatal(err) } t.Cleanup(func() { l.Close() }) return l } func handshake(c net.Conn, initial string) error { for _, response := range []string{`{"success":true}`, `[{"id":2,"name":"A","focused":true}]`, `{"id":1,"type":"root","nodes":[]}`} { m, err := readMessage(c) if err != nil { return err } if initial != "" { if err := writeMessage(c, modeEvent, []byte(initial)); err != nil { return err } } if err := writeMessage(c, m.kind, []byte(response)); err != nil { return err } } return nil } func TestSessionBaselineCancellationAndReconnect(t *testing.T) { l := listener(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() events := make(chan Event, 8) done := make(chan struct{}) server := make(chan error, 1) go func() { defer close(done) Run(ctx, func() string { return l.Addr().String() }, func(e Event) { events <- e }) }() go func() { for _, name := range []string{"personal", "default"} { c, err := l.Accept() if err != nil { server <- err return } if err = handshake(c, `{"change":"snapshot"}`); err == nil { err = writeMessage(c, modeEvent, []byte(`{"change":"`+name+`"}`)) } if name == "default" && err == nil { var b [1]byte _, _ = c.Read(b[:]) } c.Close() if err != nil { server <- err return } } server <- nil }() for _, name := range []string{"personal", "default"} { select { case e := <-events: if e.Name != name { t.Fatal(e) } case <-time.After(4 * time.Second): t.Fatal("missing event/reconnect") } } cancel() select { case <-done: case <-time.After(time.Second): t.Fatal("idle read did not cancel") } if err := <-server; err != nil { t.Fatal(err) } select { case e := <-events: t.Fatalf("snapshot announced: %+v", e) default: } } func TestSubscriptionFailure(t *testing.T) { l := listener(t) go func() { c, err := l.Accept() if err != nil { return } defer c.Close() m, err := readMessage(c) if err == nil { _ = writeMessage(c, m.kind, []byte(`{"success":false}`)) } }() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := session(ctx, l.Addr().String(), func(Event) { t.Error("unexpected event") }); err == nil { t.Fatal("accepted rejection") } } func TestUnavailableAndCanceledQuery(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() if err := session(ctx, "/nonexistent/i3", func(Event) {}); err == nil { t.Fatal("unavailable socket accepted") } l := listener(t) accepted := make(chan struct{}) go func() { c, err := l.Accept() if err != nil { return } defer c.Close() close(accepted) var b [4096]byte for { if _, err := c.Read(b[:]); err != nil { return } } }() ctx, cancel = context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { done <- session(ctx, l.Addr().String(), func(Event) {}) }() <-accepted cancel() select { case <-done: case <-time.After(time.Second): t.Fatal("query did not cancel") } } func TestInterleavedEventsAndBacklog(t *testing.T) { a, b := net.Pipe() defer a.Close() defer b.Close() c := connection{Conn: a} go func() { m, err := readMessage(b) if err != nil { return } _ = writeMessage(b, modeEvent, []byte(`{"change":"panel"}`)) _ = writeMessage(b, m.kind, []byte(`{"success":true}`)) }() var result struct{ Success bool } if err := c.request(subscribe, nil, &result); err != nil || !result.Success || len(c.pending) != 1 { t.Fatalf("%+v %v", c.pending, err) } c.pending = make([]message, maxPending) go func() { _, err := readMessage(b) if err == nil { _ = writeMessage(b, modeEvent, []byte(`{}`)) } }() if err := c.request(tree, nil, &result); err == nil { t.Fatal("unbounded pending events") } }