package phraser import ( "context" "testing" "github.com/kami/maven/internal/llm" "github.com/kami/maven/internal/router" ) type mockCompleter struct { out string err error } func (m mockCompleter) Complete(_ context.Context, _ llm.Req) (string, error) { return m.out, m.err } func TestReplierReturnsLLMReply(t *testing.T) { r := NewReplier(mockCompleter{out: `{"response":"записала, кофе закончился","mood":"neutral"}`}, nil) got, err := r.PhraseReply(context.Background(), noteDecision()) if err != nil || got != "записала, кофе закончился" { t.Errorf("got %q, %v, want %q, nil", got, err, "записала, кофе закончился") } } func TestReplierFallsBackToPlainText(t *testing.T) { r := NewReplier(mockCompleter{out: "записала, кофе закончился"}, nil) got, err := r.PhraseReply(context.Background(), noteDecision()) if err != nil || got != "записала, кофе закончился" { t.Errorf("got %q, %v, want %q, nil", got, err, "записала, кофе закончился") } } func TestReplierReportsTheModelError(t *testing.T) { r := NewReplier(mockCompleter{err: errTestLLMDown}, nil) got, err := r.PhraseReply(context.Background(), noteDecision()) if err == nil { t.Errorf("got %q, nil error — a dead model must be reported, not phrased around", got) } } // A fragment the grammar left half-open is a failed generation. It must come // back as an error so the daemon reaches its stub, not as a reply. func TestReplierRejectsBrokenJSON(t *testing.T) { r := NewReplier(mockCompleter{out: `{"response":"запис`}, nil) got, err := r.PhraseReply(context.Background(), noteDecision()) if err == nil || got != "" { t.Errorf("got %q, %v, want empty and an error", got, err) } } func TestReplierEmptyOutputIsEmpty(t *testing.T) { r := NewReplier(mockCompleter{out: ""}, nil) got, err := r.PhraseReply(context.Background(), noteDecision()) if err != nil || got != "" { t.Errorf("got %q, %v, want empty and no error", got, err) } } // grammarRecorder captures the request so the grammar can be asserted on. type grammarRecorder struct{ req llm.Req } func (g *grammarRecorder) Complete(_ context.Context, r llm.Req) (string, error) { g.req = r return `{"response":"записала","mood":"neutral"}`, nil } func TestReplierCarriesTheResponseGrammar(t *testing.T) { rec := &grammarRecorder{} r := NewReplier(rec, nil) if _, err := r.PhraseReply(context.Background(), noteDecision()); err != nil { t.Fatalf("PhraseReply: %v", err) } if rec.req.Grammar != ResponseGrammar { t.Errorf("grammar = %q, want ResponseGrammar", rec.req.Grammar) } if rec.req.System != ReplySystemPrompt { t.Errorf("system prompt = %q, want ReplySystemPrompt", rec.req.System) } } func noteDecision() router.Decision { return router.Decision{Intent: router.IntentNote, Slots: router.Slots{Text: "кофе закончился"}} } var errTestLLMDown = errTest("llm down") type errTest string func (e errTest) Error() string { return string(e) }