fe489dff6d
attentionq.go, repair.go and internal/router/complaint.go carry the last
hand-written Russian patterns of the V-522 sweep, and they live on task/467.
internal/lexicon, internal/morph and cmd/mavend/topics.go live here. One of
the two had to move.
Four conflicts, and one of them is a real collision rather than a mechanical
one. Both branches wrote the narrative stage 0 rule. This side had
NarrativeQueryGrammars, plural, with the rest-of-day rule beside it and the
verb alternation built from the lexicon; task/467 had NarrativeQueryGrammar,
singular, which extracts the topic into Slots.Text, refuses a bare "расскажи",
and excludes the shapes that are chat ("расскажи о себе", "историю на ночь").
Resolved by keeping this side's container and this side's lexicon-built
pattern, and taking every behaviour only the other side had: the topic slot,
the empty-topic refusal, chatNarrativeTopics, and its wiring position after
TaskCaptureGrammar so "запиши" still beats "расскажи".
The rest: queryFeeds keeps task/467's conditional claim (V-474 supersedes the
unconditional one), rank.go keeps Spoken and drops pluralTasksRU because
say.CountWord is the one copy of Russian count agreement, and vendor/ was
re-vendored — the merged modules.txt claimed replaces for nexus and praxis
that neither go.mod has.
Routing fixture 58/82, unchanged from both sides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
2.9 KiB
Go
81 lines
2.9 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(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(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")
|
||
}
|
||
}
|
||
|
||
// 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(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(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) }
|