feat(cybersecurity): auto-commit con 48 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 23:44:39 +02:00
parent efc9911925
commit 729921e16e
48 changed files with 3765 additions and 8 deletions
+149
View File
@@ -0,0 +1,149 @@
package infra
import (
"bytes"
"context"
"fmt"
"os/exec"
"sync"
"syscall"
"time"
"github.com/creack/pty"
)
// PTYCaptureIdle launches a command inside a pseudo-terminal (PTY) and captures
// all output until the terminal has been idle for at least idle duration, or
// maxDur has elapsed. Before sending inputs it waits warmup to let the process
// initialize. Between each input step it waits stepDelay.
//
// The returned string is the raw PTY output, ANSI escape sequences included.
// To turn it into plain text: use vt_render_go_tui to reconstruct the 2D screen
// layout for TUIs with absolute cursor positioning (claude, htop), or
// strip_ansi_go_core for sequential, log-like output.
func PTYCaptureIdle(
ctx context.Context,
name string,
args []string,
warmup time.Duration,
inputs []string,
stepDelay time.Duration,
idle time.Duration,
maxDur time.Duration,
) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
ptmx, err := pty.Start(cmd)
if err != nil {
return "", fmt.Errorf("pty_capture_idle: pty.Start: %w", err)
}
// Set a reasonable terminal size so TUIs render without truncating.
if szErr := pty.Setsize(ptmx, &pty.Winsize{Rows: 40, Cols: 120}); szErr != nil {
// Non-fatal: continue even if resize fails.
_ = szErr
}
var (
mu sync.Mutex
buf bytes.Buffer
lastByte = time.Now()
)
// Reader goroutine: copy PTY output into buf and track last-byte time.
readDone := make(chan struct{})
go func() {
defer close(readDone)
tmp := make([]byte, 4096)
for {
n, rerr := ptmx.Read(tmp)
if n > 0 {
mu.Lock()
buf.Write(tmp[:n])
lastByte = time.Now()
mu.Unlock()
}
if rerr != nil {
// EIO/EOF is normal on Linux when the PTY master is closed
// after the child exits. Not a real error.
return
}
}
}()
start := time.Now()
// Wait for warmup so the TUI/CLI has time to initialize.
select {
case <-time.After(warmup):
case <-ctx.Done():
_ = ptmx.Close()
<-readDone
mu.Lock()
out := buf.String()
mu.Unlock()
return out, fmt.Errorf("pty_capture_idle: context cancelled during warmup: %w", ctx.Err())
}
// Send inputs one by one with stepDelay between them.
for _, in := range inputs {
if _, werr := ptmx.Write([]byte(in)); werr != nil {
// PTY may have closed already; stop sending.
break
}
select {
case <-time.After(stepDelay):
case <-ctx.Done():
goto done
}
}
done:
// Poll until idle or maxDur.
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
mu.Lock()
sinceLastByte := time.Since(lastByte)
mu.Unlock()
elapsed := time.Since(start)
if sinceLastByte >= idle || elapsed >= maxDur {
goto shutdown
}
case <-ctx.Done():
goto shutdown
}
}
shutdown:
// Close the PTY master. This signals EOF to the reader goroutine.
_ = ptmx.Close()
// Graceful shutdown: SIGTERM first, then SIGKILL after 2s.
if cmd.Process != nil {
_ = cmd.Process.Signal(syscall.SIGTERM)
killTimer := time.NewTimer(2 * time.Second)
waitCh := make(chan error, 1)
go func() { waitCh <- cmd.Wait() }()
select {
case <-waitCh:
// Process exited cleanly.
case <-killTimer.C:
_ = cmd.Process.Kill()
<-waitCh
}
killTimer.Stop()
}
<-readDone
mu.Lock()
out := buf.String()
mu.Unlock()
return out, nil
}
+83
View File
@@ -0,0 +1,83 @@
---
name: pty_capture_idle
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func PTYCaptureIdle(ctx context.Context, name string, args []string, warmup time.Duration, inputs []string, stepDelay, idle, maxDur time.Duration) (string, error)"
description: "Lanza un comando dentro de un pseudo-terminal (PTY) en memoria y captura todo su output hasta que el terminal permanece idle durante al menos `idle`, o se alcanza `maxDur`. Soporta envío de inputs interactivos tras el warmup inicial. Devuelve el output RAW con secuencias ANSI intactas."
tags: ["terminal", "pty", "tui", "capture", "automation", "terminal-capture"]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
- "context"
- "time"
- "github.com/creack/pty"
tested: true
tests:
- "captura output de echo hola"
- "input interactivo con cat"
- "timeout duro con sleep 10"
test_file_path: "functions/infra/pty_capture_idle_test.go"
file_path: "functions/infra/pty_capture_idle.go"
params:
- name: ctx
desc: "Contexto de cancelación. Si se cancela, la función aborta la captura y retorna el output acumulado hasta ese momento."
- name: name
desc: "Nombre o path del ejecutable a lanzar (debe existir en PATH o ser un path absoluto)."
- name: args
desc: "Argumentos posicionales para el ejecutable. Puede ser nil o vacío."
- name: warmup
desc: "Tiempo que la función espera después de arrancar el proceso antes de enviar inputs. Permite que la TUI inicialice su render. Típico: 500ms2s para CLIs lentas."
- name: inputs
desc: "Lista de strings a escribir al PTY en secuencia, uno por vez. Incluir '\\r' al final de cada string para simular Enter. Puede ser nil si solo se quiere observar la salida sin interactuar."
- name: stepDelay
desc: "Espera entre cada input enviado. Permite que la TUI procese y renderice la respuesta de cada paso antes de enviar el siguiente."
- name: idle
desc: "Tiempo sin nuevos bytes en el PTY que se considera 'respuesta terminada'. Cuando el terminal lleva idle sin actividad, la función retorna. Típico: 500ms2s."
- name: maxDur
desc: "Timeout duro desde el inicio de la función. Garantiza que la función retorna aunque la TUI siga emitiendo output indefinidamente (spinners, relojes). Típico: 30s120s."
output: "String con el output completo del terminal desde el arranque hasta la captura, incluyendo secuencias de escape ANSI. Vacío string si el proceso no produjo nada. Error si el PTY no pudo arrancar o el contexto fue cancelado durante warmup."
---
## Ejemplo
```go
// Capturar una sesión de claude con un prompt automático
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
raw, err := PTYCaptureIdle(
ctx,
"claude", nil,
2*time.Second, // warmup: esperar que claude cargue
[]string{"hola, responde PONG\r"}, // inputs: enviar mensaje + Enter
300*time.Millisecond, // stepDelay entre inputs
2*time.Second, // idle: cortar cuando lleve 2s sin output
120*time.Second, // maxDur: timeout duro
)
if err != nil {
log.Fatal(err)
}
// raw contiene el render completo con ANSI; limpiar antes de procesar texto:
// clean := StripANSI(raw) // strip_ansi_go_tui
fmt.Println(raw)
```
## Cuando usarla
Cuando necesites automatizar o scriptear una CLI interactiva que solo entra en modo interactivo si detecta un TTY real (como `claude`, `vim`, `fzf`, `htop`, `python` REPL, `psql`). El PTY hace creer al proceso que habla con un terminal real, sin abrir ninguna ventana gráfica.
## Gotchas
- **Linux/Unix only.** Usa PTY POSIX (`creack/pty`). No funciona en Windows.
- **Output RAW con ANSI.** El string devuelto contiene secuencias de escape (`\x1b[...m`, cursor moves, etc.). Para convertirlo a texto plano: usa `vt_render_go_tui` (reconstruye el layout 2D — correcto para TUIs con posicionamiento absoluto como `claude` o `htop`) o `strip_ansi_go_core` (para output secuencial tipo log). `strip_ansi` sobre una TUI con layout absoluto deja las palabras pegadas porque los espacios entre columnas eran movimientos de cursor.
- **Idle es heurístico.** Si la TUI hace render periódico (spinners, relojes en pantalla, progress bars continuas), el idle nunca se dispara y la función esperará hasta `maxDur`. Aumentar `maxDur` o matar el spinner antes de capturar.
- **El binario debe existir en PATH** (o usar path absoluto en `name`). La función devuelve error si `pty.Start` falla.
- **EIO/EOF al cerrar PTY es normal en Linux.** El goroutine lector lo absorbe silenciosamente; no se propaga como error.
- **SIGTERM → SIGKILL.** Al terminar la captura, la función envía SIGTERM al proceso y espera 2s antes de SIGKILL. Procesos que ignoran SIGTERM (como `sleep`) se matan limpiamente.
- **Tamaño de terminal fijado a 40×120.** Suficiente para la mayoría de TUIs. Si el render se ve truncado, el llamador puede hacer `pty.Setsize` adicional después de obtener el ptmx (no expuesto por esta función; para casos avanzados, reimplementar con acceso directo al ptmx).
+74
View File
@@ -0,0 +1,74 @@
package infra
import (
"context"
"strings"
"testing"
"time"
)
func TestPTYCaptureIdle(t *testing.T) {
t.Run("captura output de echo hola", func(t *testing.T) {
if testing.Short() {
t.Skip("skip en modo corto")
}
ctx := context.Background()
out, err := PTYCaptureIdle(ctx, "echo", []string{"hola"}, 100*time.Millisecond, nil, 0, 300*time.Millisecond, 5*time.Second)
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if !strings.Contains(out, "hola") {
t.Errorf("se esperaba 'hola' en el output, got: %q", out)
}
})
t.Run("input interactivo con cat", func(t *testing.T) {
if testing.Short() {
t.Skip("skip en modo corto: timing sensible en CI")
}
ctx := context.Background()
// cat repite stdin a stdout via PTY; el PTY hace echo del input ademas.
// "ping\r" simula Enter; la palabra "ping" debe aparecer en el output.
out, err := PTYCaptureIdle(
ctx,
"cat", nil,
200*time.Millisecond,
[]string{"ping\r"},
100*time.Millisecond,
500*time.Millisecond,
5*time.Second,
)
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if !strings.Contains(out, "ping") {
t.Errorf("se esperaba 'ping' en el output, got: %q", out)
}
})
t.Run("timeout duro con sleep 10", func(t *testing.T) {
if testing.Short() {
t.Skip("skip en modo corto: espera ~1s de timeout")
}
ctx := context.Background()
start := time.Now()
_, err := PTYCaptureIdle(
ctx,
"sleep", []string{"10"},
50*time.Millisecond,
nil,
0,
600*time.Millisecond,
1*time.Second,
)
elapsed := time.Since(start)
if err != nil {
// Un error de señal/exit es esperado; no falla el test.
t.Logf("error (esperado al matar sleep): %v", err)
}
// La función debe retornar en menos de 3s, no esperar los 10s del sleep.
if elapsed >= 3*time.Second {
t.Errorf("la función tardó %v, se esperaba < 3s", elapsed)
}
})
}
+176
View File
@@ -0,0 +1,176 @@
package infra
import (
"bytes"
"context"
"fmt"
"os/exec"
"sync"
"syscall"
"time"
"github.com/creack/pty"
)
// PTYCaptureStream launches a command inside a pseudo-terminal (PTY) and
// streams periodic snapshots of the accumulated output through a channel.
// Unlike PTYCaptureIdle, which returns the full output at the end,
// PTYCaptureStream emits the ENTIRE buffer accumulated so far on every
// snapshotInterval tick — allowing callers to observe the terminal render
// while the process is still running.
//
// The returned channel is closed when capture ends (idle/maxDur/ctx cancel).
// The last value sent before closing is always a final snapshot of the
// complete buffer, regardless of tick alignment.
//
// Callers MUST drain the channel or cancel ctx to avoid blocking the
// internal goroutine. Error is returned only if pty.Start fails.
func PTYCaptureStream(
ctx context.Context,
name string,
args []string,
warmup time.Duration,
inputs []string,
stepDelay time.Duration,
snapshotInterval time.Duration,
idle time.Duration,
maxDur time.Duration,
) (<-chan string, error) {
cmd := exec.CommandContext(ctx, name, args...)
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, fmt.Errorf("pty_capture_stream: pty.Start: %w", err)
}
// Set a reasonable terminal size so TUIs render without truncating.
if szErr := pty.Setsize(ptmx, &pty.Winsize{Rows: 40, Cols: 120}); szErr != nil {
// Non-fatal: continue even if resize fails.
_ = szErr
}
var (
mu sync.Mutex
buf bytes.Buffer
lastByte = time.Now()
)
// Reader goroutine: copy PTY output into buf and track last-byte time.
readDone := make(chan struct{})
go func() {
defer close(readDone)
tmp := make([]byte, 4096)
for {
n, rerr := ptmx.Read(tmp)
if n > 0 {
mu.Lock()
buf.Write(tmp[:n])
lastByte = time.Now()
mu.Unlock()
}
if rerr != nil {
// EIO/EOF is normal on Linux when the PTY master is closed
// after the child exits. Not a real error.
return
}
}
}()
ch := make(chan string, 16)
// snapshot returns a copy of the current buffer contents.
snapshot := func() string {
mu.Lock()
s := buf.String()
mu.Unlock()
return s
}
// send emits a snapshot to ch, respecting ctx cancellation.
send := func(s string) bool {
select {
case ch <- s:
return true
case <-ctx.Done():
return false
}
}
// Conducting goroutine: handles warmup, inputs, periodic snapshots,
// idle/maxDur detection, and shutdown.
go func() {
defer func() {
// Shutdown: close PTY master, SIGTERM → SIGKILL, wait reader.
_ = ptmx.Close()
if cmd.Process != nil {
_ = cmd.Process.Signal(syscall.SIGTERM)
killTimer := time.NewTimer(2 * time.Second)
waitCh := make(chan error, 1)
go func() { waitCh <- cmd.Wait() }()
select {
case <-waitCh:
// Process exited cleanly.
case <-killTimer.C:
_ = cmd.Process.Kill()
<-waitCh
}
killTimer.Stop()
}
<-readDone
// Final snapshot — always emitted so consumers get the complete state.
send(snapshot())
close(ch)
}()
start := time.Now()
// Wait for warmup so the TUI/CLI has time to initialize.
select {
case <-time.After(warmup):
case <-ctx.Done():
return
}
// Send inputs one by one with stepDelay between them.
for _, in := range inputs {
if _, werr := ptmx.Write([]byte(in)); werr != nil {
// PTY may have closed already; stop sending.
break
}
select {
case <-time.After(stepDelay):
case <-ctx.Done():
return
}
}
// Main loop: emit snapshots on ticker, cut on idle or maxDur.
ticker := time.NewTicker(snapshotInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Emit current accumulated snapshot.
if !send(snapshot()) {
return
}
// Check termination conditions.
mu.Lock()
sinceLastByte := time.Since(lastByte)
mu.Unlock()
elapsed := time.Since(start)
if sinceLastByte >= idle || elapsed >= maxDur {
return
}
case <-ctx.Done():
return
}
}
}()
return ch, nil
}
+100
View File
@@ -0,0 +1,100 @@
---
name: pty_capture_stream
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func PTYCaptureStream(ctx context.Context, name string, args []string, warmup time.Duration, inputs []string, stepDelay, snapshotInterval, idle, maxDur time.Duration) (<-chan string, error)"
description: "Lanza un comando dentro de un pseudo-terminal (PTY) y emite snapshots acumulativos del buffer de output a través de un canal, en intervalos regulares. Cada snapshot es el contenido RAW completo del PTY hasta ese instante (ANSI incluido). Permite hacer streaming del render de una TUI mientras sigue generando, sin esperar al final."
tags: ["terminal", "pty", "tui", "capture", "automation", "streaming", "terminal-capture"]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
- "context"
- "time"
- "github.com/creack/pty"
tested: true
tests:
- "snapshots crecientes con pausas"
- "snapshot final siempre presente"
- "timeout duro con sleep 10"
test_file_path: "functions/infra/pty_capture_stream_test.go"
file_path: "functions/infra/pty_capture_stream.go"
params:
- name: ctx
desc: "Contexto de cancelación. Si se cancela, la goroutine interna aborta, emite el snapshot final y cierra el canal."
- name: name
desc: "Nombre o path del ejecutable a lanzar (debe existir en PATH o ser un path absoluto)."
- name: args
desc: "Argumentos posicionales para el ejecutable. Puede ser nil o vacío."
- name: warmup
desc: "Tiempo que se espera después de arrancar el proceso antes de enviar inputs. Permite que la TUI inicialice su render. Típico: 500ms4s para CLIs lentas como claude."
- name: inputs
desc: "Lista de strings a escribir al PTY en secuencia. Incluir '\\r' al final para simular Enter. Puede ser nil si solo se quiere observar sin interactuar."
- name: stepDelay
desc: "Espera entre cada input enviado. Permite que la TUI procese y renderice la respuesta de cada paso antes de enviar el siguiente."
- name: snapshotInterval
desc: "Cada cuánto tiempo se emite un snapshot del buffer acumulado al canal. Controla la frecuencia de actualización del streaming. Valores recomendados: 100ms300ms. Por debajo de 50ms genera mucho ruido y CPU innecesario."
- name: idle
desc: "Tiempo sin nuevos bytes en el PTY que se considera 'respuesta terminada'. Cuando el terminal lleva este tiempo sin actividad, la captura finaliza. Típico: 2s4s para claude, 500ms para CLIs rápidas."
- name: maxDur
desc: "Timeout duro desde el inicio de la función. Garantiza que el canal se cierra aunque la TUI siga emitiendo (spinners, relojes, progress bars). Típico: 60s120s para prompts de claude."
output: "Canal de strings (<-chan string). Cada string es el output RAW acumulado del terminal desde el arranque hasta ese instante, con secuencias ANSI intactas (no deltas). El canal se cierra cuando termina la captura; el último valor enviado antes del cierre es siempre el snapshot final completo. Error si pty.Start falla al arrancar el proceso."
---
## Ejemplo
```go
// Streaming de una sesión claude: ver la respuesta formarse en tiempo real.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
ch, err := PTYCaptureStream(
ctx,
"claude", nil,
4*time.Second, // warmup: esperar que claude cargue
[]string{"hola, responde PONG\r"}, // inputs: enviar mensaje + Enter
300*time.Millisecond, // stepDelay entre inputs
150*time.Millisecond, // snapshotInterval: snapshot cada 150ms
4*time.Second, // idle: cortar cuando lleve 4s sin output
120*time.Second, // maxDur: timeout duro
)
if err != nil {
log.Fatal(err)
}
var lastRender string
for raw := range ch {
// Aplicar VT render para reconstruir la pantalla 2D desde ANSI.
screen := VTRender(raw) // vt_render_go_tui
// Parsear el estado actual de la respuesta de claude.
resp := ParseClaudeTUI(screen) // parse_claude_tui_go_tui
if resp.Response != lastRender {
fmt.Printf("\r[streaming] %s", resp.Response)
lastRender = resp.Response
}
}
// Al salir del for, el canal está cerrado: captura terminada.
fmt.Println("\n[done]", lastRender)
```
## Cuando usarla
Cuando quieras observar el render de una TUI **mientras sigue generando** — por ejemplo, ver la respuesta de `claude` formarse en tiempo real en lugar de esperar al final. Cada snapshot del canal es el estado completo de la pantalla en ese instante; aplica `vt_render_go_tui` + `parse_claude_tui_go_tui` para extraer texto interpretado de cada frame.
Para captura one-shot (solo quieres el output final), usa `pty_capture_idle_go_infra` — más simple, sin goroutina consumidora.
## Gotchas
- **Linux/Unix only.** Usa PTY POSIX (`creack/pty`). No funciona en Windows.
- **Snapshots ACUMULATIVOS, no deltas.** Cada string del canal es el buffer completo desde el inicio, no solo los bytes nuevos. Para calcular lo nuevo en cada tick: `delta := snapshot[len(prevSnapshot):]` — o usa `text_prefix_delta_go_core` si existe. El consumidor decide si quiere el frame completo o el incremento.
- **El consumidor DEBE drenar el canal o cancelar ctx.** Si el canal (capacidad 16) se llena y el consumidor deja de leer, la goroutina interna se bloquea. Patrón seguro: `for range ch {}` en goroutina separada si no se necesita el contenido.
- **La TUI re-renderiza el frame entero.** El buffer crudo crece monotónicamente en bytes, pero el render VT interpretado puede no ser monótono (la TUI puede limpiar la pantalla y re-dibujar). Comparar `VTRender(snapshot)` frame a frame para detectar cambios reales.
- **snapshotInterval < 50ms genera ruido.** El output ANSI de una TUI activa puede cambiar miles de veces por segundo; muestrear muy rápido satura el canal con frames casi idénticos y consume CPU innecesariamente.
- **Idle es heurístico.** Si la TUI tiene spinners o progress bars que emiten bytes continuamente, `idle` nunca se dispara y la función espera hasta `maxDur`. Subir `maxDur` o detener el spinner antes de capturar.
- **EIO/EOF al cerrar PTY es normal en Linux.** El goroutine lector lo absorbe silenciosamente.
- **SIGTERM → SIGKILL.** Al terminar, se envía SIGTERM y se espera 2s antes de SIGKILL.
+120
View File
@@ -0,0 +1,120 @@
package infra
import (
"context"
"strings"
"testing"
"time"
)
func TestPTYCaptureStream(t *testing.T) {
t.Run("snapshots crecientes con pausas", func(t *testing.T) {
if testing.Short() {
t.Skip("skip en modo corto: timing sensible")
}
ctx := context.Background()
// bash -lc imprime A, pausa 0.3s, B, pausa 0.3s, C, pausa 0.3s.
// Con snapshotInterval 100ms e idle 400ms debería recibir varios snapshots
// y el último debe contener A, B y C.
ch, err := PTYCaptureStream(
ctx,
"bash", []string{"-lc", "printf A; sleep 0.3; printf B; sleep 0.3; printf C; sleep 0.3"},
50*time.Millisecond, // warmup
nil, // inputs
0, // stepDelay
100*time.Millisecond, // snapshotInterval
400*time.Millisecond, // idle
5*time.Second, // maxDur
)
if err != nil {
t.Fatalf("error inesperado al arrancar: %v", err)
}
var snapshots []string
for s := range ch {
snapshots = append(snapshots, s)
}
if len(snapshots) < 2 {
t.Errorf("se esperaban >=2 snapshots, got %d", len(snapshots))
}
// Snapshots deben ser acumulativos (monótonos en longitud).
for i := 1; i < len(snapshots); i++ {
if len(snapshots[i]) < len(snapshots[i-1]) {
t.Errorf("snapshot[%d] len=%d < snapshot[%d] len=%d — no acumulativo",
i, len(snapshots[i]), i-1, len(snapshots[i-1]))
}
}
// El último snapshot debe contener A, B y C.
last := snapshots[len(snapshots)-1]
for _, want := range []string{"A", "B", "C"} {
if !strings.Contains(last, want) {
t.Errorf("último snapshot no contiene %q: %q", want, last)
}
}
})
t.Run("snapshot final siempre presente", func(t *testing.T) {
if testing.Short() {
t.Skip("skip en modo corto")
}
ctx := context.Background()
// Output instantáneo; con idle 300ms el canal cierra rápido.
ch, err := PTYCaptureStream(
ctx,
"bash", []string{"-lc", "printf HOLA"},
50*time.Millisecond,
nil,
0,
150*time.Millisecond, // snapshotInterval
300*time.Millisecond, // idle
5*time.Second,
)
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
var last string
for s := range ch {
last = s
}
if !strings.Contains(last, "HOLA") {
t.Errorf("último snapshot no contiene 'HOLA': %q", last)
}
})
t.Run("timeout duro con sleep 10", func(t *testing.T) {
if testing.Short() {
t.Skip("skip en modo corto: espera ~1s de timeout")
}
ctx := context.Background()
start := time.Now()
ch, err := PTYCaptureStream(
ctx,
"sleep", []string{"10"},
50*time.Millisecond,
nil,
0,
200*time.Millisecond, // snapshotInterval
600*time.Millisecond, // idle
1*time.Second, // maxDur duro en 1s
)
if err != nil {
t.Fatalf("error inesperado al arrancar: %v", err)
}
// Drenar completamente el canal.
for range ch {
}
elapsed := time.Since(start)
// La función debe retornar en menos de 3s, no esperar los 10s del sleep.
if elapsed >= 3*time.Second {
t.Errorf("la función tardó %v, se esperaba < 3s", elapsed)
}
})
}