From dd63180e44b2be4e6b4ffaa9053386b9c6e289ed Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 5 Aug 2026 13:32:35 +0400 Subject: [PATCH] the chat path answers with no llama-server (V-45) Step 4 of the QA list, pinned as a test rather than checked by hand: the deploy has llama-server up and stopping it to look is not available here. Both halves of a turn call the model. The cascade falls to the classifier and the replier falls to the stub, and each was covered separately by a stubbed error value. This wires a real client at a closed port so a dial error walks the whole path, and asserts three utterances still come back with words. Also pins that daemonAPI.Chat errors only when the voice path was never wired, which is what keeps mavweb's /api/chat off its error branch when the model is down. mavweb never returns 500 there in any case: it redirects to /chat. --- cmd/mavend/chat_degrade_test.go | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 cmd/mavend/chat_degrade_test.go diff --git a/cmd/mavend/chat_degrade_test.go b/cmd/mavend/chat_degrade_test.go new file mode 100644 index 0000000..c376aa0 --- /dev/null +++ b/cmd/mavend/chat_degrade_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/kami/maven/internal/llm" + "github.com/kami/maven/internal/router" +) + +// The chat path must answer when llama-server is down (Vikunja #45 step 4). +// Both halves of a turn call the model — the router and the replier — and each +// has its own floor: the cascade falls to the classifier, the replier falls to +// the stub. This wires a client at a closed port so both floors are exercised +// by a dial error rather than by a stubbed error value. +func TestChatAnswersWithNoLlamaServer(t *testing.T) { + h, _, _ := newClarifyHandler(t) + dead := llm.New("http://127.0.0.1:1", 500*time.Millisecond) + emb := router.NewHashEmbedder(1024) + h.recall.embedder = emb + h.router = buildRouter(emb, h.matcher, 0.55, pickLLMRouter(true, dead)) + h.replier = newLLMReplier(dead, nil) + + ctx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web")) + for _, utt := range []string{ + "привет", + "запиши что кофе закончился", + "что у меня сегодня", + } { + reply := h.handleText(ctx, "web", utt) + if reply == "" { + t.Errorf("%q answered with nothing; a dead model must degrade to the stub", utt) + } + } +} + +// daemonAPI.Chat reports an error only when the voice path was never wired. +// A turn that reaches handleText always carries text, which is what keeps +// mavweb's /api/chat off its error branch when the model is down. +func TestChatAPIErrsOnlyWhenUnwired(t *testing.T) { + d := &daemonAPI{} + if _, err := d.Chat(context.Background(), "web", "привет"); err == nil { + t.Fatal("an unwired daemon must say so") + } + d.chatFn = func(context.Context, string, string) string { return "" } + if _, err := d.Chat(context.Background(), "web", "привет"); err != nil { + t.Fatalf("a wired daemon must not error: %v", err) + } +}