From 7fe746173d538d22f809b07a884d0ea8067cd668 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 2 Oct 2026 16:37:49 +0200 Subject: [PATCH] test(web): the upload chat offers and accepts local models only, and fails closed (red; adversarial review F1) Co-Authored-By: Claude Sonnet 5.5 --- .../web/uploads_chat_local_handler_test.go | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 internal/web/uploads_chat_local_handler_test.go diff --git a/internal/web/uploads_chat_local_handler_test.go b/internal/web/uploads_chat_local_handler_test.go new file mode 100644 index 0000000..d7b547a --- /dev/null +++ b/internal/web/uploads_chat_local_handler_test.go @@ -0,0 +1,73 @@ +package web_test + +import ( + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "git.d-ma.be/mathias/tapir/internal/adapters/chat" + "git.d-ma.be/mathias/tapir/internal/web" +) + +// The whole private transcript goes to the model chosen in the form, so the upload chat +// offers and accepts local models only, even when the shared chat service knows a cloud +// one (ADR-035 review). The cloud model here is only a name; no network is involved. + +func newUploadChatAppWithCloud(t *testing.T) (*web.App, *fakeChatter) { + t.Helper() + app, _ := newUploadApp(t, audioOK, 200<<20) + fc := &fakeChatter{ + models: []string{"koala/phi4-mini", "iguana/gemma4-26b", "berget/mistral-small"}, + reply: chat.Reply{Answer: "Anders owns the timber order."}, + } + app.Chat = fc + app.UploadChatMaxChars = 120000 + app.UploadChatModels = []string{"koala/phi4-mini", "iguana/gemma4-26b"} + return app, fc +} + +func TestUploadChatPage_DoesNotOfferACloudModelEvenThoughTheServiceKnowsOne(t *testing.T) { + app, _ := newUploadChatAppWithCloud(t) + id := seedUpload(t, userID, "Roadmap sync", "done", meetingText) + + b := body(t, do(t, app, get("/uploads/"+id+"/chat"))) + + assert.Contains(t, b, "iguana/gemma4-26b") + assert.NotContains(t, b, "berget/mistral-small", "the switcher must not list a model the transcript must not be sent to") +} + +func TestUploadChatPost_AForgedCloudModelIsReplacedByALocalOneBeforeTheTranscriptIsSent(t *testing.T) { + app, fc := newUploadChatAppWithCloud(t) + id := seedUpload(t, userID, "Roadmap sync", "done", meetingText) + + rec := do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{"question": {"Who owns timber?"}, "model": {"berget/mistral-small"}})) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, 1, fc.callCount) + assert.NotEqual(t, "berget/mistral-small", fc.gotReq.Model, "a cloud model reached the chat service with the private transcript") + assert.Contains(t, []string{"koala/phi4-mini", "iguana/gemma4-26b"}, fc.gotReq.Model) +} + +func TestUploadChatPost_ALocalModelThatIsOfferedIsHonoured(t *testing.T) { + app, fc := newUploadChatAppWithCloud(t) + id := seedUpload(t, userID, "Roadmap sync", "done", meetingText) + + rec := do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{"question": {"q"}, "model": {"koala/phi4-mini"}})) + + require.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "koala/phi4-mini", fc.gotReq.Model) +} + +func TestUploadChat_WithNoLocalModelConfiguredNothingIsSentAnywhere(t *testing.T) { + app, fc := newUploadChatAppWithCloud(t) + app.UploadChatModels = nil + id := seedUpload(t, userID, "Roadmap sync", "done", meetingText) + + rec := do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{"question": {"q"}, "model": {"berget/mistral-small"}})) + + require.Equal(t, http.StatusOK, rec.Code) + assert.Zero(t, fc.callCount, "failing closed: no local model offered means no model is called") +}