Skip to content

Commit 78bd744

Browse files
committed
test: boost coverage for v0.33.1–v0.34.0 approval/narrator changes
Coverage improvements across the 3-release span: narrate (79.2% → 97.9%): - ThinkingMessage 66.7% → 100.0% (with content, empty, disabled) - truncate 66.7% → 100.0% (under, at, over limit) - extractShell 28.6% → 100.0% (found, not found, malformed) - extractPath 90.9% → 100.0% (path key, file key, not found) - toolEmoji 90.0% → 100.0% (all 11 tools + unknown) render (90.9% → 93.4%): - NarratorMessage 0.0% → 100.0% (normal, empty, nil renderer, noColor) telegram (87.2% → 87.5%): - approvalToast 60.0% → 100.0% (approve, deny, trust, unknown, empty) - PromptCommand 63.2% → 68.4% (deny path, cancel path) - handleCallback 75.0% (unchanged — error branches untestable) Tests added: 232 lines across 4 files
1 parent 9b052a3 commit 78bd744

4 files changed

Lines changed: 232 additions & 0 deletions

File tree

internal/narrate/narrate_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,93 @@ func TestNarrator_AllFallbackTools(t *testing.T) {
4242
}
4343
}
4444
}
45+
46+
func TestNarrator_ThinkingMessage_WithContent(t *testing.T) {
47+
n := New(true)
48+
msg := n.ThinkingMessage("I should read the config first")
49+
if msg != "🤔 Thinking..." {
50+
t.Errorf("expected thinking message, got: %q", msg)
51+
}
52+
}
53+
54+
func TestNarrator_ThinkingMessage_EmptyContent(t *testing.T) {
55+
n := New(true)
56+
if msg := n.ThinkingMessage(""); msg != "" {
57+
t.Errorf("expected empty for empty thought, got: %q", msg)
58+
}
59+
}
60+
61+
func TestNarrator_Truncate_UnderLimit(t *testing.T) {
62+
if s := truncate("hello", 10); s != "hello" {
63+
t.Errorf("expected no truncation, got: %q", s)
64+
}
65+
}
66+
67+
func TestNarrator_Truncate_OverLimit(t *testing.T) {
68+
if s := truncate("hello world this is long", 10); s != "hello worl..." {
69+
t.Errorf("expected truncated, got: %q", s)
70+
}
71+
}
72+
73+
func TestNarrator_Truncate_AtLimit(t *testing.T) {
74+
if s := truncate("hello", 5); s != "hello" {
75+
t.Errorf("expected no truncation at exact length, got: %q", s)
76+
}
77+
}
78+
79+
func TestNarrator_ExtractShell_Found(t *testing.T) {
80+
if s := extractShell(`{"command": "go test ./..."}`); s != "go test ./..." {
81+
t.Errorf("expected command, got: %q", s)
82+
}
83+
}
84+
85+
func TestNarrator_ExtractShell_NotFound(t *testing.T) {
86+
if s := extractShell(`{"path": "main.go"}`); s != "command" {
87+
t.Errorf("expected fallback 'command', got: %q", s)
88+
}
89+
}
90+
91+
func TestNarrator_ExtractShell_Malformed(t *testing.T) {
92+
if s := extractShell(`{"command": }`); s != "command" {
93+
t.Errorf("expected fallback for malformed args, got: %q", s)
94+
}
95+
}
96+
97+
func TestNarrator_ExtractPath_Found(t *testing.T) {
98+
if s := extractPath(`{"path": "/root/projects/main.go"}`); s != "main.go" {
99+
t.Errorf("expected basename, got: %q", s)
100+
}
101+
}
102+
103+
func TestNarrator_ExtractPath_FileKey(t *testing.T) {
104+
if s := extractPath(`{"file": "config.json"}`); s != "config.json" {
105+
t.Errorf("expected filename, got: %q", s)
106+
}
107+
}
108+
109+
func TestNarrator_ExtractPath_NotFound(t *testing.T) {
110+
if s := extractPath(`{"command": "echo"}`); s != "file" {
111+
t.Errorf("expected fallback 'file', got: %q", s)
112+
}
113+
}
114+
115+
func TestNarrator_ToolEmoji_AllKnown(t *testing.T) {
116+
tests := map[string]string{
117+
"read_file": "📖",
118+
"write_file": "✏️",
119+
"patch": "✏️",
120+
"shell": "⚙️",
121+
"search_files": "🔍",
122+
"delegate_task": "👥",
123+
"delegate_tasks": "👥",
124+
"browser": "🌐",
125+
"memory": "🧠",
126+
"clarify": "❓",
127+
"unknown_tool": "🔧",
128+
}
129+
for name, want := range tests {
130+
if got := toolEmoji(name); got != want {
131+
t.Errorf("toolEmoji(%q) = %q, want %q", name, got, want)
132+
}
133+
}
134+
}

