feat(squads): PromoteToSubCaptain / demote (#358) - #501
Conversation
|
@gvieira18 @hefeus os dois comentários foram tratados (cada thread respondida e resolvida):
Verificação: suite completa verde — 970 testes / 3049 assertions no Pest, pint/rector/phpstan limpos. Podem bater o olho de novo, por favor? |
sirelves
left a comment
There was a problem hiding this comment.
Fui ler com calma porque essa Action mexe em quem manda na squad. Segue o que achei.
Primeiro o que me deixou tranquilo: o lockForUpdate() dentro da transaction resolve corrida de verdade, e o comentário explicando por que o lock fica na linha do subject me poupou uns dez minutos de leitura. O early return quando o role já é o desejado evita evento duplicado na auditoria, que é o tipo de coisa que só dói meses depois. E os testes cobrem os guards do capitão nos dois sentidos.
Tenho dois pontos que queria conversar antes de aprovar.
1. A squad pode ficar sem capitão, e o módulo diz que isso não acontece
O demote() permite Captain -> Member e o handle() permite Captain -> SubCaptain. Entendi que é intencional, está no corpo do PR. Só que o docblock do AssignCaptain diz o contrário:
Vacating the seat is not this Action's job — a captain who leaves becomes an
ExMember(seeMarkExMember), which frees the seat by itself.
Até agora o assento só vagava quando o capitão saía da squad. Com este PR ele pode largar o posto e continuar dentro como membro comum. O índice UNIQUE (squad_id) WHERE role = 'captain' garante no máximo um capitão, nunca pelo menos um.
É isso que a gente quer? Se for, vale atualizar o docblock do AssignCaptain, senão ficam duas explicações se contradizendo no mesmo módulo. Se não for, falta alguém assumir o posto na mesma transação.
2. Sub-capitão pode rebaixar sub-capitão
A SquadPolicy trata Captain e SubCaptain como iguais no canManage(), e a Action só protege o assento do capitão. Então um sub-capitão consegue rebaixar outro sub-capitão para Member. Não achei teste cobrindo esse caso.
É proposital? Se a ideia é que só o capitão ou um admin desfaça uma sub-capitania, dá para resolver com um guard parecido com o que já existe para o assento.
Coisas menores, ignora se discordar
O actionFor() trata tudo que não é Member -> SubCaptain como demote. Funciona, porque transition() é privado e só recebe duas roles, mas um match das quatro transições deixaria explícito e aguentaria uma role nova sem virar evento errado na auditoria.
O handle(), que se chama promoção, aceita rebaixar um capitão para sub-capitão. Funciona, mas o nome esconde.
E uma dúvida de concorrência: o lock é na linha do subject, então esta Action e o AssignCaptain podem rodar ao mesmo tempo em pessoas diferentes. O índice parcial impede dois capitães, mas o que chega no usuário nesse caso é uma QueryException de unique violation. Tem tratamento em algum lugar ou a gente aceita?
Entendo que não. Esse lock de comportamento de "só podemos dar demote em um capitão se na mesma operação elegermos um capitão novo" contradiz o mundo real. Sabemos que, pelo fato das eleições de Imagine que o atual capitão do nosso Squad D (você mesmo, Elves) precisa se afastar das ocupações dele dentro do time, mas o time não entra numa decisão de quem vai ser o substituto ou então nem tem "ninguém a altura" pra ser. Estaríamos criando um deadlock imaginário: nossa implementação impede que algo comum no mundo real aconteça, que é afastar alguém de um cargo e esperar por uma decisão de quem será o novo ocupante da cadeira. Por isso, atualizar a docstring é válido.
Cada squad só pode ter (max.) um capitão e um sub-capitão. A Action permitir que um sub-capitão consiga rebaixar outro sub-capitão na verdade seria apenas self-demote. Mas entendo que faz MUITO mais sentido um capitão rebaixar um sub (ou um admin, caso necessário). Isso é parte do domínio do jogo: existe uma hierarquia, onde o P.S: O próprio BDD da issue já previa isso, então é um erro mesmo. Quanto às "coisas menores", uma por uma:
|
|
Tomei um tempo pra pensar melhor em todos esses pontos. Corrigindo de forma transparente alguns pontos da minha resposta anterior:
As correções estão em Quando puder, pode dar uma revisada de novo? @sirelves |
Closes #358
Parent: #342
What changed
PromoteToSubCaptain::handle()agora registra somenteMember -> SubCaptain.PromoteToSubCaptain::demote()agora registra somenteSubCaptain -> Member.InvalidSquadRoleTransition.MembershipActionexplicitamente; o fallback genérico deactionFor()foi removido.AssignCaptainregistra atribuição/substituição eMarkExMemberpode deixar a cadeira vaga.SubCaptaincontinua permitido, pois não existe requisito de produto nem invariante de schema que limite essa quantidade.SquadPolicy::canManage()continua permitindo as capacidades gerais de capitão/subcapitão; a nova capacidade específica exige capitão ou super-admin.Concurrency
O lock da linha do subject serializa
PromoteToSubCaptaineAssignCaptainquando os dois operam sobre a mesma pessoa. Com a validação estrita de origem, uma promoção atrasada que encontraCaptainfalha comInvalidSquadRoleTransition.Em pessoas diferentes, esta Action não disputa o índice parcial de capitão porque nunca grava
Captain. A corrida real está entre mutações da cadeira de capitão, como doisAssignCaptainconcorrentes ouAssignCaptaincontraMarkExMember. O conserto por lock da squad foi separado em #527.Verification
vendor/bin/pest --compact app-modules/squads/tests/Feature/PromoteToSubCaptainTest.php: 16 testes, 42 assertions.vendor/bin/pest --compact app-modules/squads/tests/Feature/SquadPolicyTest.php: 10 testes, 12 assertions.vendor/bin/pest --compact app-modules/squads/tests: 61 testes, 132 assertions.make test: 987 testes, 3094 assertions.make test-pint: passou.make test-rector: passou, sem alterações.make phpstan: passou, sem erros.git diff --check: passou.