Files
OwnCord/Server/ws/can_send_ready_test.go
T
Claude e0ab0744ee feat(client): optimistic message send + composer permission gating
Implements the two highest-impact gaps from the client UX spec.

Optimistic send:
- messages.store gains addOptimisticMessage / markSendFailed /
  removeOptimistic, and confirmSend now stamps the real id + "sent" on
  the ack. addMessage reconciles the broadcast by real id (idempotent,
  replay-safe) with a defensive author match, so an echo never
  duplicates. Message gains status/correlationId/errorCode.
- ChannelController.performSend renders a pending row immediately and
  supports retry / delete-draft (retry preserves attachments).
- MessageList renders pending (dimmed) and failed (reason + Retry /
  Delete) rows; the hover action bar is limited to confirmed rows.
- Failures are precise: the server echoes the request id on error
  replies (buildErrorMsgWithID), so the dispatcher maps SLOW_MODE /
  FORBIDDEN / RATE_LIMITED / BAD_REQUEST to the exact row instead of
  dropping the code. An offline send is shown failed, not silently lost.

Composer permission + connection gating:
- The server computes an authoritative per-channel can_send in the ready
  payload (channelCanSend mirrors MessageService.checkSendPermission:
  READ|SEND, MANAGE_MESSAGES for announcement, admin bypass, channel
  overrides). channels.store carries it as Channel.canSend.
- MessageInput gains a disabled-with-reason mode; ChannelController
  derives the reason from can_send + channel type + connection status and
  disables the composer reactively (announcement read-only, no-permission,
  reconnecting) rather than accepting a click and failing. Older servers
  that omit can_send default permissive.

Docs: the corresponding "Current gap" callouts in docs/architecture/ux
are updated to reflect the implementation.

Verified: full server suite + client tsc + 3204 unit tests + lint + gofmt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 17:47:39 +00:00

47 lines
1.3 KiB
Go

package ws_test
import (
"encoding/json"
"testing"
)
// TestBuildReady_IncludesCanSend confirms every ready channel carries the
// can_send affordance flag the client composer keys off.
func TestBuildReady_IncludesCanSend(t *testing.T) {
hub, database := newCoverageHub(t)
user := seedCoverageOwner(t, database, "cansend-user")
role, err := database.GetRoleByID(1)
if err != nil || role == nil {
t.Fatalf("GetRoleByID: %v", err)
}
if _, err := database.CreateChannel("general", "text", "", "", 0); err != nil {
t.Fatalf("CreateChannel: %v", err)
}
msg, err := hub.BuildReadyWithRoleForTest(database, user.ID, role)
if err != nil {
t.Fatalf("BuildReadyWithRoleForTest: %v", err)
}
var env struct {
Payload struct {
Channels []map[string]any `json:"channels"`
} `json:"payload"`
}
if err := json.Unmarshal(msg, &env); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(env.Payload.Channels) == 0 {
t.Fatal("expected at least one channel")
}
for _, ch := range env.Payload.Channels {
canSend, ok := ch["can_send"]
if !ok {
t.Errorf("channel %v missing can_send", ch["name"])
continue
}
// Owner role → can_send true everywhere.
if canSend != true {
t.Errorf("channel %v can_send = %v, want true for owner", ch["name"], canSend)
}
}
}