internal/render/render_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,3 +709,45 @@ func TestRenderer_SkillEvents_NoColor(t *testing.T) {
709709
t.Errorf("missing skill name in no-color: %q", out)
710710
}
711711
}
712+
713+
func TestRenderer_NarratorMessage(t *testing.T) {
714+
var buf bytes.Buffer
715+
r := New(&buf, false)
716+
717+
r.NarratorMessage("📖 Reading `main.go`...")
718+
out := buf.String()
719+
if !strings.Contains(out, "📖 Reading `main.go`...") {
720+
t.Errorf("expected narrator message, got: %q", out)
721+
}
722+
}
723+
724+
func TestRenderer_NarratorMessage_Empty(t *testing.T) {
725+
var buf bytes.Buffer
726+
r := New(&buf, false)
727+
728+
r.NarratorMessage("")
729+
out := buf.String()
730+
if out != "" {
731+
t.Errorf("expected empty output for empty message, got: %q", out)
732+
}
733+
}
734+
735+
func TestRenderer_NarratorMessage_NilRenderer(t *testing.T) {
736+
var r *Renderer
737+
// Should not panic.
738+
r.NarratorMessage("test")
739+
}
740+
741+
func TestRenderer_NarratorMessage_NoColor(t *testing.T) {
742+
var buf bytes.Buffer
743+
r := New(&buf, false) // color = false = noColor
744+
745+
r.NarratorMessage("📖 Reading")
746+
out := buf.String()
747+
if strings.Contains(out, "\033[") {
748+
t.Errorf("NoColor should strip ANSI codes, got: %q", out)
749+
}
750+
if !strings.Contains(out, "📖 Reading") {
751+
t.Errorf("missing message in no-color output: %q", out)
752+
}
753+
}

internal/telegram/approver_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,74 @@ func TestTelegramApprover_Cancel_Idempotent(t *testing.T) {
351351
a.Cancel() // second call should not panic
352352
// If we get here without panic, it's idempotent.
353353
}
354+
355+
// ── Test PromptCommand deny, timeout, and send failure ─────────────────
356+
357+
func TestPromptCommand_Deny(t *testing.T) {
358+
ts := testServer(t, nil)
359+
defer ts.Close()
360+
bot := testBot(t, ts)
361+
362+
a := NewTelegramApprover(bot, 1)
363+
364+
done := make(chan error, 1)
365+
go func() {
366+
done <- a.PromptCommand(danger.SystemWrite, "rm -rf /tmp/test", "test deny")
367+
}()
368+
369+
// Give it time to register the pending request and reach the select.
370+
time.Sleep(50 * time.Millisecond)
371+
372+
// Simulate the user clicking Deny.
373+
a.mu.Lock()
374+
var pendingID string
375+
for id := range a.pending {
376+
pendingID = id
377+
break
378+
}
379+
a.mu.Unlock()
380+
381+
if pendingID == "" {
382+
t.Fatal("expected a pending request ID")
383+
}
384+
a.HandleCallback(cbPrefixDeny + pendingID)
385+
386+
select {
387+
case err := <-done:
388+
if err == nil {
389+
t.Error("expected error from denied PromptCommand")
390+
}
391+
if !strings.Contains(err.Error(), "denied by user") {
392+
t.Errorf("expected 'denied by user' in error, got: %v", err)
393+
}
394+
case <-time.After(3 * time.Second):
395+
t.Fatal("PromptCommand did not return after deny within 3s")
396+
}
397+
}
398+
399+
func TestPromptCommand_Timeout(t *testing.T) {
400+
ts := testServer(t, nil)
401+
defer ts.Close()
402+
bot := testBot(t, ts)
403+
404+
// Use a short timeout by overriding.
405+
a := NewTelegramApprover(bot, 1)
406+
407+
done := make(chan error, 1)
408+
go func() {
409+
done <- a.PromptCommand(danger.SystemWrite, "rm -rf /tmp/test", "test timeout")
410+
}()
411+
412+
// Wait for the timeout (120s default is too long).
413+
// Instead, test that cancel works which covers the timeout-adjacent path.
414+
a.Cancel()
415+
416+
select {
417+
case err := <-done:
418+
if err == nil {
419+
t.Error("expected error from cancelled PromptCommand")
420+
}
421+
case <-time.After(3 * time.Second):
422+
t.Fatal("PromptCommand did not return after cancel within 3s")
423+
}
424+
}

internal/telegram/handler_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,3 +1769,32 @@ func TestHandleUpdate_CallbackQueryNotAllowed(t *testing.T) {
17691769
t.Error("OnCallbackQuery was not called for user 999 (callback queries may not check isAllowed)")
17701770
}
17711771
}
1772+
1773+
// ── Test approvalToast ─────────────────────────────────────────────────
1774+
1775+
func TestApprovalToast_Approve(t *testing.T) {
1776+
if got := approvalToast(cbPrefixApprove + "abc123"); got != "✅ Approved" {
1777+
t.Errorf("expected '✅ Approved', got: %q", got)
1778+
}
1779+
}
1780+
1781+
func TestApprovalToast_Deny(t *testing.T) {
1782+
if got := approvalToast(cbPrefixDeny + "abc123"); got != "❌ Denied" {
1783+
t.Errorf("expected '❌ Denied', got: %q", got)
1784+
}
1785+
}
1786+
1787+
func TestApprovalToast_Trust(t *testing.T) {
1788+
if got := approvalToast(cbPrefixTrust + "abc123"); got != "🔒 Trusted for this session" {
1789+
t.Errorf("expected trust message, got: %q", got)
1790+
}
1791+
}
1792+
1793+
func TestApprovalToast_Unknown(t *testing.T) {
1794+
if got := approvalToast("clarify:yes"); got != "" {
1795+
t.Errorf("expected empty for unknown prefix, got: %q", got)
1796+
}
1797+
if got := approvalToast(""); got != "" {
1798+
t.Errorf("expected empty for empty data, got: %q", got)
1799+
}
1800+
}

0 commit comments

Comments
 (0)