package ipc import ( "context" "errors" "testing" "time" "github.com/kami/maven/internal/audio" ) // The default that matters most for a biometric: on a core that was never // configured with a speaker block, there is no wire path that takes a // voiceprint, and none that lists the ones that might exist. func TestSpeaker_OffUnlessConfigured(t *testing.T) { _, _, cli, _ := newServerWithStore(t) ctx := context.Background() if _, err := cli.EnrollSpeaker(ctx, EnrollSpeakerReq{ID: "kami"}); !errors.Is(err, ErrUnknownMethod) { t.Errorf("EnrollSpeaker error = %v, want ErrUnknownMethod", err) } if _, err := cli.ListSpeakers(ctx); !errors.Is(err, ErrUnknownMethod) { t.Errorf("ListSpeakers error = %v, want ErrUnknownMethod", err) } if err := cli.ForgetSpeaker(ctx, "kami"); !errors.Is(err, ErrUnknownMethod) { t.Errorf("ForgetSpeaker error = %v, want ErrUnknownMethod", err) } } // Enrolment carries several samples across the boundary byte for byte — a // profile averaged over the wrong bytes is a profile of nobody. func TestSpeaker_EnrollCrossesTheWire(t *testing.T) { _, srv, cli, _ := newServerWithStore(t) ctx := context.Background() enrolled := time.Now().UTC().Truncate(time.Second) var gotID, gotName string var gotSamples [][]byte srv.EnrollSpeakerFn = func(_ context.Context, req EnrollSpeakerReq) (EnrollSpeakerResp, error) { gotID, gotName = req.ID, req.Name for _, s := range req.Samples { gotSamples = append(gotSamples, s.Bytes) } return EnrollSpeakerResp{Speaker: Speaker{ ID: req.ID, Name: req.Name, Enrolled: enrolled, Samples: len(req.Samples), }}, nil } mk := func(b byte, n int) audio.Audio { buf := make([]byte, n) for i := range buf { buf[i] = b } return audio.Audio{Format: audio.PCM16kMono, Bytes: buf} } samples := []audio.Audio{mk(1, 64), mk(2, 96), mk(3, 128)} resp, err := cli.EnrollSpeaker(ctx, EnrollSpeakerReq{ID: "kami", Name: "Ками", Samples: samples}) if err != nil { t.Fatalf("EnrollSpeaker: %v", err) } if gotID != "kami" || gotName != "Ками" { t.Errorf("server saw id=%q name=%q", gotID, gotName) } if len(gotSamples) != 3 { t.Fatalf("server saw %d samples, want 3", len(gotSamples)) } for i, want := range samples { if string(gotSamples[i]) != string(want.Bytes) { t.Errorf("sample %d altered in transit", i) } } if resp.Speaker.Samples != 3 || !resp.Speaker.Enrolled.Equal(enrolled) { t.Errorf("profile came back wrong: %+v", resp.Speaker) } } // A listing says who is enrolled and whether recognition actually works. On // this box the honest answer is "enrolled, not recognising", and the response // has to be able to say so — otherwise a surface implies Maven knows who is // talking when nothing on disk can tell. func TestSpeaker_ListReportsDisabledRecognition(t *testing.T) { _, srv, cli, _ := newServerWithStore(t) ctx := context.Background() srv.ListSpeakersFn = func(context.Context) (ListSpeakersResp, error) { return ListSpeakersResp{ Speakers: []Speaker{{ID: "kami", Name: "Ками", Samples: 3}}, Enabled: false, }, nil } resp, err := cli.ListSpeakers(ctx) if err != nil { t.Fatalf("ListSpeakers: %v", err) } if len(resp.Speakers) != 1 || resp.Speakers[0].ID != "kami" { t.Fatalf("speakers = %+v", resp.Speakers) } if resp.Enabled { t.Error("Enabled = true; the seam must be able to report that nothing recognises") } } // Deletion reaches core with the id intact and reports success. This is the // request that must always work. func TestSpeaker_ForgetReachesCore(t *testing.T) { _, srv, cli, _ := newServerWithStore(t) ctx := context.Background() var forgot string srv.ForgetSpeakerFn = func(_ context.Context, req ForgetSpeakerReq) error { forgot = req.ID return nil } if err := cli.ForgetSpeaker(ctx, "гость"); err != nil { t.Fatalf("ForgetSpeaker: %v", err) } if forgot != "гость" { t.Errorf("core forgot %q, want %q", forgot, "гость") } }