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

189 lines
4.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}