test(web): chat over an uploaded transcript reuses the ADR-027 path (red)
Owner sees the panel posting to /uploads/{id}/chat; an unfinished upload
redirects to its status; ANOTHER USER'S upload is 404 on GET and POST and no
model is ever called with it; the owner's transcript is sent with the upload
budget (not the 18,000-char caption default); an unoffered model falls back; a
model failure is inline and keeps the conversation; routes are absent without a
chat backend; the done card links to chat only when chat is wired.
Red: App.UploadChatMaxChars is undefined.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
|||||||
|
package web_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chat over an uploaded transcript REUSES the ADR-027 chat path (the same
|
||||||
|
// Chatter, the same panel) — only the lookup differs: the row comes from the
|
||||||
|
// user-scoped private store, so another user's upload is a 404 and, crucially, no
|
||||||
|
// model is ever called with it. Transcript text is synthetic; this repo is public.
|
||||||
|
|
||||||
|
const meetingText = "Anders ansvarar för virket. Maria undersöker leverantörer."
|
||||||
|
|
||||||
|
func newUploadChatApp(t *testing.T) (*web.App, *fakeChatter) {
|
||||||
|
t.Helper()
|
||||||
|
app, _ := newUploadApp(t, audioOK, 200<<20)
|
||||||
|
fc := &fakeChatter{
|
||||||
|
models: []string{"koala/phi4-mini", "iguana/gemma4-26b"},
|
||||||
|
reply: chat.Reply{Answer: "Anders owns the timber order."},
|
||||||
|
}
|
||||||
|
app.Chat = fc
|
||||||
|
app.UploadChatMaxChars = 120000
|
||||||
|
return app, fc
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatPost(path string, kv url.Values) *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(kv.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatPage_OwnerOfAFinishedUploadSeesThePanelPostingToTheUploadRoute(t *testing.T) {
|
||||||
|
app, _ := newUploadChatApp(t)
|
||||||
|
id := seedUpload(t, userID, "Roadmap sync", "done", meetingText)
|
||||||
|
|
||||||
|
rec := do(t, app, get("/uploads/"+id+"/chat"))
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
b := body(t, rec)
|
||||||
|
assert.Contains(t, b, `action="/uploads/`+id+`/chat"`, "the form posts to the UPLOAD route, not /v/{id}/chat")
|
||||||
|
assert.Contains(t, b, "Roadmap sync")
|
||||||
|
assert.Contains(t, b, "koala/phi4-mini")
|
||||||
|
assert.Contains(t, b, "recording", "wording says recording, not video")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatPage_AnUnfinishedUploadRedirectsToItsStatusPage(t *testing.T) {
|
||||||
|
app, fc := newUploadChatApp(t)
|
||||||
|
id := seedUpload(t, userID, "m", "transcribing", "")
|
||||||
|
|
||||||
|
rec := do(t, app, get("/uploads/"+id+"/chat"))
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusSeeOther, rec.Code)
|
||||||
|
assert.Equal(t, "/uploads/"+id, rec.Header().Get("Location"))
|
||||||
|
assert.Zero(t, fc.callCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChat_AnotherUsersUploadIs404AndNoModelIsCalled(t *testing.T) {
|
||||||
|
app, fc := newUploadChatApp(t)
|
||||||
|
theirs := seedUpload(t, otherUserID, "Their private meeting", "done", meetingText)
|
||||||
|
|
||||||
|
getRec := do(t, app, get("/uploads/"+theirs+"/chat"))
|
||||||
|
postRec := do(t, app, chatPost("/uploads/"+theirs+"/chat", url.Values{"question": {"what did they say?"}}))
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, getRec.Code)
|
||||||
|
assert.Equal(t, http.StatusNotFound, postRec.Code)
|
||||||
|
assert.NotContains(t, body(t, getRec), "Their private meeting")
|
||||||
|
assert.Zero(t, fc.callCount, "someone else's transcript must never reach a model")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatPost_SendsTheOwnersTranscriptWithTheUploadBudget(t *testing.T) {
|
||||||
|
app, fc := newUploadChatApp(t)
|
||||||
|
app.UploadChatMaxChars = 77777
|
||||||
|
id := seedUpload(t, userID, "Roadmap sync", "done", meetingText)
|
||||||
|
|
||||||
|
rec := do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{
|
||||||
|
"question": {"Who owns the timber order?"},
|
||||||
|
"model": {"iguana/gemma4-26b"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, 1, fc.callCount)
|
||||||
|
assert.Equal(t, meetingText, fc.gotReq.Transcript)
|
||||||
|
assert.Equal(t, "Who owns the timber order?", fc.gotReq.Question)
|
||||||
|
assert.Equal(t, "iguana/gemma4-26b", fc.gotReq.Model)
|
||||||
|
assert.Equal(t, 77777, fc.gotReq.MaxChars,
|
||||||
|
"an uploaded transcript takes its own budget: the 18,000-char caption default would cut an hour to its first third")
|
||||||
|
assert.Contains(t, body(t, rec), "Anders owns the timber order.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatPost_AnUnofferedModelFallsBackToTheDefault(t *testing.T) {
|
||||||
|
app, fc := newUploadChatApp(t)
|
||||||
|
id := seedUpload(t, userID, "m", "done", meetingText)
|
||||||
|
|
||||||
|
do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{"question": {"q"}, "model": {"evil/model"}}))
|
||||||
|
|
||||||
|
assert.Equal(t, "koala/phi4-mini", fc.gotReq.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatPost_UnfinishedUploadCallsNoModel(t *testing.T) {
|
||||||
|
app, fc := newUploadChatApp(t)
|
||||||
|
id := seedUpload(t, userID, "m", "queued", "")
|
||||||
|
|
||||||
|
rec := do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{"question": {"q"}}))
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusSeeOther, rec.Code)
|
||||||
|
assert.Zero(t, fc.callCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatPost_PriorTurnsRideAlongAndATruncatedReplyIsSaidSo(t *testing.T) {
|
||||||
|
app, fc := newUploadChatApp(t)
|
||||||
|
fc.reply = chat.Reply{Answer: "second answer", Truncated: true}
|
||||||
|
id := seedUpload(t, userID, "m", "done", meetingText)
|
||||||
|
|
||||||
|
rec := do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{
|
||||||
|
"question": {"and who else?"},
|
||||||
|
"hq": {"first question"},
|
||||||
|
"ha": {"first answer"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, []chat.Turn{{Question: "first question", Answer: "first answer"}}, fc.gotReq.History)
|
||||||
|
b := body(t, rec)
|
||||||
|
assert.Contains(t, b, "first answer")
|
||||||
|
assert.Contains(t, b, "second answer")
|
||||||
|
assert.Contains(t, b, "end of the recording", "a long recording cut to a budget must say so, in its own words")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatPost_AModelFailureIsShownInlineNotAs500(t *testing.T) {
|
||||||
|
app, fc := newUploadChatApp(t)
|
||||||
|
fc.err = errors.New("gateway down")
|
||||||
|
id := seedUpload(t, userID, "m", "done", meetingText)
|
||||||
|
|
||||||
|
rec := do(t, app, chatPost("/uploads/"+id+"/chat", url.Values{"question": {"q"}, "hq": {"earlier"}, "ha": {"kept"}}))
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
b := body(t, rec)
|
||||||
|
assert.Contains(t, b, "couldn't answer")
|
||||||
|
assert.Contains(t, b, "kept", "the conversation survives a failed turn")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadChatRoutes_AreNotMountedWithoutAChatBackend(t *testing.T) {
|
||||||
|
app, _ := newUploadApp(t, audioOK, 200<<20)
|
||||||
|
id := seedUpload(t, userID, "m", "done", meetingText)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, do(t, app, get("/uploads/"+id+"/chat")).Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadStatus_DoneOffersTheChatLinkOnlyWhenChatIsWired(t *testing.T) {
|
||||||
|
appWith, _ := newUploadChatApp(t)
|
||||||
|
id := seedUpload(t, userID, "m", "done", meetingText)
|
||||||
|
assert.Contains(t, body(t, do(t, appWith, get("/uploads/"+id+"/status"))), `href="/uploads/`+id+`/chat"`)
|
||||||
|
|
||||||
|
appWithout, _ := newUploadApp(t, audioOK, 200<<20)
|
||||||
|
id2 := seedUpload(t, userID, "m2", "done", meetingText)
|
||||||
|
assert.NotContains(t, body(t, do(t, appWithout, get("/uploads/"+id2+"/status"))), "/chat")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user