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() }