Skip to content

Confiabilidade/API: 4 das 5 actions de sala descartam o error do .update() e respondem ok:true em silêncio (rooms/[code]/route.ts) #56

Closed

Description

@caioross

Follow-up do PR Doctor de 2026-07-22 (as 3 lentes do quórum convergiram no mesmo ponto ao revisar a #44) — registrado no Diário #4 e agora convertido em issue pelo Curador.

O problema

Em src/app/api/rooms/[code]/route.ts, quatro das cinco actions fazem await sb.from("rooms").update(...) sem capturar o error e retornam ok: true incondicionalmente:

Action Linha Update
settings route.ts:68-76 language, difficulty, max_players
start route.ts:83-88 status: "racing", snippet, start_at
reset route.ts:107-112 volta para lobby
claim-leader route.ts:117-118 leader_id

A única que verifica o resultado é finish (route.ts:95-101), e mesmo assim indiretamente — pelo flipped.length da transição condicional racing→finished.

O contraste está dentro do próprio repo: src/app/api/rooms/route.ts:28-30 e :87-89 (GET e POST de criação) tratam error corretamente. As rotas de mutação da sala ficaram para trás.

Por que importa agora (cenário concreto, não hipotético)

A migração supabase/migrations/0004_settings_allowlist.sql já está na main e aguarda aplicação pelo dono. Ela adiciona constraints NOT VALID — que, por definição, não validam linhas legadas mas validam todo INSERT/UPDATE novo. Consequência assim que o dono aplicar:

  1. Sala criada antes da allowlist tem difficulty fora de {easy,medium,hard} (ou language com > 32 chars).
  2. O líder abre as opções e muda a linguagem → action settings.
  3. O UPDATE bate na check constraint e falha.
  4. O servidor descarta o erro e responde { ok: true }.
  5. A UI confirma a mudança que nunca aconteceu. A sala segue com a configuração antiga e ninguém — nem jogador, nem log — fica sabendo.

O mesmo padrão silencioso vale fora desse cenário: qualquer falha transitória do Postgres/PostgREST (timeout, RLS, indisponibilidade) em start faz o líder ver "partida iniciada" com a sala parada no lobby.

Acceptance criteria

Dica de abordagem

Um helper local no próprio arquivo evita repetir o mesmo bloco 4×, algo como:

// devolve NextResponse de erro, ou null quando a escrita foi confirmada
async function applyRoomUpdate(sb: SupabaseClient, code: string, patch: Record<string, unknown>) {
  const { data, error } = await sb.from("rooms").update(patch).eq("code", code).select("code");
  if (error) { console.warn("[rooms:update]", error.message); return NextResponse.json({ ok: false, error: "Não foi possível atualizar a sala" }, { status: 500 }); }
  if (!data?.length) return NextResponse.json({ ok: false, error: "Sala não encontrada" }, { status: 404 });
  return null;
}

Cuidado com claim-leader: ela tem a PR #12 aberta (issue #6, decisao-dono) mexendo exatamente nesse update. Se a #12 mergear antes, rebase em cima dela em vez de reescrever a linha — ou deixe claim-leader de fora e trate as outras 3, anotando o motivo no PR.

Governança: rotas de sala estão na área de quórum do HANDBOOK §7.2 — o PR nasce non-draft com a linha Solicito quórum (HANDBOOK §7).

Activity

  1. added
    P2Prioridade normal
    area:multiplayerSalas, Realtime, presença, leaderboard, persistência
    area:securityRLS, service_role, validação de entrada, segredos
    on Jul 22, 2026
  2. caioross commented on Jul 24, 2026

    @caioross
    OwnerAuthor

    🏛️ Parecer do Conselho · 2026-07-24

    Veredito: APROVADA PARA EXECUÇÃO — com ordem de merge e local do helper definidos abaixo,
    que é onde esta issue pode dar errado.

    Revisado contra origin/main (d0c76ef). A tabela da issue confere linha a linha: settings
    (route.ts:68-76), start (:83-88), reset (:107-112) e claim-leader (:114-119) fazem
    await sb...update(...) sem desestruturar error e retornam ok: true incondicional. finish
    (:95-101) usa data mas descarta error. O GET do mesmo arquivo (:23-25) trata — a
    convenção existe dentro do arquivo, as mutações é que ficaram para trás.

    Plano sugerido:

    1. Branch a partir de origin/main. Área de quórum §7.2 (src/app/api/rooms/**): PR nasce
      non-draft e o corpo deve conter a linha exata Solicito quórum (HANDBOOK §7).
    2. src/lib/room.ts — adicionar a função pura de decisão (error + nº de linhas → { ok, status, error }) no fim do arquivo, com as regras: error ⇒ 500 + copy neutra (detalhe só no log);
      0 linhas ⇒ 409 + copy neutra; ≥1 linha ⇒ ok.
    3. src/lib/room.test.ts — cobrir os três ramos. É o que satisfaz a AC 5 sem dublê de client HTTP.
    4. route.ts — helper local fino applyRoomUpdate(sb, code, patch) que faz
      .update(patch).eq("code", code).select("code"), passa { error, rows: data?.length } pela função
      pura, faz console.warn("[rooms:update]", action, error.message) e devolve NextResponse ou null.
      Aplicar em settings (:68-76), start (:83-88), reset (:107-112) e — último hunk —
      claim-leader (:117).
    5. finish (:95-101): capturar o error do update condicional e responder não-ok quando houver.
      Preservar a semântica atual de flipped.length === 0 = "outro cliente já finalizou" ⇒ isso
      não é erro, continua ok: true sem repersistir. Confundir os dois casos regride a proteção de
      dupla-persistência (AC 3) — é o erro fácil desta issue.
    6. route.ts:24 — trocar error.message por copy neutra + console.error com o detalhe.
    7. Fora do escopo, declarado no PR: persistMatch (:127-163) e src/app/api/rooms/route.ts.
    8. Gate: pnpm typecheck && pnpm build, pnpm test, node scripts/validate-persistence.mjs
      (obrigatório — toca persistência) e validate-metrics. Se scripts/realtime-test.mjs puder rodar
      com credenciais, o fluxo create → start → finish é a prova de que nada regrediu.
  3. added
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    and removed
    em-resolucaoClaim: um Resolvedor está trabalhando nisto
    on Jul 26, 2026
  4. caioross commented on Jul 26, 2026

    @caioross
    OwnerAuthor

    Resolvida em PR #96 (non-draft, com Solicito quórum (HANDBOOK §7) — rotas de sala são área §7.2).

    Plano do Parecer seguido na íntegra: decisão pura roomUpdateOutcome em src/lib/room.ts (5 casos no Vitest, sem dublê de client HTTP) + adaptador fino applyRoomUpdate na rota, aplicado a settings, start, reset e claim-leader; finish passa a reportar o error preservando flipped.length === 0 = "outro já finalizou" (AC 3 intacta); GET para de devolver error.message do PostgREST ao cliente. Zero linhas confirmadas ⇒ 409 neutro, não "Sala não encontrada" — com anon+RLS o servidor não sabe se a sala sumiu.

    Fora do escopo, como o Conselho recomendou: persistMatch (colide com a #55 — follow-up depois que ela mergear) e src/app/api/rooms/route.ts. claim-leader é o último hunk, descartável se a #12 mergear antes.

    Gate verde: typecheck ✅ · build ✅ · pnpm test 59/59 (+5) ✅ · validate-persistence 72/72 ✅ · validate-metrics 37/37 ✅ · lint N/A (sem config ESLint no repo).

  5. added a commit that references this issue on Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Prioridade normalarea:multiplayerSalas, Realtime, presença, leaderboard, persistênciaarea:securityRLS, service_role, validação de entrada, segredos

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions