Files
Chase CichorzandClaude Opus 5.5 551ef86d5f Export and display Remote Braille through the reader
A controlled reader renders accepted application frames in a new
virtual-only cthulhu-braille mode at the controller's width and applies
its pan and route gestures; reader UI, flashes and secure frames are
never exported, and hidden frames are unroutable. A controller reader
reports its display width, shows peer cells as an untranslated raw
frame and forwards local pan and routing keys. The physical helper now
reports its width and edge pans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:06:38 -05:00

193 lines
6.6 KiB
Go

package braille
import (
"bytes"
"context"
"errors"
"io"
"strings"
"testing"
"time"
)
type clientBackend struct {
fakeBackend
cancel context.CancelFunc
}
func (backend *clientBackend) Write(text string, cursor int) error {
if err := backend.fakeBackend.Write(text, cursor); err != nil {
return err
}
backend.cancel()
return nil
}
func TestRunClientShowsLiveFrameAndReleasesDisplay(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
backend := &clientBackend{fakeBackend: fakeBackend{geometry: Geometry{20, 1}}, cancel: cancel}
input := strings.NewReader("{\"text\":\"Save, push button\",\"cursor\":-1}\n")
var ready bytes.Buffer
if err := RunClient(ctx, backend, io.NopCloser(input), &ready); err != nil {
t.Fatal(err)
}
if ready.String() != "ready 20\n" || len(backend.writes) != 1 ||
backend.writes[0] != "Save, push button " || backend.opens != 1 || backend.closes != 1 {
t.Fatalf("client lifecycle: ready=%q backend=%+v", ready.String(), backend)
}
}
type flakyOpenBackend struct {
fakeBackend
failures int
cancel context.CancelFunc
}
func (backend *flakyOpenBackend) Open() (Geometry, error) {
if backend.failures > 0 {
backend.failures--
backend.opens++
return Geometry{}, errors.New("display not ready")
}
return backend.fakeBackend.Open()
}
func (backend *flakyOpenBackend) Write(text string, cursor int) error {
if err := backend.fakeBackend.Write(text, cursor); err != nil {
return err
}
backend.cancel()
return nil
}
// BRLTTY refuses or half-answers connection attempts while a display is
// still negotiating (observed live: query failures, then zero geometry,
// then success). The helper must keep trying in-process so the display is
// claimed as soon as BRLTTY is ready, without a supervisor respawn cycle.
func TestRunClientRetriesOpenUntilDisplayReady(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
backend := &flakyOpenBackend{fakeBackend: fakeBackend{geometry: Geometry{20, 1}}, failures: 2, cancel: cancel}
input := strings.NewReader("{\"text\":\"Hello\",\"cursor\":-1}\n")
var ready bytes.Buffer
if err := RunClient(ctx, backend, io.NopCloser(input), &ready); err != nil {
t.Fatal(err)
}
if ready.String() != "ready 20\n" || backend.opens != 3 || backend.closes != 1 || len(backend.writes) != 1 {
t.Fatalf("ready=%q backend=%+v", ready.String(), backend)
}
}
// Cancellation while the display has not appeared yet ends the wait
// cleanly and promptly.
func TestRunClientOpenWaitHonorsCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
backend := &flakyOpenBackend{fakeBackend: fakeBackend{geometry: Geometry{20, 1}}, failures: 1 << 30, cancel: cancel}
var ready bytes.Buffer
done := make(chan error, 1)
go func() { done <- RunClient(ctx, backend, io.NopCloser(strings.NewReader("")), &ready) }()
time.Sleep(100 * time.Millisecond)
cancel()
select {
case err := <-done:
if err != nil {
t.Fatalf("cancelled open wait error = %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("open wait ignored cancellation")
}
if ready.Len() != 0 {
t.Fatalf("ready written without a display: %q", ready.String())
}
}
// A display that never becomes ready surfaces the last open error at the
// give-up bound instead of waiting silently forever.
func TestRunClientOpenWaitGivesUpWithLastError(t *testing.T) {
delay, giveUp := openRetryDelay, openRetryGiveUp
openRetryDelay, openRetryGiveUp = 10*time.Millisecond, 55*time.Millisecond
defer func() { openRetryDelay, openRetryGiveUp = delay, giveUp }()
backend := &flakyOpenBackend{fakeBackend: fakeBackend{geometry: Geometry{20, 1}}, failures: 1 << 30}
err := RunClient(context.Background(), backend, io.NopCloser(strings.NewReader("")), &bytes.Buffer{})
if err == nil || !strings.Contains(err.Error(), "display not ready") {
t.Fatalf("give-up error = %v", err)
}
if backend.opens < 2 {
t.Fatalf("open attempts = %d, want retries", backend.opens)
}
}
func TestRunClientPlacesCursorOnUnicodeCaret(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
backend := &clientBackend{fakeBackend: fakeBackend{geometry: Geometry{4, 1}}, cancel: cancel}
input := strings.NewReader("{\"text\":\"é界abcde\",\"cursor\":5}\n")
if err := RunClient(ctx, backend, io.NopCloser(input), &bytes.Buffer{}); err != nil {
t.Fatal(err)
}
if len(backend.writes) != 1 || backend.writes[0] != "cde " ||
len(backend.cursors) != 1 || backend.cursors[0] != 2 {
t.Fatalf("writes=%q cursors=%v", backend.writes, backend.cursors)
}
}
func TestRunClientRejectsInvalidCursorAndCloses(t *testing.T) {
backend := &fakeBackend{geometry: Geometry{4, 1}}
err := RunClient(context.Background(), backend,
io.NopCloser(strings.NewReader("{\"text\":\"abc\",\"cursor\":4}\n")), &bytes.Buffer{})
if err == nil || backend.opens != 1 || backend.closes != 1 || len(backend.writes) != 0 {
t.Fatalf("error=%v backend=%+v", err, backend)
}
}
func TestOfferLatestFrameReplacesPendingFrame(t *testing.T) {
frames := make(chan Frame, 1)
offerLatestFrame(frames, Frame{Text: "old"})
offerLatestFrame(frames, Frame{Text: "new"})
if frame := <-frames; frame.Text != "new" {
t.Fatalf("retained %q", frame.Text)
}
offerLatestFrame(frames, Frame{Text: "newer"})
if frame := <-frames; frame.Text != "newer" {
t.Fatalf("lost empty-queue update: %q", frame.Text)
}
}
type routingBackend struct{ fakeBackend }
func (b *routingBackend) ReadKey() (Key, error) {
if len(b.writes) == 0 {
return NoKey, nil
}
return b.fakeBackend.ReadKey()
}
func TestRunClientRoutesDisplayedFrameAfterPanningAndJoinsReader(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
input, writer := io.Pipe()
defer writer.Close()
backend := &routingBackend{fakeBackend{geometry: Geometry{4, 1}, keys: []Key{PanRight, RouteBase + 1}, cancel: cancel}}
var output bytes.Buffer
done := make(chan error, 1)
go func() { done <- RunClient(ctx, backend, input, &output) }()
if _, err := io.WriteString(writer, "{\"text\":\"é猫abcdef\",\"cursor\":-1,\"id\":42}\n"); err != nil {
t.Fatal(err)
}
if err := <-done; err != nil {
t.Fatal(err)
}
if output.String() != "ready 4\n{\"id\":42,\"offset\":5}\n" {
t.Fatalf("helper output %q", output.String())
}
if backend.closes != 1 {
t.Fatalf("close count %d", backend.closes)
}
// The writer has deliberately remained open; return requires the session to
// close the blocked input reader and join it.
if _, err := writer.Write([]byte("x")); err == nil {
t.Fatal("reader still open")
}
}