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), nil) 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) } }