package app import ( "bytes" "context" "encoding/json" "io" "sync" "testing" "time" "github.com/hjbdev/patterm/internal/protocol" ) func TestNetClientFrameLoopSendsFocusedInput(t *testing.T) { clientT, daemonT := protocol.NewLoopbackPair() inR, inW := ioPipe(t) out := &lockedBuffer{} gotInput := make(chan protocol.Input, 1) errCh := make(chan error, 1) go func() { f, err := daemonT.Recv() if err != nil { errCh <- err return } if f.Type != protocol.FrameAttach { t.Errorf("first frame = %s, want attach", f.Type) errCh <- nil return } sendTestFrame(t, daemonT, protocol.FrameHello, protocol.Hello{Version: 1, ClientID: "test", ProjectKey: "project"}) sendTestFrame(t, daemonT, protocol.FrameProjectList, protocol.ProjectList{}) model := chromeModel{ ProjectKey: "project", FocusedID: "p1", Processes: []childModel{{ID: "p1", Name: "shell", Kind: string(KindCommand), Status: string(StatusRunning)}}, Sidebar: []navEntryModel{{ChildID: "p1"}}, } sendTestFrame(t, daemonT, protocol.FrameChrome, protocol.Chrome{ProjectKey: "project", Model: mustMarshalTest(t, model)}) sendTestFrame(t, daemonT, protocol.FramePaneSnapshot, protocol.PaneSnapshot{PaneID: "p1", Bytes: []byte("READY")}) for { f, err := daemonT.Recv() if err != nil { errCh <- err return } if f.Type != protocol.FrameInput { continue } input, err := protocol.Decode[protocol.Input](f) if err != nil { errCh <- err return } gotInput <- input _ = daemonT.Close() errCh <- nil return } }() ctx, cancel := context.WithCancel(context.Background()) defer cancel() runCh := make(chan error, 1) go func() { runCh <- RunAttachedClient(ctx, ClientOptions{ Transport: clientT, Stdin: inR, Stdout: out, Cols: 80, Rows: 24, }) }() deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) && !bytes.Contains(out.Bytes(), []byte("READY")) { time.Sleep(10 * time.Millisecond) } if !bytes.Contains(out.Bytes(), []byte("READY")) { t.Fatalf("snapshot was not rendered before input; output=%q", out.String()) } if _, err := inW.Write([]byte("echo hi\r")); err != nil { t.Fatalf("write stdin: %v", err) } select { case input := <-gotInput: if input.PaneID != "p1" || string(input.Bytes) != "echo hi\r" { t.Fatalf("input = %#v", input) } case <-time.After(3 * time.Second): t.Fatalf("client did not forward input") } cancel() _ = inW.Close() select { case err := <-runCh: if err != nil { t.Fatalf("client run: %v", err) } case <-time.After(3 * time.Second): t.Fatalf("client did not stop") } if err := <-errCh; err != nil && err != protocol.ErrTransportClosed { t.Fatalf("daemon side: %v", err) } } type lockedBuffer struct { mu sync.Mutex b bytes.Buffer } func (b *lockedBuffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() return b.b.Write(p) } func (b *lockedBuffer) Bytes() []byte { b.mu.Lock() defer b.mu.Unlock() return append([]byte(nil), b.b.Bytes()...) } func (b *lockedBuffer) String() string { b.mu.Lock() defer b.mu.Unlock() return b.b.String() } func ioPipe(t *testing.T) (*io.PipeReader, *io.PipeWriter) { t.Helper() r, w := io.Pipe() return r, w } func sendTestFrame[T any](t *testing.T, tr protocol.Transport, typ protocol.FrameType, payload T) { t.Helper() f, err := protocol.NewFrame(typ, payload) if err != nil { t.Fatalf("frame %s: %v", typ, err) } if err := tr.Send(f); err != nil { t.Fatalf("send %s: %v", typ, err) } } func mustMarshalTest(t *testing.T, v any) []byte { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } return b }