nub/internal/ui/tui/permission_test.go
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

171 lines
4.6 KiB
Go

package tui
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
)
func TestRequestPermission_BlocksUntilAnswered(t *testing.T) {
m := newTestModel(t)
m.permCh = make(chan permissionRequest)
resultCh := make(chan bool, 1)
go func() {
resultCh <- m.requestPermission(context.Background(), "edit", json.RawMessage(`{"path":"main.go"}`))
}()
var req permissionRequest
select {
case req = <-m.permCh:
case <-time.After(time.Second):
t.Fatal("timed out waiting for the request on permCh")
}
if req.tool != "edit" {
t.Errorf("req.tool = %q, want edit", req.tool)
}
select {
case <-resultCh:
t.Fatal("requestPermission returned before the response was sent")
case <-time.After(50 * time.Millisecond):
}
req.resp <- true
select {
case got := <-resultCh:
if !got {
t.Error("expected requestPermission to return true after resp<-true")
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for requestPermission to return")
}
}
func TestRequestPermission_ContextCancelDeniesWithoutHanging(t *testing.T) {
m := newTestModel(t)
m.permCh = make(chan permissionRequest) // niemand liest daraus
ctx, cancel := context.WithCancel(context.Background())
resultCh := make(chan bool, 1)
go func() {
resultCh <- m.requestPermission(ctx, "bash", json.RawMessage(`{"command":"ls"}`))
}()
cancel()
select {
case got := <-resultCh:
if got {
t.Error("expected a cancelled context to deny, got true")
}
case <-time.After(time.Second):
t.Fatal("requestPermission did not return after context cancellation")
}
}
func TestHandlePermissionKey_YesAllows(t *testing.T) {
m := newTestModel(t)
resp := make(chan bool, 1)
m.permReq = &permissionRequest{tool: "edit", input: json.RawMessage(`{}`), resp: resp}
m.handlePermissionKey(yKey())
if m.permReq != nil {
t.Error("expected permReq to be cleared")
}
select {
case got := <-resp:
if !got {
t.Error("expected 'y' to allow")
}
default:
t.Fatal("expected a response to be sent on resp")
}
if len(m.entries) != 1 || m.entries[0].kind != entryCommand {
t.Errorf("expected a recorded command entry, got %+v", m.entries)
}
}
func TestHandlePermissionKey_AnyOtherKeyDenies(t *testing.T) {
m := newTestModel(t)
resp := make(chan bool, 1)
m.permReq = &permissionRequest{tool: "bash", input: json.RawMessage(`{}`), resp: resp}
m.handlePermissionKey(escKey())
if m.permReq != nil {
t.Error("expected permReq to be cleared")
}
select {
case got := <-resp:
if got {
t.Error("expected esc to deny")
}
default:
t.Fatal("expected a response to be sent on resp")
}
}
func TestUpdate_PermissionRequestMsgOpensPromptAndReListens(t *testing.T) {
m := newTestModel(t)
m.permCh = make(chan permissionRequest, 1)
req := permissionRequest{tool: "write", input: json.RawMessage(`{"path":"x"}`), resp: make(chan bool, 1)}
_, cmd := m.Update(permissionRequestMsg{req: req})
if m.permReq == nil || m.permReq.tool != "write" {
t.Fatalf("expected permReq to be set to the incoming request, got %+v", m.permReq)
}
if cmd == nil {
t.Fatal("expected Update to re-arm listenPermissionRequests")
}
if len(m.entries) != 1 || m.entries[0].kind != entryPermission {
t.Fatalf("expected the request to appear inline in the transcript, got %+v", m.entries)
}
if !strings.Contains(m.entries[0].text, "write") {
t.Errorf("permission entry should name the tool, got: %q", m.entries[0].text)
}
}
// TestView_PermissionRequestDoesNotTakeOverTheScreen ist der eigentliche
// Regressionstest für das Overlay-Problem: eine offene Rückfrage darf die
// Nachrichtenliste/Eingabe/Statuszeile nicht verdecken, sonst sieht man
// nicht mehr, was vorher im Transkript passiert ist.
func TestView_PermissionRequestDoesNotTakeOverTheScreen(t *testing.T) {
m := newTestModelWithStore(t) // renderStatusLine liest m.store.ID
m.ready = true
m.width, m.height = 80, 24
m.viewport.Width, m.viewport.Height = 80, 20
m.entries = []entry{{kind: entryUser, text: "hello from before"}}
m.renderViewport()
m.permReq = &permissionRequest{tool: "edit", input: json.RawMessage(`{}`), resp: make(chan bool, 1)}
view := m.View()
if !strings.Contains(view, m.textarea.View()) {
t.Error("expected the input box to remain visible while a permission request is pending")
}
if !strings.Contains(stripANSI(view), "hello from before") {
t.Error("expected prior transcript content to remain visible, not be replaced by a full-screen overlay")
}
}
func stripANSI(s string) string {
var b strings.Builder
inEscape := false
for _, r := range s {
if r == '\x1b' {
inEscape = true
continue
}
if inEscape {
if r == 'm' {
inEscape = false
}
continue
}
b.WriteRune(r)
}
return b.String()
}