51256c4c9a
The middle outcome is the whole task: a workstation that is configured and asleep produces a gap, and the resident model is never asked. The parity test compares the bytes PhraseWorld sends the workstation against the bytes PhraseQuery sends the resident model, so the fixtures and the daemon cannot measure two different prompts. The nudge tests cover the silent half from both sides, including the temperature, which is how the workstation would otherwise change how she sounds without anyone deciding to.
165 lines
5.8 KiB
Go
165 lines
5.8 KiB
Go
package phraser
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/llm"
|
|
"github.com/kami/maven/internal/loop"
|
|
)
|
|
|
|
// fakeRemote — a workstation model that is up or down on command, and records
|
|
// what it was asked.
|
|
type fakeRemote struct {
|
|
up bool
|
|
reply string
|
|
err error
|
|
got []llm.Req
|
|
}
|
|
|
|
func (f *fakeRemote) Available() bool { return f.up }
|
|
|
|
func (f *fakeRemote) CompleteRemote(_ context.Context, r llm.Req) (string, error) {
|
|
f.got = append(f.got, r)
|
|
if f.err != nil {
|
|
return "", f.err
|
|
}
|
|
return f.reply, nil
|
|
}
|
|
|
|
// The three outcomes of the naming half, in one place. The middle one is the
|
|
// whole task: a gap he is told about, not an answer from the smaller model.
|
|
func TestPhraseWorldNamesTheGapOnlyWhenThereIsOne(t *testing.T) {
|
|
answer := `{"response": "Небо голубое из-за рэлеевского рассеяния.", "mood": "neutral"}`
|
|
|
|
t.Run("no workstation configured: the resident model answers as today", func(t *testing.T) {
|
|
spy := newPromptSpy(t)
|
|
p := NewLLMPhraserAt(spy.srv.URL, Config{})
|
|
got, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil)
|
|
if err != nil {
|
|
t.Fatalf("PhraseWorld: %v", err)
|
|
}
|
|
if got == "" {
|
|
t.Fatal("no reply from the resident model")
|
|
}
|
|
if len(spy.user) != 1 {
|
|
t.Fatalf("resident model saw %d requests, want 1", len(spy.user))
|
|
}
|
|
})
|
|
|
|
t.Run("workstation up: it answers and the resident model is not asked", func(t *testing.T) {
|
|
spy := newPromptSpy(t)
|
|
p := NewLLMPhraserAt(spy.srv.URL, Config{})
|
|
remote := &fakeRemote{up: true, reply: answer}
|
|
p.UseRemote(remote)
|
|
got, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil)
|
|
if err != nil {
|
|
t.Fatalf("PhraseWorld: %v", err)
|
|
}
|
|
if !strings.Contains(got, "рассеяния") {
|
|
t.Errorf("reply is not the workstation's: %q", got)
|
|
}
|
|
if len(spy.user) != 0 {
|
|
t.Errorf("the resident model was asked %d times, want 0", len(spy.user))
|
|
}
|
|
})
|
|
|
|
t.Run("workstation down: the gap, and nothing invented", func(t *testing.T) {
|
|
spy := newPromptSpy(t)
|
|
p := NewLLMPhraserAt(spy.srv.URL, Config{})
|
|
p.UseRemote(&fakeRemote{up: false})
|
|
got, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil)
|
|
if !errors.Is(err, ErrNoWorldModel) {
|
|
t.Fatalf("err = %v, want ErrNoWorldModel", err)
|
|
}
|
|
if got != "" {
|
|
t.Errorf("got a reply %q with no world model", got)
|
|
}
|
|
if len(spy.user) != 0 {
|
|
t.Errorf("the resident model answered a world question %d times, want 0", len(spy.user))
|
|
}
|
|
})
|
|
|
|
t.Run("workstation errors mid-request: still the gap", func(t *testing.T) {
|
|
spy := newPromptSpy(t)
|
|
p := NewLLMPhraserAt(spy.srv.URL, Config{})
|
|
p.UseRemote(&fakeRemote{up: true, err: errors.New("connection refused")})
|
|
if _, err := p.PhraseWorld(context.Background(), "почему небо голубое", nil); !errors.Is(err, ErrNoWorldModel) {
|
|
t.Fatalf("err = %v, want ErrNoWorldModel", err)
|
|
}
|
|
if len(spy.user) != 0 {
|
|
t.Errorf("the resident model answered a world question %d times, want 0", len(spy.user))
|
|
}
|
|
})
|
|
}
|
|
|
|
// Prompt parity: the workstation model is asked the same question in the same
|
|
// words, or the fixtures measure one thing and the daemon ships another.
|
|
func TestPhraseWorldSendsTheSamePromptsAsPhraseQuery(t *testing.T) {
|
|
spy := newPromptSpy(t)
|
|
resident := NewLLMPhraserAt(spy.srv.URL, Config{})
|
|
if _, err := resident.PhraseQuery(context.Background(), "кто написал войну и мир", []string{"Лев Толстой"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
remote := &fakeRemote{up: true, reply: `{"response": "Толстой.", "mood": "neutral"}`}
|
|
offloaded := NewLLMPhraserAt(spy.srv.URL, Config{})
|
|
offloaded.UseRemote(remote)
|
|
if _, err := offloaded.PhraseWorld(context.Background(), "кто написал войну и мир", []string{"Лев Толстой"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(remote.got) != 1 {
|
|
t.Fatalf("the workstation saw %d requests, want 1", len(remote.got))
|
|
}
|
|
if remote.got[0].System != spy.system[0] {
|
|
t.Errorf("system prompts differ:\nremote: %q\nresident: %q", remote.got[0].System, spy.system[0])
|
|
}
|
|
if remote.got[0].User != spy.user[0] {
|
|
t.Errorf("user prompts differ:\nremote: %q\nresident: %q", remote.got[0].User, spy.user[0])
|
|
}
|
|
}
|
|
|
|
// The silent half. A nudge phrased on the workstation is not news, and one
|
|
// phrased here because the card is busy is not news either — but it must be
|
|
// sampled the same way, or the workstation quietly changes how she sounds.
|
|
func TestNudgePhrasingPrefersTheWorkstationSilently(t *testing.T) {
|
|
spy := newPromptSpy(t)
|
|
p := NewLLMPhraserAt(spy.srv.URL, Config{LLMNudges: true})
|
|
remote := &fakeRemote{up: true, reply: `{"response": "Выпей воды.", "mood": "neutral"}`}
|
|
p.UseRemote(remote)
|
|
|
|
pn, err := p.PhraseNudge(context.Background(), loop.Candidate{Rule: loop.WaterRule(), Severity: loop.Sev1})
|
|
if err != nil {
|
|
t.Fatalf("PhraseNudge: %v", err)
|
|
}
|
|
if pn.Body != "Выпей воды." {
|
|
t.Errorf("body = %q, want the workstation's wording", pn.Body)
|
|
}
|
|
if len(remote.got) != 1 {
|
|
t.Fatalf("the workstation saw %d requests, want 1", len(remote.got))
|
|
}
|
|
if remote.got[0].Temperature != chatTemperature {
|
|
t.Errorf("temperature = %v, want %v (what the resident transport samples at)",
|
|
remote.got[0].Temperature, chatTemperature)
|
|
}
|
|
if len(spy.user) != 0 {
|
|
t.Errorf("the resident model phrased %d nudges, want 0", len(spy.user))
|
|
}
|
|
}
|
|
|
|
func TestNudgePhrasingFallsBackWhenTheCardIsBusy(t *testing.T) {
|
|
spy := newPromptSpy(t)
|
|
p := NewLLMPhraserAt(spy.srv.URL, Config{LLMNudges: true})
|
|
p.UseRemote(&fakeRemote{up: false})
|
|
|
|
if _, err := p.PhraseNudge(context.Background(), loop.Candidate{Rule: loop.WaterRule(), Severity: loop.Sev1}); err != nil {
|
|
t.Fatalf("PhraseNudge: %v", err)
|
|
}
|
|
if len(spy.user) != 1 {
|
|
t.Fatalf("the resident model phrased %d nudges, want 1", len(spy.user))
|
|
}
|
|
}
|