merge: issue/0046-father-bot-progress — progreso en tiempo real para Father Bot
Habilita streaming en Father Bot y mejora el ProgressReporter para mostrar mensajes legibles durante la creacion de agentes (paso N, nombre del script, emoji contextual) en vez de comandos raw.
This commit is contained in:
@@ -82,6 +82,8 @@ llm:
|
||||
model: "sonnet"
|
||||
fallback_model: "haiku"
|
||||
session_id: ""
|
||||
streaming: true
|
||||
show_tool_progress: true
|
||||
add_dirs:
|
||||
- ".claude/rules"
|
||||
- "agents/_template"
|
||||
|
||||
@@ -260,6 +260,17 @@ Reintenta hasta 3 veces con backoff si falla.
|
||||
|
||||
Si `DEVELOPER_MATRIX_USERS` no esta configurado, se salta con warning (no bloquea).
|
||||
|
||||
## Progreso en tiempo real
|
||||
|
||||
El sistema muestra automaticamente tus pasos al usuario mientras trabajas. Cada vez que usas una herramienta (Bash, Read, Edit, etc.), el usuario ve un mensaje de progreso que se actualiza en tiempo real en la sala de Matrix (un unico mensaje que se edita, no mensajes separados).
|
||||
|
||||
Por ejemplo, cuando ejecutas `./dev-scripts/agent/create-full.sh`, el usuario ve:
|
||||
```
|
||||
Paso 1 — 📦 Creando agente: scaffold, build, register, E2EE, avatar...
|
||||
```
|
||||
|
||||
**No necesitas enviar mensajes intermedios manualmente.** El sistema lo hace por ti. Concentra tu respuesta final en el resumen completo del resultado.
|
||||
|
||||
### Paso 9 — Reportar al usuario
|
||||
|
||||
Confirma al usuario con:
|
||||
|
||||
@@ -56,3 +56,4 @@ afectados y notas de implementacion.
|
||||
| 43 | Guardrails de seguridad para Father Bot | [0043-father-bot-security-guardrails.md](completed/0043-father-bot-security-guardrails.md) | completado |
|
||||
| 44 | Formalizar pipeline de creacion de agentes | [0044-formalize-agent-creation-pipeline.md](completed/0044-formalize-agent-creation-pipeline.md) | completado |
|
||||
| 45 | DM rooms sin E2EE en notify-developer.sh | [0045-notify-encrypted-rooms.md](completed/0045-notify-encrypted-rooms.md) | completado |
|
||||
| 46 | Progreso en tiempo real para Father Bot | [0046-father-bot-progress.md](completed/0046-father-bot-progress.md) | completado |
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# 0046 — Progreso en tiempo real para Father Bot
|
||||
|
||||
## Objetivo
|
||||
|
||||
Cuando Father Bot crea un agente o robot, el usuario no recibe ningun feedback visual
|
||||
durante 2-3 minutos (a veces mas). El ProgressReporter ya existe y funciona para agentes
|
||||
con `claude-code` streaming, pero esta **deshabilitado** en el config de Father Bot.
|
||||
Ademas, los mensajes de progreso actuales son crípticos ("🔧 *Bash*: `./dev-scripts/...`").
|
||||
|
||||
Queremos que el usuario vea en tiempo real en que paso del pipeline se encuentra Father Bot,
|
||||
con mensajes legibles como "📦 Scaffold + Build + Register..." en vez de comandos raw.
|
||||
|
||||
## Contexto
|
||||
|
||||
### Lo que ya existe
|
||||
|
||||
- `shell/effects/progress.go` — `ProgressReporter` que envia/edita un mensaje Matrix con cada
|
||||
evento de streaming. Rate-limited a 1 edit/segundo.
|
||||
- `devagents/handler.go:388` — `isStreamingEnabled()` verifica `provider == "claude-code"` +
|
||||
`Streaming` + `ShowToolProgress`. Actualmente **ambos flags estan ausentes** en el config
|
||||
de Father Bot → streaming deshabilitado.
|
||||
- `shell/llm/claudecode.go` — el provider ya soporta `--output-format stream-json` y parsea
|
||||
eventos `StreamToolUse`, `StreamInit`, `StreamText`, `StreamResult`.
|
||||
|
||||
### Evidencia en logs
|
||||
|
||||
- `logs/father-bot/2026-04-09.jsonl`: respuesta de 160.7s, 25 turns, $0.50 — **silencio total**
|
||||
para el usuario durante todo ese tiempo.
|
||||
- `logs/father-bot/2026-04-10.jsonl`: proceso claude-code killed por timeout (SIGKILL),
|
||||
el usuario no supo que paso hasta recibir un error generico.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
shell/effects/progress.go — MODIFICAR: mejorar formateo de tool events
|
||||
agents/_specials/father-bot/
|
||||
config.yaml — MODIFICAR: habilitar streaming + show_tool_progress
|
||||
prompts/system.md — MODIFICAR: mencionar que progreso es visible
|
||||
```
|
||||
|
||||
No hay cambios en `pkg/` (puro). Todo es shell/config.
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1: Habilitar streaming (quick win)
|
||||
|
||||
- [ ] 1.1 Agregar `streaming: true` y `show_tool_progress: true` en la seccion
|
||||
`claude_code` del config de Father Bot
|
||||
|
||||
### Fase 2: Mejorar formateo del ProgressReporter
|
||||
|
||||
- [ ] 2.1 Agregar logica en `ProgressReporter.handleEvent()` para detectar patrones
|
||||
conocidos en tool inputs y mostrar nombres legibles:
|
||||
- `create-full.sh` → "📦 Creando agente: scaffold, build, register, E2EE, avatar..."
|
||||
- `health-check.sh` → "🏥 Verificando health check..."
|
||||
- `restart.sh` / `start.sh` → "🔄 Reiniciando launcher..."
|
||||
- `notify-developer.sh` → "📨 Enviando bienvenida a developers..."
|
||||
- `go build` → "🔨 Compilando..."
|
||||
- `Edit` / `Write` sobre `agents/` → "✏️ Personalizando: <archivo>"
|
||||
- `Read` sobre `agents/` → "📖 Leyendo: <archivo>"
|
||||
- Tool generico → formato actual (🔧 *tool*: `input`)
|
||||
- [ ] 2.2 Agregar contador de pasos visibles ("Paso N — <descripcion>")
|
||||
|
||||
### Fase 3: System prompt
|
||||
|
||||
- [ ] 3.1 Actualizar `prompts/system.md` del Father Bot para mencionar que el progreso
|
||||
es visible en tiempo real y que no necesita enviar mensajes intermedios manuales
|
||||
|
||||
### Fase 4: Tests
|
||||
|
||||
- [ ] 4.1 Tests unitarios para la logica de deteccion de patrones en ProgressReporter
|
||||
- [ ] 4.2 `go build -tags goolm ./...` compila sin errores
|
||||
|
||||
### Fase 5: Docs
|
||||
|
||||
- [ ] 5.1 Cerrar issue, mover a completed
|
||||
|
||||
## Ejemplo de uso
|
||||
|
||||
Antes (sin progreso):
|
||||
```
|
||||
Usuario: "Crea un bot llamado hora-bot que diga la hora"
|
||||
Father Bot: [silencio durante 2-3 minutos]
|
||||
Father Bot: "He creado hora-bot exitosamente..."
|
||||
```
|
||||
|
||||
Despues (con progreso):
|
||||
```
|
||||
Usuario: "Crea un bot llamado hora-bot que diga la hora"
|
||||
Father Bot: ⚙️ Procesando...
|
||||
Father Bot: 📦 Paso 1 — Creando agente: scaffold, build, register, E2EE, avatar...
|
||||
Father Bot: ✏️ Paso 2 — Personalizando: config.yaml
|
||||
Father Bot: ✏️ Paso 3 — Personalizando: agent.go
|
||||
Father Bot: ✏️ Paso 4 — Personalizando: prompts/system.md
|
||||
Father Bot: 🔨 Paso 5 — Compilando...
|
||||
Father Bot: 🔄 Paso 6 — Reiniciando launcher...
|
||||
Father Bot: 🏥 Paso 7 — Verificando health check...
|
||||
Father Bot: 📨 Paso 8 — Enviando bienvenida a developers...
|
||||
Father Bot: ✅ Completado
|
||||
Father Bot: "He creado hora-bot exitosamente..."
|
||||
```
|
||||
|
||||
(Todo en un unico mensaje que se edita, no mensajes separados.)
|
||||
|
||||
## Decisiones de diseno
|
||||
|
||||
1. **Un solo mensaje editado**: el ProgressReporter ya usa send+edit. Mantener ese patron
|
||||
para no spammear el room.
|
||||
2. **Deteccion por patrones**: los nombres de scripts del pipeline son estables. Usar
|
||||
string matching en los inputs de tool para detectar el paso actual.
|
||||
3. **Generico pero con hints**: la logica de patrones vive en el ProgressReporter (generico),
|
||||
pero los patrones que detecta son especificos del pipeline de creacion. Esto es aceptable
|
||||
porque el ProgressReporter ya es especifico de claude-code.
|
||||
4. **No requiere cambios en pkg/**: todo es shell/config, respeta pure core / impure shell.
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
- Issue 0036 (streaming) — completado
|
||||
- Issue 0037 (father-bot) — completado
|
||||
|
||||
## Riesgos
|
||||
|
||||
- **Rate limiting de Matrix**: el homeserver podria limitar edits frecuentes. Mitigado por
|
||||
el rate limit de 1 edit/segundo ya existente en ProgressReporter.
|
||||
- **Patrones fragiles**: si se renombran los scripts del pipeline, los patrones se rompen.
|
||||
Mitigado porque los scripts son estables y el fallback es el formato generico actual.
|
||||
+81
-14
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -15,16 +16,19 @@ import (
|
||||
// emits streaming events (tool_use, text, result).
|
||||
//
|
||||
// It rate-limits edits to at most one per second to avoid flooding the
|
||||
// homeserver.
|
||||
// homeserver. When it recognises well-known pipeline commands (e.g.
|
||||
// create-full.sh, health-check.sh) it shows a human-readable step name
|
||||
// instead of the raw command.
|
||||
type ProgressReporter struct {
|
||||
sender MatrixSender
|
||||
roomID string
|
||||
logger *slog.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
eventID string // Matrix event ID of the progress message (empty until first send)
|
||||
lastEdit time.Time // timestamp of last edit, for rate limiting
|
||||
mu sync.Mutex
|
||||
eventID string // Matrix event ID of the progress message (empty until first send)
|
||||
lastEdit time.Time // timestamp of last edit, for rate limiting
|
||||
minInterval time.Duration
|
||||
step int // visible step counter (incremented on each tool_use)
|
||||
}
|
||||
|
||||
// NewProgressReporter creates a ProgressReporter that sends progress updates
|
||||
@@ -53,16 +57,12 @@ func (p *ProgressReporter) handleEvent(evt coretypes.StreamEvent) {
|
||||
|
||||
switch evt.Kind {
|
||||
case coretypes.StreamToolUse:
|
||||
// Show which tool is being used
|
||||
input := evt.ToolInput
|
||||
if len(input) > 60 {
|
||||
input = input[:57] + "..."
|
||||
}
|
||||
if input != "" {
|
||||
markdown = fmt.Sprintf("\U0001f527 *%s*: `%s`", evt.ToolName, input)
|
||||
} else {
|
||||
markdown = fmt.Sprintf("\U0001f527 *%s*", evt.ToolName)
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.step++
|
||||
step := p.step
|
||||
p.mu.Unlock()
|
||||
|
||||
markdown = formatToolEvent(step, evt.ToolName, evt.ToolInput)
|
||||
|
||||
case coretypes.StreamResult:
|
||||
// Final result — no need to update progress; the handler will send the actual reply
|
||||
@@ -86,6 +86,73 @@ func (p *ProgressReporter) handleEvent(evt coretypes.StreamEvent) {
|
||||
p.updateMessage(markdown)
|
||||
}
|
||||
|
||||
// pipelineHint describes a well-known command pattern and its human-readable label.
|
||||
type pipelineHint struct {
|
||||
substr string // substring to match in the tool input
|
||||
emoji string
|
||||
label string
|
||||
}
|
||||
|
||||
// pipelineHints maps well-known pipeline commands to friendly labels.
|
||||
// Order matters: first match wins.
|
||||
var pipelineHints = []pipelineHint{
|
||||
{"create-full.sh", "\U0001f4e6", "Creando agente: scaffold, build, register, E2EE, avatar..."},
|
||||
{"health-check.sh", "\U0001f3e5", "Verificando health check..."},
|
||||
{"notify-developer.sh", "\U0001f4e8", "Enviando bienvenida a developers..."},
|
||||
{"restart.sh", "\U0001f504", "Reiniciando launcher..."},
|
||||
{"start.sh", "\U0001f504", "Arrancando launcher..."},
|
||||
{"go build", "\U0001f528", "Compilando..."},
|
||||
{"go test", "\U0001f9ea", "Ejecutando tests..."},
|
||||
}
|
||||
|
||||
// formatToolEvent returns a human-readable markdown line for a streaming tool event.
|
||||
// If the tool/input matches a well-known pipeline pattern, a friendly label is shown;
|
||||
// otherwise falls back to a generic format.
|
||||
func formatToolEvent(step int, toolName, toolInput string) string {
|
||||
prefix := fmt.Sprintf("**Paso %d** \u2014 ", step)
|
||||
|
||||
// Check pipeline hints for Bash commands
|
||||
if toolName == "Bash" {
|
||||
for _, h := range pipelineHints {
|
||||
if strings.Contains(toolInput, h.substr) {
|
||||
return prefix + h.emoji + " " + h.label
|
||||
}
|
||||
}
|
||||
// Generic Bash command
|
||||
input := truncateInput(toolInput, 50)
|
||||
return prefix + "\U0001f527 `" + input + "`"
|
||||
}
|
||||
|
||||
// File operation tools with agent path detection
|
||||
if toolName == "Edit" || toolName == "Write" {
|
||||
file := truncateInput(toolInput, 60)
|
||||
return prefix + "\u270f\ufe0f Editando: `" + file + "`"
|
||||
}
|
||||
if toolName == "Read" {
|
||||
file := truncateInput(toolInput, 60)
|
||||
return prefix + "\U0001f4d6 Leyendo: `" + file + "`"
|
||||
}
|
||||
if toolName == "Glob" || toolName == "Grep" {
|
||||
input := truncateInput(toolInput, 50)
|
||||
return prefix + "\U0001f50d Buscando: `" + input + "`"
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
if toolInput != "" {
|
||||
input := truncateInput(toolInput, 50)
|
||||
return prefix + "\U0001f527 *" + toolName + "*: `" + input + "`"
|
||||
}
|
||||
return prefix + "\U0001f527 *" + toolName + "*"
|
||||
}
|
||||
|
||||
// truncateInput shortens a string for display, appending "..." if truncated.
|
||||
func truncateInput(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
|
||||
// updateMessage sends or edits the progress message, respecting rate limits.
|
||||
func (p *ProgressReporter) updateMessage(markdown string) {
|
||||
p.mu.Lock()
|
||||
|
||||
@@ -69,22 +69,22 @@ func TestIntegration_StreamToProgressReporter(t *testing.T) {
|
||||
t.Fatalf("expected 3 edits (tool uses), got %d", len(sender.edits))
|
||||
}
|
||||
|
||||
// First edit: Bash
|
||||
if !strings.Contains(sender.edits[0], "Bash") {
|
||||
t.Errorf("edit[0] should mention Bash, got %q", sender.edits[0])
|
||||
// First edit: Bash with "git status" (generic command, no pipeline hint)
|
||||
if !strings.Contains(sender.edits[0], "Paso 1") {
|
||||
t.Errorf("edit[0] should have step 1, got %q", sender.edits[0])
|
||||
}
|
||||
if !strings.Contains(sender.edits[0], "git status") {
|
||||
t.Errorf("edit[0] should show input, got %q", sender.edits[0])
|
||||
}
|
||||
|
||||
// Second edit: Read
|
||||
if !strings.Contains(sender.edits[1], "Read") {
|
||||
t.Errorf("edit[1] should mention Read, got %q", sender.edits[1])
|
||||
// Second edit: Read → "Leyendo"
|
||||
if !strings.Contains(sender.edits[1], "Leyendo") {
|
||||
t.Errorf("edit[1] should contain 'Leyendo', got %q", sender.edits[1])
|
||||
}
|
||||
|
||||
// Third edit: Edit
|
||||
if !strings.Contains(sender.edits[2], "Edit") {
|
||||
t.Errorf("edit[2] should mention Edit, got %q", sender.edits[2])
|
||||
// Third edit: Edit → "Editando"
|
||||
if !strings.Contains(sender.edits[2], "Editando") {
|
||||
t.Errorf("edit[2] should contain 'Editando', got %q", sender.edits[2])
|
||||
}
|
||||
|
||||
// All edits should target the same event ID
|
||||
|
||||
@@ -67,8 +67,9 @@ func TestProgressReporter_ToolUseEditsMessage(t *testing.T) {
|
||||
if len(sender.edits) != 1 {
|
||||
t.Fatalf("expected 1 edit, got %d", len(sender.edits))
|
||||
}
|
||||
if !strings.Contains(sender.edits[0], "Bash") {
|
||||
t.Errorf("edit = %q, should contain tool name", sender.edits[0])
|
||||
// Should contain step number and the command
|
||||
if !strings.Contains(sender.edits[0], "Paso 1") {
|
||||
t.Errorf("edit = %q, should contain step number", sender.edits[0])
|
||||
}
|
||||
if !strings.Contains(sender.edits[0], "ls -la") {
|
||||
t.Errorf("edit = %q, should contain tool input", sender.edits[0])
|
||||
@@ -224,3 +225,185 @@ func TestProgressReporter_ToolInputTruncation(t *testing.T) {
|
||||
t.Error("truncated input should end with ...")
|
||||
}
|
||||
}
|
||||
|
||||
// ── formatToolEvent unit tests ──────────────────────────────────────────
|
||||
|
||||
func TestFormatToolEvent_PipelineHints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tool string
|
||||
input string
|
||||
wantSub string // substring that must be present
|
||||
wantNot string // substring that must NOT be present (empty = skip)
|
||||
}{
|
||||
{
|
||||
name: "create-full.sh detected",
|
||||
tool: "Bash",
|
||||
input: "./dev-scripts/agent/create-full.sh hora-bot \"Hora Bot\"",
|
||||
wantSub: "Creando agente",
|
||||
},
|
||||
{
|
||||
name: "health-check.sh detected",
|
||||
tool: "Bash",
|
||||
input: "./dev-scripts/agent/health-check.sh hora-bot",
|
||||
wantSub: "health check",
|
||||
},
|
||||
{
|
||||
name: "notify-developer.sh detected",
|
||||
tool: "Bash",
|
||||
input: "./dev-scripts/agent/notify-developer.sh hora-bot agent \"Hora Bot\"",
|
||||
wantSub: "bienvenida",
|
||||
},
|
||||
{
|
||||
name: "restart.sh detected",
|
||||
tool: "Bash",
|
||||
input: "./dev-scripts/server/restart.sh",
|
||||
wantSub: "Reiniciando",
|
||||
},
|
||||
{
|
||||
name: "start.sh detected",
|
||||
tool: "Bash",
|
||||
input: "./dev-scripts/server/start.sh",
|
||||
wantSub: "Arrancando",
|
||||
},
|
||||
{
|
||||
name: "go build detected",
|
||||
tool: "Bash",
|
||||
input: "go build -tags goolm ./...",
|
||||
wantSub: "Compilando",
|
||||
},
|
||||
{
|
||||
name: "go test detected",
|
||||
tool: "Bash",
|
||||
input: "go test -tags goolm ./pkg/...",
|
||||
wantSub: "tests",
|
||||
},
|
||||
{
|
||||
name: "generic Bash command",
|
||||
tool: "Bash",
|
||||
input: "cat /etc/hostname",
|
||||
wantSub: "cat /etc/hostname",
|
||||
},
|
||||
{
|
||||
name: "Edit tool",
|
||||
tool: "Edit",
|
||||
input: "agents/hora-bot/config.yaml",
|
||||
wantSub: "Editando",
|
||||
},
|
||||
{
|
||||
name: "Write tool",
|
||||
tool: "Write",
|
||||
input: "agents/hora-bot/prompts/system.md",
|
||||
wantSub: "Editando",
|
||||
},
|
||||
{
|
||||
name: "Read tool",
|
||||
tool: "Read",
|
||||
input: "agents/hora-bot/agent.go",
|
||||
wantSub: "Leyendo",
|
||||
},
|
||||
{
|
||||
name: "Glob tool",
|
||||
tool: "Glob",
|
||||
input: "agents/*/config.yaml",
|
||||
wantSub: "Buscando",
|
||||
},
|
||||
{
|
||||
name: "Grep tool",
|
||||
tool: "Grep",
|
||||
input: "func Rules",
|
||||
wantSub: "Buscando",
|
||||
},
|
||||
{
|
||||
name: "unknown tool with input",
|
||||
tool: "CustomTool",
|
||||
input: "some argument",
|
||||
wantSub: "CustomTool",
|
||||
},
|
||||
{
|
||||
name: "unknown tool without input",
|
||||
tool: "CustomTool",
|
||||
input: "",
|
||||
wantSub: "CustomTool",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatToolEvent(1, tt.tool, tt.input)
|
||||
if !strings.Contains(got, tt.wantSub) {
|
||||
t.Errorf("formatToolEvent(1, %q, %q) = %q, want substring %q",
|
||||
tt.tool, tt.input, got, tt.wantSub)
|
||||
}
|
||||
if tt.wantNot != "" && strings.Contains(got, tt.wantNot) {
|
||||
t.Errorf("formatToolEvent(1, %q, %q) = %q, should NOT contain %q",
|
||||
tt.tool, tt.input, got, tt.wantNot)
|
||||
}
|
||||
// All outputs should have step prefix
|
||||
if !strings.Contains(got, "Paso 1") {
|
||||
t.Errorf("formatToolEvent output %q should contain step number", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatToolEvent_StepCounter(t *testing.T) {
|
||||
r1 := formatToolEvent(1, "Bash", "echo hello")
|
||||
r5 := formatToolEvent(5, "Read", "file.go")
|
||||
r12 := formatToolEvent(12, "Edit", "config.yaml")
|
||||
|
||||
if !strings.Contains(r1, "Paso 1") {
|
||||
t.Errorf("step 1: %q", r1)
|
||||
}
|
||||
if !strings.Contains(r5, "Paso 5") {
|
||||
t.Errorf("step 5: %q", r5)
|
||||
}
|
||||
if !strings.Contains(r12, "Paso 12") {
|
||||
t.Errorf("step 12: %q", r12)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressReporter_StepCounterIncrements(t *testing.T) {
|
||||
sender := &mockProgressSender{}
|
||||
pr := NewProgressReporter(sender, "!room:test", slog.Default())
|
||||
pr.minInterval = 0
|
||||
|
||||
fn := pr.StreamFunc()
|
||||
fn(coretypes.StreamEvent{Kind: coretypes.StreamInit})
|
||||
fn(coretypes.StreamEvent{Kind: coretypes.StreamToolUse, ToolName: "Bash", ToolInput: "echo 1"})
|
||||
fn(coretypes.StreamEvent{Kind: coretypes.StreamToolUse, ToolName: "Read", ToolInput: "file.go"})
|
||||
fn(coretypes.StreamEvent{Kind: coretypes.StreamToolUse, ToolName: "Edit", ToolInput: "file.go"})
|
||||
|
||||
if len(sender.edits) != 3 {
|
||||
t.Fatalf("expected 3 edits, got %d", len(sender.edits))
|
||||
}
|
||||
if !strings.Contains(sender.edits[0], "Paso 1") {
|
||||
t.Errorf("first edit should be Paso 1, got %q", sender.edits[0])
|
||||
}
|
||||
if !strings.Contains(sender.edits[1], "Paso 2") {
|
||||
t.Errorf("second edit should be Paso 2, got %q", sender.edits[1])
|
||||
}
|
||||
if !strings.Contains(sender.edits[2], "Paso 3") {
|
||||
t.Errorf("third edit should be Paso 3, got %q", sender.edits[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
maxLen int
|
||||
want string
|
||||
}{
|
||||
{"short", 10, "short"},
|
||||
{"exactly10!", 10, "exactly10!"},
|
||||
{"this is longer than ten", 10, "this is..."},
|
||||
{"", 10, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := truncateInput(tt.input, tt.maxLen)
|
||||
if got != tt.want {
|
||||
t.Errorf("truncateInput(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user