// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "bufio" "context" "fmt" "os" "os/exec" "strings" "testing" "time" ) func TestNativeMumbleTranscriptCapture(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_MUMBLE") != "1" { t.Skip("requires private Qt/Xorg/AT-SPI session") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() adapter := New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) for _, process := range []string{"mumble", "messaging-test"} { t.Run(process, func(t *testing.T) { command := exec.CommandContext(ctx, "python3", "../../tests/qt_messaging.py", process) input, err := command.StdinPipe() if err != nil { t.Fatal(err) } output, err := command.StdoutPipe() if err != nil { t.Fatal(err) } command.Stderr = os.Stderr if err := command.Start(); err != nil { t.Fatal(err) } defer func() { input.Close(); command.Process.Kill(); command.Wait() }() line, err := bufio.NewReader(output).ReadString('\n') fields := strings.Fields(line) if err != nil || len(fields) != 2 || fields[0] != "ready" { t.Fatalf("fixture readiness: %q, %v", line, err) } if _, err := adapter.CaptureDesktop(ctx); err != nil { t.Fatal(err) } if out, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", fields[1]).CombinedOutput(); err != nil { t.Fatalf("focus: %s, %v", out, err) } fmt.Fprintln(input, "messages\nbarrier") var messages []string for { select { case event := <-adapter.Events(): if event.Source.ApplicationProcess != process { continue } if message := event.ChatMessage; message != nil { if process != "mumble" { t.Fatal("unrelated process received chat metadata") } if message.Provider != event.Source.BusName || message.Conversation != event.Source.Path || message.Window == "" || message.RoomName != "Mumble" { t.Fatalf("incomplete message identities: %+v", message) } if message.Text != "" { messages = append(messages, message.Text) } } if event.Type == TextInsertedEvent && event.Payload == "Messaging barrier" { want := "Username: hello world 界|Repeated message|Repeated message" if process == "mumble" && strings.Join(messages, "|") != want { t.Fatalf("captured messages = %q", messages) } return } case <-ctx.Done(): t.Fatalf("transcript/barrier missing: %v (messages=%q)", ctx.Err(), messages) } } }) } } func TestNativeMumbleTableRows(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_MUMBLE") != "1" { t.Skip("requires private Qt/Xorg/AT-SPI session") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() adapter := New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) for _, process := range []string{"mumble", "table-test"} { t.Run(process, func(t *testing.T) { command := exec.CommandContext(ctx, "python3", "../../tests/qt_mumble_table.py", process) output, err := command.StdoutPipe() if err != nil { t.Fatal(err) } command.Stderr = os.Stderr if err := command.Start(); err != nil { t.Fatal(err) } defer func() { command.Process.Kill(); command.Wait() }() line, err := bufio.NewReader(output).ReadString('\n') fields := strings.Fields(line) if err != nil || len(fields) != 2 || fields[0] != "ready" { t.Fatalf("fixture readiness: %q, %v", line, err) } if _, err := adapter.CaptureDesktop(ctx); err != nil { t.Fatal(err) } if out, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", fields[1]).CombinedOutput(); err != nil { t.Fatalf("focus: %s, %v", out, err) } for rowIndex, label := range []string{"Server Alpha", "Server Beta", "Server Gamma, 界"} { key := "Down" if rowIndex == 0 { key = "Home" } if out, err := exec.CommandContext(ctx, "xdotool", "key", key).CombinedOutput(); err != nil { t.Fatalf("select row: %s, %v", out, err) } selected: for { select { case <-ctx.Done(): t.Fatalf("row %d (%s) missing: %v", rowIndex, label, ctx.Err()) case event := <-adapter.Events(): cell := event.Source if cell.ApplicationProcess != process || cell.Name != label || cell.TableColumnCount != 3 { continue } want := strings.Join([]string{label, label, label}, ", ") if process == "mumble" { want = label } if rowIndex == 1 { want = label + ", 12, 12" } if cell.TableRowText != want || !cell.GUITable || cell.TableRowIndex != rowIndex { t.Fatalf("row %d context = %+v, want %q", rowIndex, cell, want) } break selected } } } }) } }