package vision import ( "bytes" "context" "errors" "image" "image/png" "testing" "github.com/kami/maven/internal/media" ) type fakeProvider struct { reply string err error seen int lastQ string lastDim int } func (f *fakeProvider) Describe(_ context.Context, im media.Image, prompt string) (string, error) { f.seen++ f.lastQ = prompt f.lastDim = im.Width return f.reply, f.err } func pngPayload(t *testing.T, w, h int) []byte { t.Helper() var buf bytes.Buffer if err := png.Encode(&buf, image.NewRGBA(image.Rect(0, 0, w, h))); err != nil { t.Fatal(err) } return buf.Bytes() } func testIntake(t *testing.T, p Provider) (*Intake, *media.Store) { t.Helper() s, err := media.Open(t.TempDir(), 0, 0) if err != nil { t.Fatal(err) } return NewIntake(s, p, 64), s } func TestAcceptStoresThenDescribes(t *testing.T) { fp := &fakeProvider{reply: "кот на подоконнике"} in, store := testIntake(t, fp) res, err := in.Accept(context.Background(), pngPayload(t, 200, 100), "telegram", "кто это?") if err != nil { t.Fatalf("accept: %v", err) } if res.Description != "кот на подоконнике" { t.Errorf("description = %q", res.Description) } if fp.lastQ != "кто это?" { t.Errorf("question not passed through: %q", fp.lastQ) } if fp.lastDim != 64 { t.Errorf("image not downscaled to maxDim: width %d", fp.lastDim) } // The sniffed mime wins over anything a sender claimed. got, _, err := store.Read(res.Blob.ID) if err != nil { t.Fatalf("blob not stored: %v", err) } if got.MIME != "image/png" || got.Source != "telegram" { t.Errorf("blob metadata = %+v", got) } } // The ordering promise: with no vision model on the box — today's real state — // the image is still on disk and the id is still reported, so it can be // described later instead of being lost. func TestAcceptKeepsBlobWhenDescribeFails(t *testing.T) { in, store := testIntake(t, Disabled{}) res, err := in.Accept(context.Background(), pngPayload(t, 32, 32), "web:upload", "") if !errors.Is(err, ErrDisabled) { t.Fatalf("got %v, want ErrDisabled", err) } if res.Blob.ID == "" { t.Fatal("no blob id reported on a description failure") } if _, _, err := store.Read(res.Blob.ID); err != nil { t.Errorf("blob was not kept: %v", err) } } func TestRerunDescribesAStoredBlob(t *testing.T) { fp := &fakeProvider{reply: "текст: ошибка E24"} in, _ := testIntake(t, fp) first, err := in.Accept(context.Background(), pngPayload(t, 40, 40), "telegram", "") if err != nil { t.Fatal(err) } res, err := in.Rerun(context.Background(), first.Blob.ID, "прочитай текст") if err != nil { t.Fatalf("rerun: %v", err) } if res.Description != "текст: ошибка E24" { t.Errorf("description = %q", res.Description) } if fp.lastQ != "прочитай текст" { t.Errorf("new question not used: %q", fp.lastQ) } if fp.seen != 2 { t.Errorf("provider called %d times, want 2", fp.seen) } } func TestRerunRefusesAudioBlob(t *testing.T) { in, store := testIntake(t, &fakeProvider{reply: "x"}) b, err := store.Put(media.KindAudio, "audio/wav", "capture:meeting", []byte("pcm bytes")) if err != nil { t.Fatal(err) } if _, err := in.Rerun(context.Background(), b.ID, ""); err == nil { t.Error("audio blob was accepted as an image") } } func TestRerunUnknownID(t *testing.T) { in, _ := testIntake(t, &fakeProvider{}) if _, err := in.Rerun(context.Background(), "nope", ""); err == nil { t.Error("malformed id accepted") } } func TestAcceptRefusesNonImage(t *testing.T) { in, _ := testIntake(t, &fakeProvider{}) if _, err := in.Accept(context.Background(), []byte("this is a text file"), "web:upload", ""); !errors.Is(err, media.ErrUnsupportedImage) { t.Errorf("got %v, want ErrUnsupportedImage", err) } } func TestNilProviderDegradesToDisabled(t *testing.T) { s, err := media.Open(t.TempDir(), 0, 0) if err != nil { t.Fatal(err) } in := NewIntake(s, nil, 0) if _, err := in.Accept(context.Background(), pngPayload(t, 8, 8), "x", ""); !errors.Is(err, ErrDisabled) { t.Errorf("got %v, want ErrDisabled", err) } }