agent-orchestrator/backend/internal/terminal/fakes_test.go

167 lines
3.2 KiB
Go

package terminal
import (
"context"
"io"
"sync"
"testing"
"time"
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)
// fakeSource is a scripted PTYSource.
type fakeSource struct {
argv []string
mu sync.Mutex
alive bool
aliveErr error
attachErr error
}
func (f *fakeSource) AttachCommand(ports.RuntimeHandle) ([]string, error) {
if f.attachErr != nil {
return nil, f.attachErr
}
if f.argv == nil {
return []string{"zellij", "attach"}, nil
}
return f.argv, nil
}
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()
}
// fakePTY is a scripted ptyProcess: 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).
type fakeSpawner struct {
mu sync.Mutex
ptys []*fakePTY
n int
err error
created []*fakePTY
}
func (f *fakeSpawner) spawn(context.Context, []string) (ptyProcess, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.err != nil {
return nil, f.err
}
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
}
// 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)
}