// 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 TestNativePidginTranscriptCapture(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_MESSAGING") != "1" { t.Skip("requires private Xorg/AT-SPI session") } ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() adapter := New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) for _, processName := range []string{"pidgin", "messaging-test"} { t.Run(processName, func(t *testing.T) { command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_messaging.py", processName) 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 output, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", fields[1]).CombinedOutput(); err != nil { t.Fatalf("focus: %s, %v", output, err) } fmt.Fprintln(input, "message\ntyping\ntyping-weight\nedit\nbarrier") messages, typing := 0, 0 var transcript ObjectID retired, barrier := false, false for { select { case event := <-adapter.Events(): if event.Source.ApplicationProcess != processName { continue } if event.ChatMessage != nil { if processName != "pidgin" { t.Fatal("unrelated process received chat metadata") } message := event.ChatMessage transcript = event.Source.ID() // GTK can publish an empty insertion during initial enumeration. if message.Text == "" { continue } if message.Provider != event.Source.BusName || message.Conversation != event.Source.Path || message.Window == "" || message.RoomName == "" { t.Fatalf("incomplete identities: %+v", message) } if message.Typing { typing++ } else { messages++ if message.Text != "Username: Hello\n" { t.Fatalf("message content = %q", message.Text) } } } if event.Type == DefunctStateEvent && event.Detail1 != 0 && transcript.Valid() && event.Source.ID() == transcript { retired = true } if event.Type == TextInsertedEvent && event.Payload == "Messaging barrier" { barrier = true if processName == "pidgin" && (messages != 1 || typing != 2) { t.Fatalf("captured %d messages, %d typing statuses", messages, typing) } if processName == "pidgin" { fmt.Fprintln(input, "destroy") } } if barrier && (processName != "pidgin" || retired) { return } case <-ctx.Done(): t.Fatalf("transcript/barrier missing: %v (messages=%d, typing=%d)", ctx.Err(), messages, typing) } } }) } }