Completa la copertura test del badge Sherpa dei palloni e ne documenta il comportamento
Nessun bug nella logica di calcolo: soglie 3/6/10 corrette, gradoRaggiunto()/ statoBadge() si comportano come per gli altri badge da contatore. Trovato però un comportamento reale ma non ovvio (già accennato in palloni.md, mai dimostrato con dati veri): il conteggio del badge include anche le proposte automatiche di completaTurni() non ancora confermate da nessuno, non solo i turni salvati in turni_palloni. Aggiunti test unit sulle soglie (badges.test.ts) e un nuovo end-to-end (palloni-badge.test.ts) che scrive eventi/turni parzialmente confermati sul database locale e verifica l'intera pipeline fetchTurni() -> completaTurni() -> conteggioTurni() -> statoBadge(), incluso il caso "evento futuro non conta". docs/modules/badge.md aggiornato con la pipeline, la copertura test e il limite noto. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+43
-8
@@ -32,7 +32,17 @@ badge assegnati per voto dai compagni.
|
||||
già pronto: `Giocatore` arriva da `useRosa()` con presenze, palloni, serie, infortuni,
|
||||
ritardi, cacche e media pagelle già calcolati da altri moduli — `badges.ts` si limita a
|
||||
confrontarli con le soglie. Fanno eccezione, con logica propria descritta sotto, il
|
||||
Pagellone, l'MVP e i badge social.
|
||||
Pagellone, lo Sherpa dei palloni, l'MVP e i badge social.
|
||||
- **Badge Sherpa dei palloni** (`palloni`, in `badgeDefs`): `g.palloni` non è un contatore
|
||||
incrementato a ogni evento, ma ricalcolato da `conteggioTurni()` (`palloni-core.ts`) su
|
||||
`Giocatore.palloni` (`rosa.ts`) — meccanismo di turni/rotazione descritto per intero in
|
||||
[palloni.md](palloni.md), non ripetuto qui. Un punto rilevante per il badge, **non ovvio**:
|
||||
il conteggio include anche le proposte automatiche di `completaTurni()` **non ancora
|
||||
confermate** da nessuno (solo per partite/eventi, mai per allenamenti), non solo i turni
|
||||
salvati esplicitamente in `turni_palloni` — un giocatore può ricevere credito per un turno
|
||||
che non ha mai confermato di aver fatto, semplicemente perché l'algoritmo di rotazione lo ha
|
||||
proposto per un evento ormai passato. Conta solo per eventi già trascorsi (`e.data < oggi`,
|
||||
stesso criterio delle presenze).
|
||||
- **Badge Pagellone** (`pagella`, in `badgeDefs`): a differenza degli altri badge da
|
||||
contatore, richiede un numero minimo di voti (`VOTI_MINIMI_PAGELLA = 5`, `badges.ts`) prima
|
||||
che `g.mediaVoto` conti — sotto soglia `valore(g)` è forzato a `0` (badge bloccato), anche
|
||||
@@ -81,7 +91,7 @@ soglia raggiunta o superata (soglie inclusive), oltre l'oro resta oro.
|
||||
| --- | --- | --- | --- |
|
||||
| `mvp` | MVP | partite vinte nettamente al voto MVP dei compagni (`g.mvp`, vedi pipeline sopra) | 1 / 3 / 5 |
|
||||
| `pagella` | Pagellone | media dei voti pagella ricevuti dai compagni a fine partita (`g.mediaVoto`), solo se ne ha ricevuti almeno `VOTI_MINIMI_PAGELLA` (5) | 6.5 / 7.5 / 8.5 |
|
||||
| `palloni` | Sherpa dei palloni | quante volte ti sei incaricato di portare la sacca palloni (`g.palloni`) | 3 / 6 / 10 |
|
||||
| `palloni` | Sherpa dei palloni | quante volte hai fatto (o ti è stato proposto, vedi sopra) il turno palloni (`g.palloni`) | 3 / 6 / 10 |
|
||||
| `presenze` | Presenza fissa | totale presenze a eventi/partite in stagione (`g.presenze`) | 5 / 15 / 30 |
|
||||
| `serie-allenamenti` | Sempre in palestra | allenamenti consecutivi presenti, senza saltarne uno (`g.serieAllenamenti`) | 3 / 6 / 10 |
|
||||
| `serie-conferme` | Risposta lampo | conferme di presenza consecutive date entro 24h dalla convocazione (`g.serieConferme`) | 3 / 8 / 15 |
|
||||
@@ -138,13 +148,16 @@ categoria): nessun bug trovato nella logica di calcolo di nessuno dei 16 badge.
|
||||
- `pagella`: caso critico delle soglie decimali senza arrotondamento per eccesso — 6.4 →
|
||||
nessun grado, 6.5 → bronzo (`badges.test.ts:65-67`); un vero 6.49 non diventa "quasi
|
||||
bronzo". Più la soglia minima di voti (vedi sotto).
|
||||
- `palloni`, `presenze`, `serie-allenamenti`, `serie-conferme`: stessa funzione di soglia già
|
||||
testata a fondo su `mvp`/`pagella`, coperti dagli invarianti generali
|
||||
- `palloni`: soglie 3/6/10 testate esplicitamente (bronzo/argento/oro, confine incluso e
|
||||
oltre l'oro resta oro, `badges.test.ts:94-104`), oltre a un caso di progresso non tondo
|
||||
(5/6 → 83%). Pipeline end-to-end sotto, come `mvp`/`pagella`.
|
||||
- `presenze`, `serie-allenamenti`, `serie-conferme`: stessa funzione di soglia già testata a
|
||||
fondo su `mvp`/`pagella`/`palloni`, coperti dagli invarianti generali
|
||||
(`badges.test.ts:154-159`: soglie crescenti, testi presenti, id unici) e da
|
||||
`collezioneBadge`/`prossimoTraguardo` con valori al massimo (`:119-144`).
|
||||
- Nessun integration dedicato per questi 4: non toccano il database, le statistiche sorgente
|
||||
(`presenze.test.ts`, `palloni-core.test.ts`, ecc.) sono già coperte nei rispettivi moduli.
|
||||
`mvp` e `pagella` fanno eccezione (sotto) perché la loro fonte passa da una tabella di voto.
|
||||
- Nessun integration dedicato per questi 3: non toccano il database, le statistiche sorgente
|
||||
(`presenze.test.ts`, ecc.) sono già coperte nei rispettivi moduli. `mvp`, `pagella` e
|
||||
`palloni` fanno eccezione (sotto) perché la loro fonte passa da una tabella di voto/turni.
|
||||
|
||||
**Badge segreti** — ognuno testato con la propria condizione esatta e il confine appena sotto
|
||||
(`badges.test.ts:77-109`): `s-tiebreak` (mediaVoto 7.9 non basta, serve 8), `s-mai-forfait`
|
||||
@@ -184,6 +197,23 @@ dopo l'analisi che ha trovato il gap "un voto solo sblocca il badge"):
|
||||
tenga il badge bloccato sotto soglia, lo sblocchi al voto minimo con il grado giusto, e
|
||||
applichi le soglie normali sopra soglia.
|
||||
|
||||
**Badge Sherpa dei palloni — pipeline end-to-end e proposte non confermate** (analisi
|
||||
dedicata: nessun bug trovato, ma il comportamento "le proposte contano" — già in
|
||||
`palloni.md` — non era mai stato dimostrato con dati veri):
|
||||
- Unit: `badges.test.ts:93-104` — soglie 3/6/10 (confine incluso, oltre l'oro resta oro) +
|
||||
`palloni-core.test.ts`, già completo prima di questa sessione (`completaTurni()`,
|
||||
`conteggioTurni()`, rotazione bilanciata su un giro completo di partite, allenamenti mai
|
||||
proposti in automatico, turno di un giocatore non più in rosa che non rompe il conteggio).
|
||||
- Integration (`npx supabase start` richiesto):
|
||||
- `scritture.test.ts` — un turno resta uno per evento (l'upsert sostituisce, non aggiunge).
|
||||
- `palloni-badge.test.ts` (nuovo) — end-to-end reale: scrive eventi e turni **solo
|
||||
parzialmente confermati** su `eventi_app`/`turni_palloni`, rilegge via REST come fa
|
||||
`fetchTurni()`/`daRiga()`, passa i dati per `completaTurni()` → `conteggioTurni()` fino a
|
||||
`statoBadge()`: dimostra che un evento passato senza turno confermato riceve comunque una
|
||||
proposta automatica che **conta per il badge di chi viene scelto**, anche se non ha mai
|
||||
confermato nulla; verifica anche che un evento futuro non conti, pur avendo già
|
||||
un'assegnazione.
|
||||
|
||||
**Badge social** — nessuna delle 5 categorie ha logica *propria* nel codice: l'id è solo una
|
||||
chiave di raggruppamento, `conteggioCategoria`/`vincitoreCategoria`/`badgeSocialVinti` sono
|
||||
identici per tutte (`badge-social.ts:107-158`). Testare a fondo 2-3 categorie copre l'intero
|
||||
@@ -201,7 +231,7 @@ meccanismo:
|
||||
| - | --- | --- | --- | --- |
|
||||
| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) |
|
||||
| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) |
|
||||
| 3 | `palloni` | normale | ✅ | non necessario |
|
||||
| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) |
|
||||
| 4 | `presenze` | normale | ✅ | non necessario |
|
||||
| 5 | `serie-allenamenti` | normale | ✅ (limite noto sotto) | non necessario |
|
||||
| 6 | `serie-conferme` | normale | ✅ (limite noto sotto) | non necessario |
|
||||
@@ -243,6 +273,11 @@ Trovati in audit, nessuno bloccante (nessun bug nella logica di calcolo):
|
||||
risposte precedenti a `m9`: su quelle righe la serie è un'approssimazione.
|
||||
- Nessuno storico dei badge sbloccati: se cambiano le soglie o i dati sorgente, un badge già
|
||||
"ottenuto" può sparire o apparire retroattivamente.
|
||||
- **Sherpa dei palloni conta anche le proposte non confermate**: `g.palloni` include i turni
|
||||
che `completaTurni()` propone in automatico per un evento passato senza assegnazione
|
||||
esplicita, non solo quelli confermati in `turni_palloni` — comportamento voluto (documentato
|
||||
in [palloni.md](palloni.md)), dimostrato con dati veri in `palloni-badge.test.ts`, ma non
|
||||
intuitivo: un giocatore può vedere avanzare il badge senza aver mai cliccato nulla.
|
||||
- Notifiche "nuovo badge" solo locali al dispositivo (localStorage), si ripetono cambiando
|
||||
browser o dispositivo.
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Badge Sherpa dei palloni end-to-end contro il database locale:
|
||||
* `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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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 { daRiga, type RigaEvento, type Evento } from "@/lib/eventi";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("badge palloni sul database", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("palloni-badge");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge palloni su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-palloni-badge";
|
||||
const palloniDef = badgeDefs.find((b) => b.id === "palloni")!;
|
||||
// "Oggi" fissato nel futuro: gli eventi scritti sotto sono tutti datati nel passato remoto,
|
||||
// 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,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
async function creaEvento(id: string, data: string) {
|
||||
const res = await rest("eventi_app", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({ id, tipo: "partita", titolo: `Partita ${id}`, data }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function confermaTurno(eventoId: string, giocatoreId: string) {
|
||||
const res = await rest("turni_palloni?on_conflict=evento_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({ evento_id: eventoId, giocatore_id: giocatoreId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su turni_palloni: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
/** Rilegge esattamente come fa l'app: `daRiga()` per gli eventi, una mappa per i turni. */
|
||||
async function leggiEventi(): Promise<Evento[]> {
|
||||
const res = await rest(
|
||||
"eventi_app?id=like." +
|
||||
PREFISSO +
|
||||
"*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il",
|
||||
);
|
||||
const righe = (await res.json()) as RigaEvento[];
|
||||
return righe.map(daRiga);
|
||||
}
|
||||
|
||||
async function leggiTurniSalvati(): Promise<Record<string, string>> {
|
||||
const res = await rest(
|
||||
`turni_palloni?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id`,
|
||||
);
|
||||
const righe = (await res.json()) as Array<{ evento_id: string; giocatore_id: string }>;
|
||||
const mappa: Record<string, string> = {};
|
||||
for (const r of righe) mappa[r.evento_id] = r.giocatore_id;
|
||||
return mappa;
|
||||
}
|
||||
|
||||
/** Un giocatore azzerato, come in `test/unit/badges.test.ts`. */
|
||||
function giocatoreAzzerato(palloni: number): Giocatore {
|
||||
return { ...giocatori[0]!, palloni, mvp: 0, mediaVoto: 0, votiPagella: 0, presenze: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
await prova(
|
||||
"il badge palloni passa da bronzo ad argento con turni reali, incluse le proposte non confermate",
|
||||
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).
|
||||
// m5-m7: confermati di nuovo a pv1 (6 turni totali -> soglia argento).
|
||||
const eventi = [
|
||||
[`${PREFISSO}-m1`, "2020-01-01", "pv1"],
|
||||
[`${PREFISSO}-m2`, "2020-01-02", "pv1"],
|
||||
[`${PREFISSO}-m3`, "2020-01-03", "pv1"],
|
||||
[`${PREFISSO}-m4`, "2020-01-04", null],
|
||||
[`${PREFISSO}-m5`, "2020-01-05", "pv1"],
|
||||
[`${PREFISSO}-m6`, "2020-01-06", "pv1"],
|
||||
[`${PREFISSO}-m7`, "2020-01-07", "pv1"],
|
||||
] as const;
|
||||
|
||||
for (const [id, data] of eventi) await creaEvento(id, data);
|
||||
for (const [id, , giocatore] of eventi) if (giocatore) await confermaTurno(id, giocatore);
|
||||
|
||||
const eventiLetti = await leggiEventi();
|
||||
assert.equal(eventiLetti.length, 7, "tutti gli eventi scritti si rileggono");
|
||||
|
||||
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);
|
||||
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",
|
||||
);
|
||||
|
||||
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)");
|
||||
},
|
||||
);
|
||||
|
||||
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.
|
||||
const eventiLetti = await leggiEventi();
|
||||
const salvati = await leggiTurniSalvati();
|
||||
const turniCompleti = completaTurni(salvati, eventiLetti, ROSA);
|
||||
const conteggio = conteggioTurni(turniCompleti, eventiLetti, "2019-01-01");
|
||||
assert.deepEqual(conteggio, {}, "tutti gli eventi sono nel futuro rispetto a 'oggi'");
|
||||
});
|
||||
} finally {
|
||||
await rest(`turni_palloni?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("palloni-badge");
|
||||
}
|
||||
@@ -90,6 +90,21 @@ assert.equal(
|
||||
"sopra la soglia minima, valgono le normali soglie di grado",
|
||||
);
|
||||
|
||||
// --- palloni: soglie 3 / 6 / 10 -----------------------------------------------
|
||||
const palloniDef = badgeDefs.find((b) => b.id === "palloni")!;
|
||||
assert.equal(gradoRaggiunto(palloniDef, 2), null, "sotto la prima soglia nessun grado");
|
||||
assert.equal(gradoRaggiunto(palloniDef, 3), "bronzo", "la soglia è inclusiva");
|
||||
assert.equal(gradoRaggiunto(palloniDef, 5), "bronzo");
|
||||
assert.equal(gradoRaggiunto(palloniDef, 6), "argento");
|
||||
assert.equal(gradoRaggiunto(palloniDef, 9), "argento");
|
||||
assert.equal(gradoRaggiunto(palloniDef, 10), "oro");
|
||||
assert.equal(gradoRaggiunto(palloniDef, 99), "oro", "oltre l'oro resta oro");
|
||||
assert.equal(
|
||||
statoBadge(palloniDef, g({ palloni: 5 })).progresso,
|
||||
83,
|
||||
"5 turni su 6 per l'argento: 83%",
|
||||
);
|
||||
|
||||
// --- badgeGiocatore ----------------------------------------------------------
|
||||
assert.equal(badgeGiocatore(g()).length, badgeDefs.length, "i badge normali sono sempre tutti");
|
||||
assert.ok(
|
||||
|
||||
Reference in New Issue
Block a user