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
+214
View File
@@ -0,0 +1,214 @@
package tui
import (
"testing"
)
// goldenScreen is the exact sample screen from the spec.
const goldenScreen = `╭─── Claude Code v2.1.161 ─────────────────────────────────────────────────────────────────────────────────────────────╮
│ │ Tips for getting started │
│ Welcome back Enmanuel! │ Run /init to create a CLAUDE.md file with instructions for Cla… │
│ │ ─────────────────────────────────────────────────────────────── │
│ ▐▛███▜▌ │ What's new │
│ ▝▜█████▛▘ │ ` + "`OTEL_RESOURCE_ATTRIBUTES`" + ` values are now included as labels o… │
│ ▘▘ ▝▝ │ ` + "`claude agents`" + ` rows now show ` + "`done/total`" + ` before the detail w… │
│ Opus 4.8 (1M context) with xh… · Claude Max · │ ` + "`/mcp`" + ` now collapses claude.ai connectors you've never signed … │
│ gutierenmanuel15@gmail.com's Organization │ /release-notes for more │
│ ~/fn_registry │ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
responde unicamente con la palabra PONG, sin explicaciones
● PONG
✻ Crunched for 2s
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Opus 4.8 (1M context) │ CTX: █░░░░░░░░░ 11% (107k/1.0M) │ IN:6k OUT:5 (cache:17k) │ ⎇ master [~4 ?28 ↑1] │ 22:26
$0.565 │ +0/-0 │ Total: ↓107k/↑5 │ Limits: 5h:6% →02:40 │ 7d:11% →Sun 17:00 │ ⏱ 7s │ ~/fn_registry
← for agents`
func TestParseClaudeTUI(t *testing.T) {
t.Run("golden screen — banner + status bar + single Q&A", func(t *testing.T) {
got := ParseClaudeTUI(goldenScreen)
if got.Answer != "PONG" {
t.Errorf("Answer = %q, want %q", got.Answer, "PONG")
}
if len(got.Turns) != 2 {
t.Errorf("len(Turns) = %d, want 2", len(got.Turns))
for i, turn := range got.Turns {
t.Logf(" Turns[%d]: role=%s text=%q", i, turn.Role, turn.Text)
}
return
}
if got.Turns[0].Role != ClaudeTurnUser {
t.Errorf("Turns[0].Role = %q, want %q", got.Turns[0].Role, ClaudeTurnUser)
}
wantUserText := "responde unicamente con la palabra PONG, sin explicaciones"
if got.Turns[0].Text != wantUserText {
t.Errorf("Turns[0].Text = %q, want %q", got.Turns[0].Text, wantUserText)
}
if got.Turns[1].Role != ClaudeTurnAssistant {
t.Errorf("Turns[1].Role = %q, want %q", got.Turns[1].Role, ClaudeTurnAssistant)
}
if got.Turns[1].Text != "PONG" {
t.Errorf("Turns[1].Text = %q, want %q", got.Turns[1].Text, "PONG")
}
})
t.Run("multiline assistant response", func(t *testing.T) {
screen := ` explica brevemente
● linea uno
linea dos`
got := ParseClaudeTUI(screen)
if len(got.Turns) != 2 {
t.Fatalf("len(Turns) = %d, want 2; turns: %+v", len(got.Turns), got.Turns)
}
wantText := "linea uno\nlinea dos"
if got.Turns[1].Text != wantText {
t.Errorf("Turns[1].Text = %q, want %q", got.Turns[1].Text, wantText)
}
if !contains(got.Answer, "linea uno") || !contains(got.Answer, "linea dos") {
t.Errorf("Answer %q should contain both continuation lines", got.Answer)
}
})
t.Run("tool_use + tool_result + final assistant text", func(t *testing.T) {
screen := ` pregunta
● Read(main.go)
⎿ Read 50 lines
● aqui esta el resumen`
got := ParseClaudeTUI(screen)
if len(got.Turns) != 4 {
t.Fatalf("len(Turns) = %d, want 4; turns: %+v", len(got.Turns), got.Turns)
}
if got.Turns[0].Role != ClaudeTurnUser {
t.Errorf("Turns[0].Role = %q", got.Turns[0].Role)
}
if got.Turns[1].Role != ClaudeTurnToolUse {
t.Errorf("Turns[1].Role = %q, want tool_use", got.Turns[1].Role)
}
if got.Turns[1].ToolName != "Read" {
t.Errorf("Turns[1].ToolName = %q, want Read", got.Turns[1].ToolName)
}
if got.Turns[2].Role != ClaudeTurnToolResult {
t.Errorf("Turns[2].Role = %q, want tool_result", got.Turns[2].Role)
}
if got.Turns[3].Role != ClaudeTurnAssistant {
t.Errorf("Turns[3].Role = %q, want assistant", got.Turns[3].Role)
}
// Answer must be ONLY the assistant text, not the tool_use.
if got.Answer != "aqui esta el resumen" {
t.Errorf("Answer = %q, want %q", got.Answer, "aqui esta el resumen")
}
})
t.Run("multi-turn — answer from last user only", func(t *testing.T) {
screen := ` primera pregunta
● primera respuesta
segunda pregunta
● segunda respuesta`
got := ParseClaudeTUI(screen)
if len(got.Turns) != 4 {
t.Fatalf("len(Turns) = %d, want 4; turns: %+v", len(got.Turns), got.Turns)
}
if got.Answer != "segunda respuesta" {
t.Errorf("Answer = %q, want %q", got.Answer, "segunda respuesta")
}
})
t.Run("no banner no status bar — minimal screen", func(t *testing.T) {
screen := " hola\n\n● mundo"
got := ParseClaudeTUI(screen)
if len(got.Turns) != 2 {
t.Fatalf("len(Turns) = %d, want 2; turns: %+v", len(got.Turns), got.Turns)
}
if got.Answer != "mundo" {
t.Errorf("Answer = %q, want %q", got.Answer, "mundo")
}
})
t.Run("determinism — same input produces same output", func(t *testing.T) {
first := ParseClaudeTUI(goldenScreen)
second := ParseClaudeTUI(goldenScreen)
if first.Answer != second.Answer {
t.Errorf("non-deterministic: %q != %q", first.Answer, second.Answer)
}
if len(first.Turns) != len(second.Turns) {
t.Errorf("non-deterministic turns count: %d != %d", len(first.Turns), len(second.Turns))
}
})
}
// TestParseClaudeTUI_Spinner verifies that the generation spinner — which shows a
// DIFFERENT random gerund word on every frame ("Whatchamacalliting", "Forging",
// "Puzzling", "Crunched"...) — is never folded into the answer, regardless of the
// word, the glyph, or whether the "(Ns · tokens)" suffix is present yet.
func TestParseClaudeTUI_Spinner(t *testing.T) {
cases := []struct {
name string
screen string
want string
}{
{
name: "spinner with tokens suffix glued after answer",
screen: " di PONG\n\n● PONG\n\n✽ Whatchamacalliting… (2s · ↓ 1 tokens · esc to interrupt)\n",
want: "PONG",
},
{
name: "spinner early frame, no suffix yet, different word",
screen: " di HOLA\n\n● HOLA\n\n✶ Puzzling…\n",
want: "HOLA",
},
{
name: "classic crunched line",
screen: " x\n\n● respuesta\n\n✻ Crunched for 4s\n",
want: "respuesta",
},
{
name: "spinner BEFORE the answer block (mid-generation snapshot)",
screen: " pregunta\n\n✽ Forging… (1s · ↑ 3 tokens · esc to interrupt)\n\n● respuesta parcial\n",
want: "respuesta parcial",
},
{
name: "assistant line ending in ellipsis is NOT treated as spinner",
screen: " x\n\n● la historia continua…\n",
want: "la historia continua…",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := ParseClaudeTUI(tc.screen)
if got.Answer != tc.want {
t.Errorf("Answer = %q, want %q", got.Answer, tc.want)
}
})
}
}
func contains(s, sub string) bool {
return len(sub) == 0 || (len(s) >= len(sub) && (s == sub ||
len(s) > 0 && containsStr(s, sub)))
}
func containsStr(s, sub string) bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}