package terminal import ( "context" "io" "sync" "testing" "time" "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) // fakeSource is a scripted terminal Source: Attach hands out fake Streams from // an embedded spawner (or a custom attachFn closure); IsAlive is scriptable. // attachErr makes Attach fail. type fakeSource struct { spawner *fakeSpawner attachFn func(ctx context.Context, rows, cols uint16) (ports.Stream, error) mu sync.Mutex alive bool aliveErr error attachErr error } func (f *fakeSource) Attach(ctx context.Context, _ ports.RuntimeHandle, rows, cols uint16) (ports.Stream, error) { if f.attachErr != nil { return nil, f.attachErr } if f.attachFn != nil { return f.attachFn(ctx, rows, cols) } if f.spawner == nil { f.spawner = &fakeSpawner{} } return f.spawner.spawn(rows, cols) } func (f *fakeSource) IsAlive(context.Context, ports.RuntimeHandle) (bool, error) { f.mu.Lock() defer f.mu.Unlock() return f.alive, f.aliveErr } func (f *fakeSource) setAlive(v bool) { f.mu.Lock() f.alive = v f.mu.Unlock() } func (f *fakeSource) setAliveResult(v bool, err error) { f.mu.Lock() f.alive = v f.aliveErr = err f.mu.Unlock() } // fakePTY is a scripted ports.Stream: Read drains the out channel, Write // records, Resize records, and Close unblocks reads. type fakePTY struct { out chan []byte closed chan struct{} once sync.Once mu sync.Mutex written []byte resizes [][2]uint16 } func newFakePTY() *fakePTY { return &fakePTY{out: make(chan []byte, 64), closed: make(chan struct{})} } func (p *fakePTY) push(b []byte) { p.out <- b } func (p *fakePTY) Read(b []byte) (int, error) { select { case chunk := <-p.out: return copy(b, chunk), nil case <-p.closed: return 0, io.EOF } } func (p *fakePTY) Write(b []byte) (int, error) { p.mu.Lock() defer p.mu.Unlock() p.written = append(p.written, b...) return len(b), nil } func (p *fakePTY) Resize(rows, cols uint16) error { p.mu.Lock() defer p.mu.Unlock() p.resizes = append(p.resizes, [2]uint16{rows, cols}) return nil } func (p *fakePTY) Close() error { p.once.Do(func() { close(p.closed) }) return nil } func (p *fakePTY) writtenBytes() []byte { p.mu.Lock() defer p.mu.Unlock() out := make([]byte, len(p.written)) copy(out, p.written) return out } func (p *fakePTY) resizeCalls() [][2]uint16 { p.mu.Lock() defer p.mu.Unlock() return append([][2]uint16(nil), p.resizes...) } // fakeSpawner hands out pre-built fakePTYs in order; once exhausted it returns // idle PTYs that block until closed (so a re-attach loop does not busy-spin). // It is the attach seam the fakeSource backs: each Attach call is one spawn. type fakeSpawner struct { mu sync.Mutex ptys []*fakePTY n int err error created []*fakePTY sizes [][2]uint16 // rows×cols passed to each attach call, in order } func (f *fakeSpawner) spawn(rows, cols uint16) (ports.Stream, error) { f.mu.Lock() defer f.mu.Unlock() if f.err != nil { return nil, f.err } f.sizes = append(f.sizes, [2]uint16{rows, cols}) var p *fakePTY if f.n < len(f.ptys) { p = f.ptys[f.n] } else { p = newFakePTY() } f.n++ f.created = append(f.created, p) return p, nil } func (f *fakeSpawner) calls() int { f.mu.Lock() defer f.mu.Unlock() return f.n } func (f *fakeSpawner) spawnSizes() [][2]uint16 { f.mu.Lock() defer f.mu.Unlock() return append([][2]uint16(nil), f.sizes...) } // eventually polls cond until true or the deadline, failing the test otherwise. func eventually(t *testing.T, d time.Duration, cond func() bool) { t.Helper() deadline := time.Now().Add(d) for time.Now().Before(deadline) { if cond() { return } time.Sleep(2 * time.Millisecond) } t.Fatal("condition not met within " + d.String()) } // safeBytes is a concurrency-safe byte accumulator for subscriber callbacks. type safeBytes struct { mu sync.Mutex b []byte } func (s *safeBytes) add(p []byte) { s.mu.Lock() s.b = append(s.b, p...) s.mu.Unlock() } func (s *safeBytes) string() string { s.mu.Lock() defer s.mu.Unlock() return string(s.b) }