19242ef73b
Every clarify turn said one sentence per gap, and a re-ask repeated it word for word. A question he already failed to answer is the worst one to ask again unchanged: the second wording is what tells him which part she missed. clarifytemplates.go holds three wordings per slot, picked by attempt rather than at random — short first, then naming the gap, then spelling it out with an example. Past the end she keeps the most explicit one instead of wrapping back to the short question he has already not answered. The intents with nothing identifiable to ask about (note, query, chat, system) kept the stub's single "не совсем поняла — можешь переформулировать?", which is the line he hears whenever she misses him completely. Four wordings now, picked by a hash of the utterance so one question asked twice reads the same and two different misses do not. Still no model call on this path: the resident model would wander, and this text has to be right every time. No schema_version either, unlike the nudge templates — these are Go constants, so no file can drift out of step with the code that reads it. The persona test already in clarify_test.go covers the new lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
70 lines
2.4 KiB
Go
70 lines
2.4 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/kami/maven/internal/llm"
|
||
"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(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)
|
||
assertStub(t, r, router.Decision{Intent: router.IntentNote}, "llm error")
|
||
}
|
||
|
||
func TestLLMReplierFallsBackToStubOnEmpty(t *testing.T) {
|
||
r := newLLMReplier(stubCompleter{out: ""}, nil)
|
||
assertStub(t, r, router.Decision{Intent: router.IntentNote}, "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(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(router.Decision{Clarify: true, Utterance: "а"}); same == got {
|
||
t.Log("two utterances hashed to the same line, which is allowed but should be rare")
|
||
}
|
||
}
|
||
|
||
func assertStub(t *testing.T, r *llmReplier, d router.Decision, what string) {
|
||
t.Helper()
|
||
got, want := r.Reply(d), voice.NewStubReplier().Reply(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) }
|