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

107
internal/ui/store_test.go Normal file
View File

@@ -0,0 +1,107 @@
package ui
import (
"context"
"testing"
"github.com/kamelion/ratatoskr-go/internal/storage"
)
func newTestStore(t *testing.T) *storage.Storage {
t.Helper()
s, err := storage.Open(context.Background(), ":memory:")
if err != nil {
t.Fatalf("open storage: %v", err)
}
t.Cleanup(func() { s.Close() })
return s
}
func TestDBStoreListTasksReturnsCopies(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
id1, err := s.CreateTask(ctx, &storage.Task{ChatID: "ui://local", Title: "A"})
if err != nil {
t.Fatalf("create: %v", err)
}
if _, err := s.CreateTask(ctx, &storage.Task{ChatID: "tg://1", Title: "B"}); err != nil {
t.Fatalf("create: %v", err)
}
st := NewDBStore(s)
tasks, err := st.ListTasks(ctx)
if err != nil {
t.Fatalf("ListTasks: %v", err)
}
if len(tasks) != 2 {
t.Fatalf("got %d tasks, want 2", len(tasks))
}
// мутация полученной копии не влияет на БД
tasks[0].Title = "mutated"
fetched, err := s.GetTask(ctx, id1)
if err != nil {
t.Fatalf("get: %v", err)
}
if fetched.Title == "mutated" {
t.Fatal("mutating snapshot leaked into DB")
}
}
func TestDBStoreGetTaskAndHistory(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
id, err := s.CreateTask(ctx, &storage.Task{ChatID: "ui://local", Title: "A"})
if err != nil {
t.Fatalf("create: %v", err)
}
if err := s.AppendHistory(ctx, id, "user", "hello"); err != nil {
t.Fatalf("append: %v", err)
}
st := NewDBStore(s)
task, err := st.GetTask(ctx, id)
if err != nil {
t.Fatalf("GetTask: %v", err)
}
if task.ID != id {
t.Fatalf("task.ID = %d, want %d", task.ID, id)
}
hist, err := st.GetHistory(ctx, id)
if err != nil {
t.Fatalf("GetHistory: %v", err)
}
if len(hist) != 1 || hist[0].Content != "hello" {
t.Fatalf("bad history: %+v", hist)
}
}
func TestDBStoreGetTraces(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
id, err := s.CreateTask(ctx, &storage.Task{ChatID: "ui://local"})
if err != nil {
t.Fatalf("create: %v", err)
}
tid, err := s.AppendTrace(ctx, &storage.Trace{TaskID: id, Agent: "dev", Prompt: "p"})
if err != nil {
t.Fatalf("append trace: %v", err)
}
_ = s.UpdateTraceStatus(ctx, tid, storage.TraceSuccess)
st := NewDBStore(s)
traces, err := st.GetTraces(ctx, id)
if err != nil {
t.Fatalf("GetTraces: %v", err)
}
if len(traces) != 1 {
t.Fatalf("got %d traces, want 1", len(traces))
}
if traces[0].Status != storage.TraceSuccess {
t.Fatalf("trace status = %s, want success", traces[0].Status)
}
}