Il badge Sherpa dei palloni conta solo i turni confermati, non le proposte

rosa.ts calcolava Giocatore.palloni su completaTurni() (turni salvati + proposte
automatiche di rotazione non ancora confermate da nessuno): un giocatore poteva
avanzare nel badge senza aver mai confermato un turno, solo perché l'algoritmo lo
proponeva per un evento passato. Ora passa a conteggioTurni() solo turniSalvati
(i turni davvero confermati in turni_palloni); completaTurni() resta in uso solo
per la UI di rotazione (TurnoPalloni.tsx, PromemoriaPalloni.tsx). Corregge anche
la StatTile "quante volte hai portato i palloni" nel profilo, che leggeva lo
stesso valore.

Aggiornato palloni-badge.test.ts per verificare il nuovo comportamento (un evento
non confermato non conta più per nessuno) invece del vecchio. docs/modules/
badge.md e palloni.md aggiornati di conseguenza, spostando la voce da "Limiti
noti" a "risolto".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 14:43:58 +02:00
co-authored by Claude Sonnet 5
parent 4fb05748c3
commit d250f41749
4 changed files with 77 additions and 71 deletions
+27 -38
View File
@@ -3,24 +3,26 @@
* `bun test/integration/palloni-badge.test.ts`.
*
* I test unitari (`test/unit/palloni-core.test.ts`, `test/unit/badges.test.ts`) verificano
* `completaTurni()`/`conteggioTurni()` e `statoBadge()` come funzioni pure, con eventi e turni
* costruiti a mano. Qui invece si scrivono eventi e turni veri su `eventi_app`/`turni_palloni`,
* si rileggono via REST con la stessa forma di `fetchTurni()`/`daRiga()`, e si passa il
* risultato attraverso `completaTurni()` → `conteggioTurni()` fino a `statoBadge()` sul badge
* `palloni`: se una colonna cambia nome o la mappatura si rompe, qui il grado del badge torna
* sbagliato anche se i test unitari restano verdi, perché quelli non toccano mai il database.
* `conteggioTurni()` e `statoBadge()` come funzioni pure, con eventi e turni costruiti a mano.
* Qui invece si scrivono eventi e turni veri su `eventi_app`/`turni_palloni`, si rileggono via
* REST con la stessa forma di `fetchTurni()`/`daRiga()`, e si passa il risultato attraverso
* `conteggioTurni()` fino a `statoBadge()` sul badge `palloni`: se una colonna cambia nome o la
* mappatura si rompe, qui il grado del badge torna sbagliato anche se i test unitari restano
* verdi, perché quelli non toccano mai il database.
*
* Copre in particolare un comportamento **documentato ma non ovvio** (`docs/modules/
* palloni.md` § Limiti noti): il conteggio usato dal badge include anche le proposte
* automatiche non ancora confermate da nessuno, non solo i turni salvati esplicitamente. Qui
* lo si dimostra con dati veri, non solo con l'affermazione in doc.
* Verifica in particolare che il badge conti **solo i turni confermati**
* (`Giocatore.palloni` in `rosa.ts` usa `turniSalvati`, non l'output di `completaTurni()`):
* un evento passato senza turno confermato non deve avanzare il badge di nessuno, anche se
* `completaTurni()` (usato altrove, per la UI di rotazione) lo proporrebbe automaticamente a
* qualcuno. Prima di questo fix il badge includeva anche le proposte non confermate — vedi
* `docs/modules/badge.md` § Limiti noti (risolto) per il dettaglio del bug e della correzione.
*
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
* usa id con il prefisso `test-palloni-badge`, che nessun dato vero può avere.
*/
import assert from "node:assert/strict";
import { badgeDefs, statoBadge } from "@/lib/badges";
import { completaTurni, conteggioTurni, type CandidatoTurno } from "@/lib/palloni-core";
import { conteggioTurni } from "@/lib/palloni-core";
import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi";
import { giocatori, type Giocatore } from "@/lib/crapp-data";
import { statoLocale } from "../helpers/locale";
@@ -41,11 +43,6 @@ if (!locale) {
// così restano "passati" (e quindi contati) a prescindere da quando gira il test.
const OGGI = "2099-01-01";
const ROSA: CandidatoTurno[] = [
{ id: "pv1", nome: "Uno" },
{ id: "pv2", nome: "Due" },
];
const rest = (percorso: string, init?: RequestInit) =>
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
...init,
@@ -86,6 +83,7 @@ if (!locale) {
return righe.map(daRiga);
}
/** Rilegge esattamente come `fetchTurni()`: solo i turni confermati, nessuna proposta. */
async function leggiTurniSalvati(): Promise<Record<string, string>> {
const res = await rest(
`turni_palloni?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id`,
@@ -103,12 +101,11 @@ if (!locale) {
try {
await prova(
"il badge palloni passa da bronzo ad argento con turni reali, incluse le proposte non confermate",
"il badge palloni passa da bronzo ad argento con turni reali, ignorando gli eventi mai confermati",
async () => {
// m1-m3: turno confermato esplicitamente a pv1 (3 turni -> soglia bronzo).
// m4: NESSUN turno salvato -> completaTurni() deve proporlo in automatico a chi ha
// fatto meno turni (pv2, a zero) -> conta comunque per il badge di pv2, pur non
// essendo mai stato confermato da nessuno (comportamento documentato in palloni.md).
// m4: NESSUN turno confermato -> non deve contare per nessuno, anche se un algoritmo
// di rotazione (usato altrove per la UI) proporrebbe qualcuno in automatico.
// m5-m7: confermati di nuovo a pv1 (6 turni totali -> soglia argento).
const eventi = [
[`${PREFISSO}-m1`, "2020-01-01", "pv1"],
@@ -129,36 +126,28 @@ if (!locale) {
const salvati = await leggiTurniSalvati();
assert.equal(Object.keys(salvati).length, 6, "6 turni confermati esplicitamente, m4 no");
const turniCompleti = completaTurni(salvati, eventiLetti, ROSA);
assert.equal(
turniCompleti[`${PREFISSO}-m4`],
"pv2",
"m4 senza turno salvato: la proposta automatica sceglie pv2 (meno turni)",
);
const conteggio = conteggioTurni(turniCompleti, eventiLetti, OGGI);
const conteggio = conteggioTurni(salvati, eventiLetti, OGGI);
assert.equal(conteggio["pv1"], 6, "6 turni confermati per pv1");
assert.equal(
conteggio["pv2"],
1,
"pv2 non ha mai confermato nulla, ma la proposta per m4 conta comunque",
conteggio["pv2"] ?? 0,
0,
"pv2 non ha mai confermato nulla: m4 senza conferma non conta per nessuno",
);
const badgePv1 = statoBadge(palloniDef, giocatoreAzzerato(conteggio["pv1"]!));
assert.equal(badgePv1.grado, "argento", "6 turni: soglia argento raggiunta");
const badgePv2 = statoBadge(palloniDef, giocatoreAzzerato(conteggio["pv2"]!));
assert.equal(badgePv2.grado, null, "1 turno (solo proposto): sotto la soglia bronzo (3)");
const badgePv2 = statoBadge(palloniDef, giocatoreAzzerato(conteggio["pv2"] ?? 0));
assert.equal(badgePv2.grado, null, "zero turni confermati: nessun badge");
},
);
await prova("un evento futuro non riceve conteggio, anche se già assegnato", async () => {
// Stesso scenario di sopra ma con "oggi" prima di tutti gli eventi: nessuno dei turni,
// confermati o proposti, deve ancora contare per il badge.
await prova("un evento futuro non riceve conteggio, anche se già confermato", async () => {
// Stesso scenario di sopra ma con "oggi" prima di tutti gli eventi: nessun turno
// confermato deve ancora contare per il badge.
const eventiLetti = await leggiEventi();
const salvati = await leggiTurniSalvati();
const turniCompleti = completaTurni(salvati, eventiLetti, ROSA);
const conteggio = conteggioTurni(turniCompleti, eventiLetti, "2019-01-01");
const conteggio = conteggioTurni(salvati, eventiLetti, "2019-01-01");
assert.deepEqual(conteggio, {}, "tutti gli eventi sono nel futuro rispetto a 'oggi'");
});
} finally {