diff --git a/backend/internal/web/handlers_discussion_test.go b/backend/internal/web/handlers_discussion_test.go
new file mode 100644
index 0000000..799d242
--- /dev/null
+++ b/backend/internal/web/handlers_discussion_test.go
@@ -0,0 +1,287 @@
+package web
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "backend/internal/auth"
+ "backend/internal/db/sqlc"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5/pgtype"
+ "github.com/jackc/pgx/v5/pgxpool"
+)
+
+func newDiscussionTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
+ authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
+ tabloDeps := TablosDeps{Queries: q}
+ router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
+ if err != nil {
+ panic("newDiscussionTestRouter: " + err.Error())
+ }
+ return router
+}
+
+func insertDiscussionTestTablo(t *testing.T, ctx context.Context, q *sqlc.Queries, user sqlc.User, title string) sqlc.Tablo {
+ t.Helper()
+ tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
+ UserID: user.ID,
+ Title: title,
+ Description: pgtype.Text{Valid: false},
+ Color: pgtype.Text{Valid: false},
+ })
+ if err != nil {
+ t.Fatalf("InsertTablo: %v", err)
+ }
+ return tablo
+}
+
+func insertDiscussionTestMessage(t *testing.T, ctx context.Context, pool *pgxpool.Pool, q *sqlc.Queries, tabloID, authorID uuid.UUID, body string, createdAt time.Time) sqlc.DiscussionMessage {
+ t.Helper()
+ msg, err := q.CreateDiscussionMessage(ctx, sqlc.CreateDiscussionMessageParams{
+ TabloID: tabloID,
+ AuthorUserID: authorID,
+ Body: body,
+ })
+ if err != nil {
+ t.Fatalf("CreateDiscussionMessage: %v", err)
+ }
+ if _, err := pool.Exec(ctx, `UPDATE discussion_messages SET created_at = $1, updated_at = $1 WHERE id = $2`, createdAt, msg.ID); err != nil {
+ t.Fatalf("set discussion message created_at: %v", err)
+ }
+ msg.CreatedAt = pgtype.Timestamptz{Time: createdAt, Valid: true}
+ msg.UpdatedAt = pgtype.Timestamptz{Time: createdAt, Valid: true}
+ return msg
+}
+
+func TestDiscussionTabRendersHistoryAndComposer(t *testing.T) {
+ pool, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ ctx := context.Background()
+ q := sqlc.New(pool)
+ store := auth.NewStore(q)
+ router := newDiscussionTestRouter(q, store)
+
+ user := preInsertUser(t, ctx, q, "discussion-history@example.com", "correct-horse-12")
+ tablo := insertDiscussionTestTablo(t, ctx, q, user, "Discussion History Tablo")
+ first := insertDiscussionTestMessage(t, ctx, pool, q, tablo.ID, user.ID, " first", time.Date(2026, 5, 16, 9, 30, 0, 0, time.UTC))
+ second := insertDiscussionTestMessage(t, ctx, pool, q, tablo.ID, user.ID, "Second message", time.Date(2026, 5, 17, 10, 45, 0, 0, time.UTC))
+ sessionCookie := sessionCookieForUser(t, ctx, store, user)
+
+ req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/discussion", nil)
+ req.Header.Set("HX-Request", "true")
+ req.AddCookie(sessionCookie)
+ rec := httptest.NewRecorder()
+ router.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("GET discussion tab status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
+ }
+ body := rec.Body.String()
+ for _, want := range []string{"Discussion", "1 participant", user.Email, "May 16, 2026", "May 17, 2026", "Message", "Write a message...", "Send message"} {
+ if !strings.Contains(body, want) {
+ t.Errorf("discussion tab missing %q; body: %.1000s", want, body)
+ }
+ }
+ if strings.Contains(body, "