1607009215
voice.Replier.Reply had no context, so llmReplier phrased under context.Background() and the only bound on a reply was phraser.timeout, 60s in deploy. Both call sites already held a context. The stub ignores it: it makes no model call.
81 lines
3.0 KiB
Go
81 lines
3.0 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/kami/maven/internal/llm"
|
||
"github.com/kami/maven/internal/phraser"
|
||
"github.com/kami/maven/internal/router"
|
||
"github.com/kami/maven/internal/voice"
|
||
)
|
||
|
||
// The phrasing itself is tested in internal/phraser. What is left here is the
|
||
// only thing the daemon adds: the stub floor, on the three ways a reply can
|
||
// fail to arrive.
|
||
type stubCompleter struct {
|
||
out string
|
||
err error
|
||
}
|
||
|
||
func (s stubCompleter) Complete(_ context.Context, _ llm.Req) (string, error) { return s.out, s.err }
|
||
|
||
func TestLLMReplierPassesTheModelReplyThrough(t *testing.T) {
|
||
r := newLLMReplier(stubCompleter{out: `{"response":"записала, кофе закончился","mood":"neutral"}`}, nil)
|
||
got := r.Reply(context.Background(), router.Decision{Intent: router.IntentNote, Slots: router.Slots{Text: "кофе закончился"}})
|
||
if got != "записала, кофе закончился" {
|
||
t.Errorf("got %q, want %q", got, "записала, кофе закончился")
|
||
}
|
||
}
|
||
|
||
func TestLLMReplierFallsBackToStubOnError(t *testing.T) {
|
||
r := newLLMReplier(stubCompleter{err: errReplierTest}, nil)
|
||
assertAck(t, r, router.Decision{Intent: router.IntentNote}, phraser.AckNote, "llm error")
|
||
}
|
||
|
||
func TestLLMReplierFallsBackToStubOnEmpty(t *testing.T) {
|
||
r := newLLMReplier(stubCompleter{out: ""}, nil)
|
||
assertAck(t, r, router.Decision{Intent: router.IntentNote}, phraser.AckNote, "empty llm")
|
||
}
|
||
|
||
// A clarify never reaches the model, and since Vikunja #457 it is answered from
|
||
// the clarify deck rather than the stub's single sentence.
|
||
func TestLLMReplierClarifyReadsTheDeck(t *testing.T) {
|
||
r := newLLMReplier(stubCompleter{out: "я всё поняла"}, nil)
|
||
got := r.Reply(context.Background(), router.Decision{Clarify: true, Utterance: "мгм"})
|
||
if got == "я всё поняла" {
|
||
t.Fatal("a clarify must not be phrased by the model")
|
||
}
|
||
if want := clarifyMissedFor("мгм"); got != want {
|
||
t.Errorf("on clarify: got %q, want %q", got, want)
|
||
}
|
||
// Two different misses do not sound identical.
|
||
if same := r.Reply(context.Background(), router.Decision{Clarify: true, Utterance: "а"}); same == got {
|
||
t.Log("two utterances hashed to the same line, which is allowed but should be rare")
|
||
}
|
||
}
|
||
|
||
// assertAck — the stub picks between variants now, so two calls to it are not
|
||
// expected to match. What must hold is that the reply is a line that entry can
|
||
// produce, which is the same claim without pinning one wording.
|
||
func assertAck(t *testing.T, r *llmReplier, d router.Decision, key, what string) {
|
||
t.Helper()
|
||
if got := r.Reply(context.Background(), d); !phraser.IsAck(key, nil, got) {
|
||
t.Errorf("on %s: got %q, want a %q line", what, got, key)
|
||
}
|
||
}
|
||
|
||
func assertStub(t *testing.T, r *llmReplier, d router.Decision, what string) {
|
||
t.Helper()
|
||
got, want := r.Reply(context.Background(), d), voice.NewStubReplier().Reply(context.Background(), d)
|
||
if got != want {
|
||
t.Errorf("on %s: got %q, want stub %q", what, got, want)
|
||
}
|
||
}
|
||
|
||
var errReplierTest = errTest("llm down")
|
||
|
||
type errTest string
|
||
|
||
func (e errTest) Error() string { return string(e) }
|