cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / ui / jev_test.go
jev_test.go 5.4K
1package ui23import (4 "context"5 "strings"6 "sync"7 "testing"89 tea "charm.land/bubbletea/v2"10 typesafe "github.com/FelineStateMachine/typesafe-go"1112 "src.rest/cat/012/internal/jev"13 "src.rest/cat/012/internal/sheet"14)1516// fakeJEV answers every question the same way and counts requests.17type fakeJEV struct {18 mu sync.Mutex19 calls int20 noul float6421}2223func (f *fakeJEV) SystemOne(_ context.Context, r typesafe.SystemOneRequest) (*typesafe.SystemOneResponse, error) {24 f.mu.Lock()25 defer f.mu.Unlock()26 f.calls++27 var a typesafe.Answer = typesafe.NoulAnswer{Noul: f.noul}28 if _, ok := r.Questions["answer"].(typesafe.ChoiceQuestion); ok {29 a = typesafe.ChoiceAnswer{Choice: "negative", Confidence: 0.8}30 }31 return &typesafe.SystemOneResponse{Answers: map[string]typesafe.Answer{"answer": a}}, nil32}3334// jevModel returns a model with JEV on, answered by a fake.35func jevModel(t *testing.T) (*Model, *fakeJEV) {36 t.Helper()37 fake := &fakeJEV{noul: 0.9}38 cache := jev.NewCache()39 m := tallModel()40 m.EnableJEV(fake, cache)41 return m, fake42}4344func TestJEVAnswersArriveInBackground(t *testing.T) {45 m, fake := jevModel(t)46 press(t, m, "The box was crushed", "<enter>")47 // Typing the formula queues a question; its answer comes back as a48 // message, which the press helper feeds in.49 press(t, m, `=JEV.TEST(A1, "Is this a complaint?")`, "<enter>", "<up>")50 if got := m.sheet.Value(addr("A2")); got.Kind != sheet.Bool || got.Num != 1 {51 t.Fatalf("A2 = %+v", got)52 }53 if !strings.Contains(line(m, contextLine), "JEV: 90% likely yes") {54 t.Errorf("context line %q", line(m, contextLine))55 }56 // Asking again with the same inputs is answered from the cache.57 press(t, m, "<down>", `=JEV.PROB(A1, "Is this a complaint?")`, "<enter>")58 if fake.calls != 1 {59 t.Errorf("%d requests for one question", fake.calls)60 }61 if got := strings.TrimSpace(rowCells(m, 3, 1)[0]); got != "90%" {62 t.Errorf("JEV.PROB shows %q", got)63 }64 // Ask JEV again forgets the answers for the selection.65 press(t, m, "<up>", "<shift+down>", "<ctrl+k>", "ask jev", "<enter>")66 if fake.calls != 2 {67 t.Errorf("refresh made %d requests in total", fake.calls)68 }69}7071func TestJEVPendingAndBusy(t *testing.T) {72 m, _ := jevModel(t)73 // Send the formula without running the resulting commands, so the74 // question stays in flight.75 for _, r := range `=JEV.CLASSIFY("meh", "Sentiment", "positive, negative")` {76 m.Update(tea.KeyPressMsg{Code: r, Text: string(r)})77 }78 _, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})79 if !sheet.IsPending(m.sheet.Value(addr("A1"))) {80 t.Fatalf("A1 = %+v", m.sheet.Value(addr("A1")))81 }82 status := line(m, m.height-1)83 if !strings.Contains(status, "JEV answering 1") || m.View().ProgressBar == nil {84 t.Errorf("status %q, progress %v", status, m.View().ProgressBar)85 }86 if got := strings.TrimSpace(rowCells(m, 1, 1)[0]); got != "Loading…" {87 t.Errorf("pending cell shows %q", got)88 }89 run(m, cmd)90 if got := m.sheet.Value(addr("A1")); got.Str != "negative" {91 t.Errorf("A1 = %+v", got)92 }93 if strings.Contains(line(m, m.height-1), "JEV") || m.View().ProgressBar != nil {94 t.Error("still busy after the answer")95 }96}9798func TestJEVWithoutKeyExplains(t *testing.T) {99 m := tallModel()100 press(t, m, `=JEV.TEST("x", "Q")`, "<enter>", "<up>")101 if !strings.Contains(line(m, contextLine), "Settings > JEV API key") {102 t.Errorf("context line %q", line(m, contextLine))103 }104 if commands["jev.refresh"].enabled(m) {105 t.Error("Ask JEV again is available without a key")106 }107}108109// Answers that arrive within a frame are recalculated together, once.110func TestJEVAnswersRecalculateOncePerFrame(t *testing.T) {111 m, _ := jevModel(t)112 var cmds []tea.Cmd113 for _, q := range []string{"Q1", "Q2", "Q3"} {114 for _, r := range `=JEV.TEST("x", "` + q + `")` {115 m.Update(tea.KeyPressMsg{Code: r, Text: string(r)})116 }117 _, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})118 cmds = append(cmds, cmd)119 }120 if in, _ := m.jev.cache.Busy(); in != 3 {121 t.Fatalf("%d questions in flight, want 3", in)122 }123 recalcs := 0124 sheet.OnRecalc = func(any, sheet.RecalcInfo) { recalcs++ }125 defer func() { sheet.OnRecalc = nil }()126 var answers []jevAnswerMsg127 for _, cmd := range cmds {128 for _, msg := range collect(cmd) {129 if a, ok := msg.(jevAnswerMsg); ok {130 answers = append(answers, a)131 }132 }133 }134 if len(answers) != 3 {135 t.Fatalf("%d answers", len(answers))136 }137 var tick tea.Cmd138 for i, a := range answers {139 _, cmd := m.Update(a)140 if i == 0 {141 tick = cmd142 }143 }144 if recalcs != 0 {145 t.Errorf("%d recalculations before the frame", recalcs)146 }147 run(m, tick)148 if recalcs != 1 {149 t.Errorf("%d recalculations for 3 answers, want 1", recalcs)150 }151 for _, a := range []string{"A1", "A2", "A3"} {152 if got := m.sheet.Value(addr(a)); got.Kind != sheet.Bool {153 t.Errorf("%s = %+v", a, got)154 }155 }156}157158// collect runs cmd and returns the messages it and its batches produce,159// without feeding them to a model.160func collect(cmd tea.Cmd) []tea.Msg {161 if cmd == nil {162 return nil163 }164 switch msg := cmd().(type) {165 case nil:166 return nil167 case tea.BatchMsg:168 var out []tea.Msg169 for _, c := range msg {170 out = append(out, collect(c)...)171 }172 return out173 default:174 return []tea.Msg{msg}175 }176}177178// Starting a new sheet or opening a file keeps JEV on.179func TestJEVSurvivesNewSheet(t *testing.T) {180 m, fake := jevModel(t)181 run(m, m.runCommand("file.new"))182 press(t, m, `=JEV.TEST("x", "Is this on?")`, "<enter>")183 if got := m.sheet.Value(addr("A1")); got.Kind != sheet.Bool || fake.calls != 1 {184 t.Errorf("after File > New: A1 %+v, %d requests", got, fake.calls)185 }186}