feat(ui): команды, snapshots, Fyne-окно и интеграция (--noui)
Some checks failed
CI / test (pull_request) Failing after 1m19s
CI / build-and-package (amd64, linux) (pull_request) Failing after 58s
CI / build-and-package (amd64, windows) (pull_request) Successful in 53s

Команды (спец 12.2):
- ui.Commands: действия UI → текстовые команды канала (start/cancel/skip/
  retry/continue/approve/send), единый путь через chat.Channel.
- ui.Window = chat.Channel + View; NilWindow для headless/тестов.

Snapshots (спец 12.5):
- ui.Store: чтение-модель, возвращает только копии (ListTasks/GetTask/
  GetHistory/GetTraces); ui.DBStore поверх storage.

Fyne-окно (internal/ui/desktop, build-tag cgo):
- список задач слева, сплиты рабочей области и панели «Логи»/«Состояние»,
  ввод+кнопки команд, тёмная тема, fullscreen, сохранение layout в
  Preferences, сворачивание при закрытии крестиком.
- Колбэки View через fyne.Do (спец 12.4).

Интеграция:
- app.New(..., noUI); флаг --noui; UI собирается только с cgo (ui_cgo/
  ui_noui фабрики), приложение headless без него.
- Run: окно блокирует главную горутину; «Завершить» → cancel → graceful
  shutdown (спец 12.6).
- go.mod: fyne.io/fyne/v2 v2.6.0 (direct).
This commit is contained in:
ki.sagidullin
2026-08-20 08:54:55 +05:00
parent baf2ad7147
commit c2272137b3
17 changed files with 1056 additions and 18 deletions

View File

@@ -0,0 +1,80 @@
package ui
import (
"context"
"testing"
"time"
"github.com/kamelion/ratatoskr-go/internal/chat"
)
func TestCommandsMapToText(t *testing.T) {
var got []string
c := NewCommands(func(text string) { got = append(got, text) })
c.Start()
c.Cancel()
c.Skip()
c.Retry(7)
c.Status(7)
c.Continue(3)
c.Approve()
c.SendText("просто текст")
want := []string{"/start", "/cancel", "/skip", "/retry 7", "/status 7", "/continue 3", "создавай", "просто текст"}
if len(got) != len(want) {
t.Fatalf("got %d commands, want %d: %v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("command %d = %q, want %q", i, got[i], want[i])
}
}
}
func TestNilWindowSubmitReachesHandler(t *testing.T) {
w := NewNilWindow()
incoming := make(chan chat.Incoming, 1)
w.OnMessage(func(inc chat.Incoming) {
incoming <- inc
})
w.Submit("/start")
select {
case inc := <-incoming:
if inc.UserID != UID {
t.Errorf("UserID = %q, want %q", inc.UserID, UID)
}
if inc.Address != Address {
t.Errorf("Address = %q, want %q", inc.Address, Address)
}
if inc.Msg.Text != "/start" {
t.Errorf("Msg.Text = %q, want /start", inc.Msg.Text)
}
case <-time.After(2 * time.Second):
t.Fatal("handler not called")
}
}
func TestNilWindowRunStopsOnCancel(t *testing.T) {
w := NewNilWindow()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() { done <- w.Run(ctx) }()
time.Sleep(50 * time.Millisecond)
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Run did not stop on cancel")
}
}
func TestNilWindowSendNoop(t *testing.T) {
w := NewNilWindow()
if err := w.Send(context.Background(), Address, chat.Message{Text: "hi"}); err != nil {
t.Fatalf("Send: %v", err)
}
}