Assistente indipendente per Pegaso, Mercatorum e San Raffaele
Snapshot di riferimento del capitolo
Questo capitolo usa il candidato PlumePilot 2.35.2, branchfeat/ui-ux-2.35.2, commit
f6fcb4c29c83ba9150df0be31da04fc91dffb634.
Un bug corretto oggi può tornare domani.
Un bug corretto e trasformato in:
riproduzione
+ test
+ invariante
diventa invece conoscenza permanente del progetto.
Questa è una delle differenze più importanti fra:
"far funzionare il codice"
e:
"costruire un sistema che continua a funzionare mentre cambia"
PlumePilot è un buon laboratorio perché molte delle sue feature più robuste sono nate da regressioni reali:
accordion chiusi
player riutilizzato
master index stale
display_order ripetuti
tab nascosto
builder duplicato
collisioni fra piattaforme
layout popup differenti
vendor code rifiutato dagli store
Ogni caso ci obbliga a fare una domanda più precisa di:
“Dov’è il bug?”
La domanda utile è:
“Quale assunzione del nostro modello era falsa?”
E, subito dopo:
“Come facciamo a impedire che la stessa assunzione torni inosservata?”
Il ciclo ideale è:
sintomo
↓
riproduzione
↓
osservazione
↓
ipotesi
↓
esperimento
↓
root cause
↓
fix minimo
↓
regression test
↓
invariante
Molti processi si fermano a:
fix minimo
ed è lì che nasce il debito.
Consideriamo:
"l'autoplay si ferma quando chiudo l'accordion"
Una possibile reazione superficiale:
riapri l'accordion
Ma il problema profondo era:
l'autoplay aveva implicitamente legato
l'identità logica della lezione
alla presenza corrente della sua riga DOM
Quindi la root cause non era:
accordion chiuso
ma:
DOM temporaneo usato come identità durevole
Il fix architetturale diventa:
remember logical identity
↓
await
↓
re-resolve fresh DOM row
↓
validate
Il regression test deve proteggere questa proprietà, non soltanto un click specifico.
Possiamo descriverlo così:
modello mentale
↓ predice
comportamento atteso
↓ confronta
comportamento reale
Se divergono:
il modello è incompleto
o
l'implementazione viola il modello
Il debugger serve a capire quale delle due.
Modello iniziale:
master percentage == 100
↓
capitolo completo
Esperienza reale:
master == 100
ma
attività ancora incompleta
Nuovo modello:
master percentage
=
euristica / indice
detail response
=
evidenza più forte
visual fallback
=
ultima verifica locale
Il bug ha raffinato l’architettura.
Un bug difficilmente riproducibile produce fix fragili.
Una buona riproduzione definisce:
precondizioni
azioni
timing
stato iniziale
risultato atteso
risultato reale
Esempio:
dato:
- capitolo 1 corrente
- ultimo video completo
- primi capitoli successivi completi
- capitolo 5 con video incompleto
- "Salta videolezioni completate" attivo
quando:
- termina il video corrente
allora:
- apri direttamente il target nel capitolo 5
non:
- aprire visivamente capitoli 2, 3 e 4
Questa descrizione è già quasi un test.
Se il bug appare in un corso di:
80 capitoli
300 video
40 test
cerchiamo il minimo modello che conserva la failure:
chapter 1 complete
chapter 2 complete
chapter 3 incomplete
Un minimal reproducer riduce il search space del debugging.
Per una browser extension, le variabili possono essere:
browser
platform
pagina
tab visibility
storage preesistente
autoplay settings
operation running
DOM open/closed
network response
course identity
Cambiarne molte insieme confonde la diagnosi.
Se il bug appare solo in Chrome:
prima proviamo:
stesso corso
stesso account
stesse preferenze
stessa versione
stessa attività
e cambiamo solo il browser.
Se appare solo in background:
manteniamo il resto uguale e cambiamo:
visible ↔ hidden
L’obiettivo è trovare la variabile causale.
Abbiamo almeno:
popup
background
bridge
MAIN-world content
ISOLATED-world content
floating menu
builder pages
generated offline HTML
Un console.log() nel contesto sbagliato può dirci:
niente
anche quando il codice funziona.
Se stiamo debuggando:
chrome.storage.onChanged
non ha senso guardare soltanto la console MAIN-world della pagina.
Se stiamo debuggando:
DOM LMS
non basta la console del service worker.
Il primo passo è sempre identificare:
execution context
Due script possono vedere lo stesso DOM ma non lo stesso:
window variables
monkey patch
function identity
Questo è essenziale quando debugghiamo:
fetch interception
window.postMessage
bridge
Supponiamo:
commission-interceptor.js
pubblichi un evento MAIN-world.
Il bridge isolated non lo inoltra.
La domanda non è:
"l'API ha risposto?"
ma:
1. request partita?
2. interceptor l'ha vista?
3. postMessage partito?
4. bridge ha ricevuto?
5. runtime message partito?
6. background ha persistito?
Stiamo seguendo una pipeline.
Per un flusso:
A → B → C → D
loggare soltanto in D è debole.
Meglio osservare i boundary:
A emitted request X
B received request X
B emitted response X
C forwarded X
D stored X
Qui diventano utili:
requestId
operationId
sourceTabId
courseCode
lessonNumber
Il Capitolo 2 aveva già incontrato:
requestId
nel mini-RPC.
Nel debugging il suo valore è enorme.
Senza correlation:
response arrivata
Con correlation:
response per request studywing-...-17
Possiamo distinguere operazioni concorrenti.
Log debole:
here
test
step 2
Log utile:
Course index: module 5 is 100%; skipping detailed API call
contiene:
decisione
identità
motivazione
Se il problema è routing:
course
lesson number
lp_id
paragraph id
master order
sono utili.
Il colore CSS del bottone no.
L’instrumentation deve seguire l’ipotesi.
Workflow debole:
cambia timeout
riprova
cambia selector
riprova
aggiungi sleep
riprova
Workflow migliore:
ipotesi:
la riga DOM viene sostituita durante waitForLessonCompletion
predizione:
l'oggetto row precedente non sarà più === alla riga nuova
esperimento:
salva identity, attesa, query fresca
risultato:
nuova row con stessa identità logica
Ora il fix ha una ragione.
Nel repository troviamo test come:
autoplay-collapsed-followups.mjs
first-incomplete-activities.mjs
test-hidden-tab-recovery.mjs
menu-ux.mjs
test-html-notes.mjs
multiversity-storage-isolation.mjs
epub-regions.mjs
release-candidate-*.mjs
Non fanno tutti la stessa cosa.
Possiamo dividerli in:
behavioral unit-ish tests
source invariant tests
contract tests
fixture/golden tests
browser integration checks
artifact/release validation
benchmarks
manual smoke tests
Questa tassonomia è più utile della parola generica:
test
node:assert/strictMolti test PlumePilot partono con:
import assert from "node:assert/strict";
e usano direttamente:
assert.equal(...)
assert.deepEqual(...)
assert.match(...)
assert.ok(...)
Non c’è bisogno di Jest per ogni problema.
Node documenta node:assert come modulo stabile per verificare invarianti.
strictCon:
strict equality
deep strict equality
evitiamo coercizioni sorprendenti.
In un test di identity:
1
e:
"1"
non devono essere equivalenti per accidente.
node:vm come piccolo laboratorioMolti test fanno:
const source = readFileSync(
new URL("../content.js", import.meta.url),
"utf8"
);
const context = vm.createContext({
enabled: true,
...
});
vm.runInContext(
extract("async function ...", "function ..."),
context
);
Questo è un pattern particolare.
Possiamo eseguire una porzione del codice production fornendo global controllati:
fake DOM
fake timers
fake API
fake storage
fake current chapter
fake video
quindi:
production function
↓
controlled universe
Normalmente potremmo scrivere:
function advance({
currentLesson,
recoverPlaybackLesson,
sleep,
...
}) { ... }
e passare esplicitamente le dipendenze.
Il codice attuale usa molte dipendenze di modulo/global.
vm.createContext() permette al test di iniettarle comunque.
È una forma pragmatica di:
dependency injection at test time.
vm non è una sandbox di sicurezzaNode documenta esplicitamente che:
node:vm
permette di eseguire codice in V8 contexts separati, ma:
non è un security mechanism
In PlumePilot non stiamo eseguendo codice ostile.
Stiamo costruendo ambienti di test controllati.
In:
tests/autoplay-collapsed-followups.mjs
abbiamo:
currentLesson: () => null
per simulare:
entrambi gli accordion chiusi
mentre:
recoverPlaybackLesson:
async () =>
++recoveries === 1
? firstRow
: restoredRow
simula una riga DOM ricostruita.
Non serve implementare l’intera piattaforma.
Ci interessa una proprietà:
dopo l'await la row vecchia non è più affidabile
Il fake simula esattamente quel fatto.
Questo è un buon test double.
Il test afferma:
assert.equal(
nextRowReceived,
restoredRow,
"advance must use the re-rendered lesson row"
);
Questa frase è quasi una specifica.
Non dice:
chiama funzione X
Dice:
usa la riga ricostruita
Subito dopo:
assert.equal(
nextRowReceived,
null,
"a different active lesson must not be advanced"
);
Un regression test forte include spesso:
positive path
+
negative guard
Perché il fix:
riapri sempre e continua
avrebbe potuto sovrascrivere una scelta manuale dell’utente.
Nel test:
setTimeout: (fn) => {
pendingTimer = fn;
return 1;
}
Non aspettiamo davvero.
Conserviamo il callback.
Poi:
await pendingTimer();
Questo ci permette di decidere precisamente:
cosa cambia prima che il timer scatti
Per esempio:
schedule resume
↓
utente avvia di nuovo il video
↓
esegui timer
↓
non deve avanzare
Una race temporale diventa un test deterministico.
<video>, contenuto diversoLa piattaforma può riutilizzare lo stesso elemento video.
Il test crea:
const reused = {
ended: true,
duration: 60,
currentTime: 60,
addEventListener(...)
};
poi simula:
ended
play
ended
sullo stesso oggetto.
ogni video reale terminato
deve poter generare un nuovo advance
senza assumere:
nuovo elemento DOM
Il regression test protegge esattamente questa semantica.
Un tab nascosto può subire:
timer clamping
rendering sospeso
DOM non aggiornato
Un wait può scadere.
Se interpretiamo il timeout come:
pagina rotta
potremmo fare reload inutili.
test-hidden-tab-recovery.mjs definisce:
let now = 0;
Date: {
now: () => now
}
e:
sleep = async (ms) => {
now += ms;
...
};
Nessun secondo reale deve passare.
Il tempo è parte dell’input del test.
Il test parte:
hidden
poi a now >= 350:
visible
ed emette:
visibilitychange
La proprietà verificata:
assert.equal(
result.rendered,
true,
"DOM waits must resume after the tab is visible"
);
assert.equal(
reloads,
0,
"chapter recovery must not reload a hidden page"
);
Questa è una safety property:
hidden page
→ no destructive recovery
“Non deve succedere qualcosa di sbagliato.”
Esempio:
non reloadare hidden tab
non avanzare una lezione diversa
non rispondere automaticamente a un test nel bookmark
“Prima o poi deve succedere qualcosa di corretto.”
Esempio:
quando il tab torna visibile
il wait deve poter proseguire
Un sistema può essere safe ma bloccato.
Oppure live ma pericoloso.
Servono entrambi.
first-incomplete-activities.mjs costruisce:
outline
master index
detail response
visible activity
fallback state
e combina casi.
Questo non è soltanto un test UI.
È un test dell’algoritmo di discovery.
Per:
chapter 2 pending
il test pretende:
assert.equal(
result.entry.lessonNumber,
2
);
e inoltre:
assert.deepEqual(
requested.map(args => args[1]),
[1, 2]
);
Quindi verifica:
correttezza del risultato
+
costo/ordine della ricerca
Con master:
chapter 4 = 80%
others = 100%
il test richiede:
requested === [4]
Non basta trovare capitolo 4.
Deve usare l’indice per:
non interrogare inutilmente 1,2,3
Questo è un performance invariant.
Se chapter 4 si rivela completo ma chapter 1 è in realtà incompleto:
requested === [4, 1]
La suite protegge quindi contemporaneamente:
fast path
+
fallback completeness
La suite include:
master all 100%
master stale
ambiguous identity
API failed
missing test metadata
unknown objective percentage
current visible activity
navigation during discovery
Questi sono adversarial cases.
Non nel senso security.
Nel senso:
input che mette in crisi le nostre assunzioni
Un pattern importante:
percentage = 100
percentage = 0
percentageKnown = false
Il test verifica che:
unknown
non venga interpretato arbitrariamente come:
complete
o:
incomplete
Questo protegge il modello introdotto nel Capitolo 8.
display_orderplayback-master-routing.mjs costruisce:
Section A:
1 Alpha
2 Beta
Section B:
1 Gamma
2 Delta
Quindi:
display_order
si ripete.
La suite verifica:
assert.equal(
playbackCourseRouteMap(ambiguous, outline).size,
0
);
Se non abbiamo abbastanza evidenza:
non mappiamo
anziché:
indovinare
È un test di fail-closed identity matching.
Esempi reali:
non saltare video durante replay
non aprire capitoli intermedi
non submit test dal bookmark
non reloadare hidden tab
non usare route ambigua
non nascondere cancel control
Questi casi proteggono side effect pericolosi.
Con il supporto a più università nasce il rischio:
exam_id 7 Pegaso
exam_id 7 Mercatorum
Se usiamo soltanto:
7
come identity:
collisione
multiversity-storage-isolation.mjs salva simultaneamente:
pegaso: exam_id 7
mercatorum: exam_id 7
e poi pretende entrambe.
Questo è un collision test.
Il test usa:
await Promise.all([
storePegaso(),
storeMercatorum()
]);
Quindi non verifica soltanto l’identità.
Verifica anche che la serializzazione delle write non perda uno dei due update concorrenti.
La stessa suite verifica che:
SAME
senza platform prefix venga ancora interpretato come legacy Pegaso.
Ma non come Mercatorum.
Questo è un test di migration semantics.
Una migrazione non deve soltanto:
leggere nuovo formato
Deve definire:
cosa significa il vecchio formato
Il test lo rende esplicito.
ui-mode-invariants.mjs contiene assertion come:
assert.match(
popupHtmlSource,
/id="chapterLimitValue" .../
);
oppure:
assert.ok(
contentSource.indexOf("if (thresholdReached)") <
contentSource.indexOf("if (sessionLimitReached)")
);
Questi sono test sul sorgente.
Sono ottimi per invarianti strutturali semplici:
asset incluso nel manifest
ordine di due guardie
elemento presente
default dichiarato
file caricato prima di un altro
CSS rule esistente
Costano pochissimo.
Questo test:
assert.match(
source,
/someFunction/
);
dimostra:
la stringa esiste
non:
la funzione funziona
Perciò i source tests devono integrare, non sostituire, i behavioral tests.
In menu-ux.mjs troviamo:
assert.ok(
scripts.indexOf("menu-ux.js") <
scripts.indexOf("floating-menu.js")
);
Qui l’ordine di caricamento è un contratto architetturale.
Il test sul manifest è perfettamente appropriato.
Possiamo chiamare questa categoria:
architecture tests.
Verificano cose come:
dipendenza A prima di B
layer X non importa Y
manifest contiene asset
shared module presente in entrambe le UI
Non cercano di simulare l’utente.
Proteggono la forma del sistema.
test-html-notes.mjs fa una cosa interessante.
Esegue:
test-builder.js
con:
fake document
fake storage
fake job
poi clicca logicamente:
downloadHtml
e cattura il Blob.
Il test apre il testo del Blob e controlla:
note key
toolbar
review filters
theme
Unicode
signature
sanitization calls
Questo è più forte di testare soltanto:
funzione generateHtml()
perché osserva l’artefatto finale.
La suite estrae:
<script> ... </script>
e fa:
new Function(script);
Qui l’obiettivo non è eseguire codice remoto.
È usare il parser JS locale come syntax check del documento generato.
Un fake DOM non ci dirà realmente:
font caricato?
focus visibile?
scroll corretto?
Shadow DOM layout?
checkbox accent?
clipping?
animation?
Per questo la 2.35.2 include:
scripts/check-menu-ux-browser.mjs
che esegue popup e floating controller in Chromium con uno store extension mock condiviso.
Questo livello verifica casi come:
Back / Escape
focus restoration
settings sync
layout sync
hidden active operation
font loading
disclosure scrolling
runtime exceptions
Sono proprietà difficili da validare con sole regex.
La documentazione Chrome sui test end-to-end consiglia, quando possibile, di basare gli integration test su ciò che è visibile all’utente invece di legarsi troppo allo stato interno.
Perché?
Se testiamo:
implementation detail
ogni refactor rompe la suite anche se il comportamento utente è identico.
Meglio verificare:
clic → risultato visibile
setting → UI sincronizzata
operazione → cancel disponibile
piuttosto che:
variabile interna foo === 3
salvo quando quella variabile rappresenta un contract importante.
Chrome documenta un dettaglio molto importante:
aprire DevTools sul service worker
può mantenerlo attivo
Quindi un bug di lifecycle può sparire mentre lo stiamo osservando.
È un classico observer effect nel debugging.
Una feature che dipende accidentalmente da:
global memory del service worker
può sembrare stabile durante il debug.
Poi fallire in produzione quando il worker viene terminato.
Chrome documenta anche come terminare il service worker durante un test Puppeteer e verificare che l’estensione continui a rispondere.
Per PlumePilot sarebbe un futuro test molto utile per:
operation recovery
sound state
builder ownership
commission leases
Un test flaky:
stesso codice
stesso input
→ a volte passa
→ a volte fallisce
È particolarmente pericoloso nei sistemi asincroni.
sleep reale
network reale
DOM timing
animation
service worker lifetime
focus
background tab throttling
ordine eventi
Come visto:
Date.now controllato
sleep simulato
timer callback catturato
Le detail request diventano funzioni deterministiche:
requestLessonWithRetry: async (...) => ({
ok: true,
data: ...
});
Costruiamo solo:
row
video
chapter
document visibility
necessari all’invariante.
Questa è una forma di hermetic-ish testing.
Se tutti i test usano fake perfetti:
produzione può fallire
perché il browser vero è diverso
Quindi servono livelli complementari:
fast deterministic tests
+
real browser checks
+
manual exploratory smoke
E2E
integration
unit
molti unit test, pochi E2E.
Abbiamo tante boundary esterne:
browser
DOM host
API host
storage
manifest
store
Una forma più realistica è:
pure/VM behavioral tests ← molti
architecture/contracts ← molti
browser fixture tests ← alcuni
artifact validation ← sempre
live platform smoke ← pochi ma indispensabili
Non è una piramide perfetta.
Per autoplay:
completed / incomplete
skip on / off
test policy
accordion open / closed
same / next section
master accurate / stale
visible / hidden
Non possiamo testare il prodotto cartesiano completo.
Cerchiamo combinazioni che coprano interazioni importanti.
Esempio:
skip enabled
+
next chapter complete
+
target chapter incomplete
ha scoperto una regressione reale.
Per una state machine non ci interessa soltanto:
line coverage
ma:
transition coverage
Esempio:
WATCHING → VERIFYING → NEXT_VIDEO
WATCHING → VERIFYING → TEST_STOP
WATCHING → VERIFYING → NEXT_CHAPTER
WATCHING → VERIFYING → THRESHOLD_STOP
Ogni guard critica dovrebbe avere:
true case
false case
Per esempio:
busy = false
busy = true
oppure:
v === lastVideo
v !== lastVideo
Una funzione può essere eseguita interamente senza testare:
ordine eventiidentity collision
stale state
race
negative side effect
Quindi la domanda migliore è:
Quale proprietà del sistema abbiamo dimostrato?
Esempio reale:
"master 100% is not evidence
that every activity is complete"
Questo non è soltanto utile quando fallisce.
È documentazione dell’architettura.
Preferiamo:
hidden DOM timeout must not trigger reload
a:
test case 7
Fra sei mesi, il primo spiega perché il codice “strano” esiste.
Per comprendere un workaround possiamo triangolare:
CHANGELOG
test
production code
Il changelog racconta:
perché
Il test:
cosa non deve regredire
Il source:
come lo implementiamo
Questa è una tecnica potente nei progetti longevi.
Se:
v2.34 funziona
v2.35 fallisce
Git ci permette di restringere il range.
git bisect automatizza la ricerca binaria fra commit good/bad.
Anche senza usarlo letteralmente, il modello è:
history search space
↓ dimezza
candidate range
Se abbiamo un comando:
node tests/foo.mjs
possiamo usarlo come oracle:
good / bad
e trovare il commit che introduce la regressione.
Esempio hidden-tab:
timer clamping
+
DOM non aggiornato
+
timeout interpretato come failure
+
reload recovery aggressivo
Qual è la root cause?
Spesso è più utile dire:
root design flaw:
timeout was treated as proof of page failure
contributing factors:
hidden tab scheduling + rendering
Perché autoplay ha aperto il capitolo sbagliato?
Perché route map ha associato display_order = 1.
Perché era ambiguo?
Perché display_order ricomincia in ogni folder.
Perché lo trattavamo come globale?
Perché avevamo dedotto identity da un campo presentazionale.
Perché non avevamo un secondo controllo?
Perché il primo supportava un solo layout.
Quale invariante manca?
non usare una label locale come identity globale
Ora il test può proteggerla.
Possiamo aggiungere:
console.log(allDOMNodes)
per una sessione.
Poi rimuoverlo.
Può valere la pena mantenere:
operationId
lessonNumber
fallback reason
retry count
perché descrive eventi di dominio utili anche in futuro.
Una extension opera su:
contenuto universitario
sessioni autenticate
esami
Un log deve evitare:
token
credential
raw private payload non necessario
La diagnostica deve essere:
minima
sanitizzata
locale
Confronta:
Request failed
con:
API resume discovery could not read module 5;
opening that module directly for visual verification
Il secondo comunica:
dove
cosa
fallback scelto
L’utente ha bisogno di:
"Impossibile leggere questo capitolo.
Provo la verifica visuale."
Il developer può aver bisogno di:
lesson 5
error LESSON_DATA_INCOMPLETE
lpId 104
paragraphId 204
Non serve mostrare tutto all’utente.
Se scriviamo:
API fails → DOM fallback
dobbiamo testare:
API fails davvero
fallback selezionato
fallback produce risultato
Altrimenti il fallback può marcire inosservato.
if (args[1] === failedChapter) {
return {
ok: false,
error: "LESSON_DATA_INCOMPLETE"
};
}
Questa è failure injection.
Non aspettiamo che l’API reale fallisca casualmente.
Costruiamo il failure state.
assert.equal(
failed.result.visualVerification,
true
);
Il test non pretende:
nessun errore
Pretende:
errore gestito correttamente
Questa è resilienza testata.
Una Promise abortita non è semplicemente una Promise fallita.
I test dovrebbero distinguere:
failure
cancel
timeout
Il Capitolo 5 aveva già separato queste semantiche.
Per operazioni lunghe è utile testare:
quanto spesso controlliamo abort signal?
Per questo l’EPUB inserisce:
throwIfAborted()
yieldToBrowser()
in vari punti.
Un futuro test potrebbe imporre abort durante:
render
encode
regional analysis
ZIP preparation
e verificare cleanup.
Un test risponde:
è corretto?
Un benchmark:
quanto costa?
Gli script:
benchmark-epub-node.mjs
benchmark-epub-regions.mjs
raccolgono:
elapsedMs
asset counts
EPUB bytes
diagnostics
Non dovremmo trasformare un singolo timing in:
assert elapsed < 1200ms
su qualsiasi macchina.
Sarebbe flaky.
Meglio testare invarianti come:
chapter 5 è l'unica detail call necessaria
cache bounded <= 50
canvas viene rilasciato
no duplicate image asset
Questi sono proxy più deterministici del wall-clock.
Poi misuriamo:
baseline
new implementation
sullo stesso ambiente.
Test e benchmark si completano.
Una fixture può rappresentare:
tabella
formula
slide visuale
testo normale
mixed page
e diventare input stabile.
Possiamo confrontare:
struttura XHTML
numero asset
manifest
region bounds
testo estratto
senza necessariamente pretendere bytes identici per ogni libreria/browser.
Salvare:
intero popup HTML
e confrontarlo byte per byte può produrre diff enormi per modifiche irrilevanti.
Meglio estrarre:
tab labels
action order
ARIA states
visible groups
e confrontare una rappresentazione semantica.
Bug come:
Preferenze troppo stretta
checkbox accent incoerente
spazio insufficiente
non sempre emergono da DOM assertions.
CSS invariant
browser DOM/layout check
visual screenshot review
Per alcune regressioni serve davvero il terzo.
Utile per:
layout
clipping
spacing
theme
font
sprite
Meno utile per:
algoritmo autoplay
storage migration
API retry
Visual regression richiede controllare:
browser version
font
viewport
DPI
animation
OS rendering
altrimenti avremo rumore.
Nel Capitolo 11 abbiamo visto:
Back
Escape
tab navigation
roving tabindex
Un refactor visuale può rompere accessibilità senza cambiare la UI apparente.
Proprietà utili:
open detail → title focused
Escape → opener focused
tab switch → exactly one tabindex=0
hidden panel → not focusable
Questi sono behavior tests, non screenshot.
Esempi:
stop 70% prima del session limit
master incomplete candidates prima dei 100%
current chapter check prima del master refresh
storage write serialized
menu dependency caricata prima del controller
Se l’ordine cambia:
stesse funzioni
stessi dati
possono produrre comportamento diverso.
Perciò è legittimo testarlo.
Un test JS può creare concurrency logica con:
Promise.all([...])
e controllare:
read-modify-write
serialization
deduplication
Nel test storage:
storageGetMany: async (...) => {
await new Promise(resolve => setTimeout(resolve, 2));
...
}
Il piccolo delay espone una possibile interleaving.
Poi la queue deve impedire lost update.
Un pattern futuro migliore:
Promise barrier A
Promise barrier B
per decidere esattamente:
task 1 legge
task 2 legge
task 1 scrive
task 2 scrive
Questo evita affidarsi a 2ms reali.
clock
random
network
browser scheduling
storage state
DOM order
locale
timezone
Non significa mockare tutto.
Significa sapere quali input devono essere fissi per rendere significativo il risultato.
crypto.randomUUID()In produzione è ottimo per:
operationId
jobId
In un test che confronta output potremmo iniettare un UUID deterministico.
Non vogliamo snapshot che cambiano a ogni run per motivi irrilevanti.
Se testiamo:
Intl.DateTimeFormat("it-IT")
dobbiamo sapere se:
timezone locale
influenza il risultato.
Un test robusto controlla o evita dipendenze non necessarie.
Se test A lascia:
storage.enabled = false
e test B assume true:
order-dependent suite
è un smell.
Ogni test dovrebbe costruire:
context
stored
document
maps
da zero, salvo test espliciti di upgrade/migration.
Idealmente:
riproduci failure con test rosso
↓
applica fix
↓
verde
Questo è il classico:
red → green → refactor.
A volte dobbiamo prima esplorare manualmente.
Ma prima di considerare chiuso il bug, proviamo comunque a catturare la property in un test.
Un typo statico:
"Capitoli" font errato
può essere protetto da una semplice CSS invariant.
Non serve avviare tre browser.
Regola molto utile:
Testa al livello più basso che può dimostrare correttamente il comportamento.
Questo riduce:
tempo
flakiness
complessità
Se la property è:
focus torna realmente al bottone dopo Escape
una regex su:
.focus()
non basta.
Serve un ambiente DOM/browser che esegua il comportamento.
Un test seam è un punto in cui possiamo sostituire una dipendenza.
Esempi:
fetchImpl
clock
storage wrapper
DOM adapter
API adapter
Più chiari sono i seam, più facile è testare.
Molti seam sono impliciti e vengono costruiti con:
vm globals
Funziona, ma segnala anche una possibile evoluzione architetturale.
source.slice(
source.indexOf("async function X"),
source.indexOf("function Y")
)
È estremamente pratico.
Ma dipende dalla struttura testuale.
Se spostiamo:
helper Y
prima di X, il comportamento production può essere identico ma il test non trova più il boundary.
Questo è test brittleness.
Un test è brittle se fallisce facilmente per modifiche che non cambiano la property che vuole proteggere.
Esempi:
exact DOM string
exact source ordering non semantico
private variable name
whitespace
A volte vogliamo intenzionalmente proteggere:
source order
come nel caso delle guardie.
La domanda è:
questa forma è davvero parte del contract?
In futuro potremmo spostare:
courseIndexRouteMap
autoplayPresentation
normalizeLayout
candidate ordering
identity matching
in moduli con export espliciti.
Test:
import {
courseIndexRouteMap
} from "../playback-discovery.mjs";
anziché:
slice content.js
create VM
inject globals
content.js opera in un ambiente particolare e alcune funzioni dipendono fortemente dal DOM host.
Estrarre tutto per “testabilità” può creare abstraction artificiale.
Meglio estrarre:
logica pura / quasi pura
non necessariamente ogni funzione.
PlumePilot dipende da contratti non controllati:
Multiversity API
DOM shape
Chrome API
Firefox manifest
store policy
Un contract test verifica:
la forma che ci aspettiamo
per esempio:
outline entry contains display_order
lesson response maps lp_id/id
manifest references existing files
Quando il provider cambia:
field
DOM nesting
route
il test dovrebbe fallire vicino al boundary, non produrre un bug lontano.
Per normalizzatori:
null
missing field
string instead of number
oversized text
duplicate identity
sono input importanti.
Non necessariamente:
accetta tutto
ma:
fail predictable
validate-release.mjs verifica lo ZIP finale.
Questo è debugging preventivo.
Source:
corretto
Build:
menu-ux.css dimenticato
Runtime source tests:
verdi
Artifact validator:
rosso
Ogni layer cattura classi di bug diverse.
Prima di store submission abbiamo ancora valore in:
install package reale
login demo
open course
popup
floating menu
export
quiz
commission page
Non:
"faccio un giro"
Meglio:
1. install
2. course loads
3. popup state matches
4. floating state matches
5. first incomplete
6. PDF/HTML export
7. EPUB builder opens
8. operation cancel
9. preferences persist
10. no console fatal errors
La ripetibilità conta anche nel manual testing.
La checklist verifica invarianti conosciuti.
L’exploratory testing cerca:
problemi che non sappiamo ancora nominare
Entrambi servono.
Molte regressioni di PlumePilot sono emerse perché il prodotto veniva usato su:
corsi reali
layout reali
sequenze reali
Questo produce input molto più ricchi dei fixture.
Quando troviamo il bug:
esperienza reale
↓
minimal reproducer
↓
automated regression
Altrimenti rimane conoscenza nella memoria del maintainer.
Un report efficace contiene:
versione PlumePilot
browser/versione
piattaforma
pagina/corso
precondizioni
passi
expected
actual
riproducibilità
console error rilevanti
Sono molto utili per:
layout
timing
focus
navigation
meno per:
API identity
storage race
Il formato del report deve adattarsi al bug.
Possiamo classificare:
data corruption
wrong side effect
navigation error
operation lock
export failure
UI inaccessible
visual polish
store rejection
Questo aiuta a prioritizzare.
Se non sappiamo quale capitolo corrisponde:
non saltare
Se non sappiamo se un test è completo:
non dichiarare tutto completato
Se replaceExactly() non trova esattamente il pattern:
non produrre Firefox ZIP
Questi sono tutti la stessa filosofia:
incertezza + side effect rischioso → stop / fallback sicuro.
Una rete transitoria:
retry
Un DOM che non ha ancora renderizzato:
wait
Una identity ambigua:
non indovinare
Il debugging serve anche a classificare l’errore correttamente.
| Failure | Recovery |
|---|---|
| rete temporanea | retry/backoff |
| API incomplete | visual fallback |
| DOM not ready | wait/re-observe |
| identity ambiguous | stop/fallback |
| hidden tab timeout | defer |
| stale cache | refresh |
| user cancel | abort/cleanup |
| artifact invariant | fail build |
| store warning | classify/document |
La stessa parola:
errore
non basta.
Quando una race è difficile:
T0 video ended
T1 advance starts
T2 wait begins
T3 accordion closes
T4 DOM re-render
T5 wait resolves
T6 stale row used
La timeline spesso rende evidente il bug prima del debugger.
sequenceDiagram
participant V as Video
participant A as Autoplay
participant DOM as LMS DOM
participant U as User
V->>A: ended
A->>DOM: remember current lesson
A->>A: await completion
U->>DOM: close accordion
DOM->>DOM: destroy/recreate rows
A->>DOM: resume
Note over A,DOM: old row is stale
A->>DOM: recover by logical identity
DOM-->>A: fresh row
A->>DOM: select next activity
Questo diagramma è anche una specifica temporale.
Task A read {pegaso}
Task B read {pegaso}
Task A write {pegaso, mercatorum}
Task B write {pegaso, utsr}
Risultato:
mercatorum perso
La queue serializzata cambia la timeline:
A read
A write
B read updated state
B write
Ogni:
await
è un potenziale punto di interleaving.
Nel test chiediamo:
cosa può cambiare qui?
Poi costruiamo il cambio.
Aggiungiamo:
skip videos
test policy
70% stop
chapter limit
bookmark
Le combinazioni crescono velocemente.
Non possiamo enumerare tutti gli stati.
Scegliamo casi che rappresentano proprietà:
order preserved
priority respected
manual selection wins
unknown doesn't become complete
active operation remains cancellable
Un buon test copre una classe di bug, non una singola sequenza.
Property:
all suspected incomplete chapters
must preserve relative course order
Possiamo testare:
[100,80,80,100]
→ request [2,3]
Una library potrebbe generare automaticamente:
percentages
orders
duplicate labels
missing IDs
e verificare invarianti.
Sarebbe interessante per:
route mapping
layout normalization
storage migrations
Per normalizzatori possiamo generare:
null
undefined
NaN
-1
huge
duplicate
unknown string
empty array
e verificare:
no crash
bounded output
valid schema
Un test passa.
Ma fallirebbe se rompessimo volontariamente il codice?
Mutation testing prova modifiche come:
< → <=
true → false
remove guard
swap order
Se la suite rimane verde:
test insufficiente
Particolarmente in:
threshold precedence
candidate ordering
identity validation
layout normalization
Non è indispensabile oggi, ma è un buon passo futuro.
assert.ok(result);
assert.equal(result.status, "target");
assert.equal(result.entry.lessonNumber, 4);
assert.deepEqual(requested, [4]);
Più la failure message ci dice:
quale proprietà è rotta
più rapido sarà il debug.
await new Promise(r => setTimeout(r, 3000));
wait for condition
fake timer
explicit promise barrier
event listener
Il test diventa più veloce e meno flaky.
Supponiamo di creare:
FakeMultiversityEngine
con 1000 righe che replica l’LMS.
Ora abbiamo due sistemi da mantenere.
Preferiamo fake che modellano:
solo il comportamento rilevante
come:
currentLesson: () => null
per un accordion chiuso.
Testare:
funzione privata chiamata esattamente 2 volte
può essere fragile.
Meglio, se possibile:
API requests effettuate = [4,1]
perché quello è un effetto osservabile rilevante.
Closed accordion fix:
riapri correttamente
ma deve anche garantire:
non sovrascrivere manual selection
Perciò il test ha entrambi.
Non blocchiamo un futuro refactor imponendo:
helper A fisicamente prima di helper B
a meno che l’ordine di caricamento/esecuzione sia davvero parte del contract.
Ogni PR esegue:
Node regression suites
browser UX check
release validator quando rilevante
e impedisce il merge se rompe un’invariante.
Anche se il progetto non usa ancora una pipeline CI completa per tutto, la struttura dei test è già abbastanza vicina a diventarlo.
Il passo architetturale successivo sarebbe:
un singolo comando
→ tutta la suite fast
→ browser checks opzionali/CI
→ release validation
Se esistono 29 file:
node tests/foo.mjs
node tests/bar.mjs
...
il maintainer deve ricordare la lista.
Meglio:
npm test
o:
node scripts/test-all.mjs
anche se non vogliamo introdurre npm dependencies.
Può:
enumerare tests/*.mjs
escludere benchmark
eseguire in ordine stabile
fermare/fallire
riassumere
Questo riduce errori umani.
test:unit
test:browser
test:release
test:all
La categoria comunica costo e requisiti.
Playwright permette di eseguire progetti configurati su:
Chromium
Firefox
WebKit
Chrome
Edge
Ma per una WebExtension non basta dire:
page.goto()
Dobbiamo caricare realmente l’estensione e i suoi contesti.
Un test web in:
Playwright Firefox
non dimostra automaticamente che:
manifest Firefox package
background scripts
AMO transforms
funzionino.
Serve testare il package target appropriato.
build release artifact
↓
launch browser
↓
load target artifact
↓
run smoke E2E
Questo avvicina il test a ciò che l’utente installa.
web-ext lintPuò verificare classi di problemi del pacchetto.
Non verifica:
```textautoplay corretto export corretto UI corretta
---
## 187. Complementarità
```text
lint → package/static rules
tests → behavior
manual → real platform
review → external policy
Nessuno sostituisce gli altri.
AMO rejects
Non modifichiamo subito il codice.
Prima classifichiamo:
first-party source?
vendor?
manifest?
source package?
policy?
false positive?
Se il reviewer segnala:
Function constructor
creiamo scanner locale che cerca:
Function(
nel package generato, non solo nel repository.
Da qui è nato il review-safe validator.
store rejection
↓
rule understood
↓
build change
↓
validator
Così la stessa rejection non dipende più dalla memoria.
release-candidate-2.34.0.mjs controlla:
manifest version
changelog
release notes
AMO README
popup Novità
platform-neutral copy
Questo può sembrare “non software”.
Ma un package con:
manifest 2.35.2
AMO README 2.35.1
è un release bug reale.
Se un reviewer userà:
AMO_SOURCE_README
quel file è parte della delivery.
Vale la pena testarne:
versione
filename
commands
Per un clipping:
getBoundingClientRect
computedStyle
scrollWidth/clientWidth
viewport
scrollbar gutter
ci dicono più di:
aggiungi 8px
Source test:
overflow hidden presente
Browser check:
panel rect dentro viewport
Sono due livelli diversi.
Le animazioni devono considerare:
prefers-reduced-motion
Un test browser può emulare la preference.
Non è soltanto estetica:
timing di focus/scroll
può cambiare.
Non basta guardare la UI builder.
Dobbiamo ispezionare:
bytes finali
metadata
manifest EPUB
XHTML
links
fonts
images
L’EPUB builder accetta:
diagnostics
e produce:
pages
assets
mathFontBytes
epubBytes
elapsedMs
Questa è instrumentation progettata per test/benchmark.
Invece di stampare ogni cosa:
diagnostics.pages.push(record);
poi il caller decide:
log
assert
write JSON
ignore
Questo separa osservabilità da output.
Se per testare:
candidate ordering
dobbiamo simulare:
intero browser
probabilmente logica e side effect sono troppo intrecciati.
Alcune feature sono intrinsecamente browser-integrated.
Il valore sta nel separare:
decision
da:
effect
quando possibile.
input
↓
pure decision logic
↓
command / target
↓
imperative browser effect
Per esempio:
master + outline
↓
route mapping
↓
lesson target
↓
openChapter/clickRow
Il core è facile da testare.
La shell richiede browser/fake DOM.
Potremmo estrarre:
candidate ordering
identity matching
result classification
dal DOM adapter.
autoplayPresentation() è già un esempio riuscito:
settings
→ presentation model
facilmente testabile.
Per esempio:
operation.kind
operation.phase
operation.id
sono già un piccolo modello osservabile.
UI, background e test possono usarlo.
Non dobbiamo far dipendere test da:
stringa esatta di console.log
se possiamo esporre una struttura.
I log cambiano spesso.
In produzione non vogliamo lanciare assert ovunque.
Ma possiamo normalizzare:
if (!validIdentity(...)) return fallback;
e testare quella guardia.
Nel tooling, invece, fallire forte è spesso corretto:
missing file
unexpected manifest
vendor checksum changed
Il release builder deve essere severo.
Prima annotiamo:
severity
scope
last known good
first known bad
browser/platform
repro rate
side effect risk
high-risk wrong action
→ fail closed first
performance regression
→ benchmark/profiler
layout regression
→ browser measurement/screenshot
race
→ timeline + controlled scheduler
API drift
→ raw response + boundary normalizer
Quali:
platform
course
section
chapter
lesson
operation
tab
sono coinvolti?
Dove gira:
popup
background
MAIN
isolated
builder
?
Il dato arriva da:
DOM
API
cache
storage
master index
?
Può essere diventato stale durante:
await
reload
tab hide
service worker restart
?
Qual è il danno se sbagliamo?
click
navigation
test submission
reload
storage write
download
Esiste un path più sicuro?
Qual è il minimo ambiente che riproduce la property?
Autoplay si ferma.
Stale DOM identity.
recoverPlaybackLesson().
DOM row cambia durante await.
logical lesson identity survives DOM reconstruction
Per arrivare al capitolo 5 vengono aperti 2,3,4.
Search path usa DOM traversal invece di master-guided target.
Forward API discovery.
requests == [5]
completed intermediate chapters need not be opened
“Tutto completo” falso.
Index trattato come authority.
Two-pass + details.
all master 100
but chapter 5 video incomplete
→ still find 5
master 100 alone cannot prove all activities complete
Recovery reload loop.
Timeout while hidden interpreted as page failure.
Visibility-aware wait/recovery.
Fake clock + visibilitychange.
hidden timeout cannot trigger destructive recovery
Dati Pegaso/Mercatorum con same ID si sovrascrivono.
Identity domain non include platform.
Composite scoped keys.
Concurrent same-ID writes.
platform is part of persistent identity
Store rejection.
Review-sensitive constructs nei runtime vendor/artifact.
Readable upstream + deterministic transforms.
Package scanner + checksums.
Firefox artifact contains no rejected constructs
checkbox accent
spacing
font mismatch
long tab labels
auto-scroll
Presentation rules replicate across two surfaces.
Shared UX/CSS primitives.
Source invariants + Chromium controller fixture.
same semantic role → same presentation scale/behavior
È stato molto efficace per crescere velocemente.
Ma un progetto più modulare potrebbe esporre direttamente la logica testabile.
Riduce il rischio di dimenticare una suite.
Specialmente:
service worker termination
popup/floating sync
Firefox package smoke
Edge package smoke
Dati sanitizzati/versionati possono proteggere i normalizzatori da drift.
Soltanto per componenti dove pixel/layout sono realmente parte del contract.
Sarebbero:
lenti
flaky
difficili da diagnosticare
Perderemmo:
manifest
DOM
Shadow DOM
focus
browser lifecycle
artifact
La qualità sta nelle property protette.
tests/unit/
tests/contracts/
tests/regressions/
tests/browser/
tests/fixtures/
scripts/validate-release.mjs
scripts/benchmark-*.mjs
Il nome della cartella comunica lo scopo.
Deve essere abbastanza veloce da essere eseguita:
prima di ogni merge
Contiene:
Node behavioral
normalization
source invariants
fixture logic
UI
extension contexts
focus
storage synchronization
service worker lifecycle
Può essere più costosa.
browser-specific ZIP
manifest
vendor
licenses
source archive
checksums
Obbligatoria prima degli store.
Pochi flussi ad alto valore:
real Multiversity
real authentication
real course DOM/API
real export
Perché non possiamo controllare completamente la piattaforma esterna con fixture.
Un bug parte da:
milioni di possibili cause
Ogni osservazione elimina ipotesi.
solo Firefox?
→ elimina dominio comune
solo hidden tab?
→ timing/visibility
solo master 100?
→ source hierarchy
solo after await?
→ stale state/race
Debugging è search-space reduction.
Quando una modifica futura rompe:
closed accordion invariant
la suite ci porta immediatamente vicino alla causa.
Riduce il search space prima ancora che l’utente segnali il bug.
// Don't break closed accordions
può essere ignorato.
assert.equal(nextRowReceived, restoredRow);
non può essere ignorato durante una suite verde richiesta.
Questa è la potenza di una executable specification.
Senza test:
refactor = paura
Con test comportamentali:
refactor = ipotesi verificabile
Non elimina il rischio. Lo rende osservabile più rapidamente.
1. comportamento realmente rotto
2. test troppo accoppiato all'implementazione
3. requisito intenzionalmente cambiato
Non facciamo automaticamente:
update snapshot / change expected
Prima classifichiamo.
Se decidiamo che:
normal autoplay deve saltare i completati
cambiamo volutamente il comportamento.
Il vecchio test:
replay all by default
deve fallire.
Poi aggiorniamo:
spec
test
code
docs
insieme.
Può essere sbagliato o superato.
La suite non sostituisce il ragionamento.
Ci dice:
hai cambiato una proprietà che prima consideravamo importante
Poi decidiamo se era intenzionale.
Ottimo per:
stato istantaneo
stack
scope
breakpoint
Ottimo per:
timeline
eventi distribuiti
produzione locale
Ottimo per:
riprodurre
automatizzare
proteggere
Nessuno sostituisce gli altri.
Se apriamo DevTools sul service worker:
possiamo modificarne il lifecycle
Quindi per bug di termination:
test without attached inspector
è essenziale.
before await
during await
after await
Poi elenca ciò che può cambiare:
DOM
enabled
current video
operation
course
tab visibility
storage
Questa tecnica trova molte race.
key
entry
fetchedAt
authority
invalidation
Poi chiedi:
stessa identity?
entry fresh?
dato monotono?
cache può mentire?
fallback?
logical identity
selector
when queried
node can be replaced?
visibility?
container open?
Confronta:
assumed schema
actual schema
normalizer output
Non debugghiamo direttamente dalla UI se il problema nasce al boundary.
Non il repository.
unzip
manifest
file list
checksum
static scan
Il bug può essere stato introdotto dal build.
Un utente segnala:
dopo aver aperto il popup,
autoplay mostra "attivo"
ma il floating menu mostra "in pausa"
Costruisci:
hypotheses
instrumentation points
minimal test
Quale ruolo ha:
chrome.storage.onChanged
?
Il bookmark trova capitolo 8 invece del 3.
Master:
3 = 100
8 = 70
Detail:
3 contiene un test pending
Quale test regressivo scriveresti?
Firefox funziona quando DevTools è aperto ma a volte smette di rispondere dopo minuti.
Perché:
lifecycle
deve entrare subito nelle ipotesi?
Un test fa:
await sleep(2000);
assert.equal(...)
ed è flaky.
Progetta una versione deterministica con:
fake clock
condition
event
o promise barrier
Un refactor sposta una funzione in content.js.
Dieci test falliscono con:
Missing function boundary
ma il browser funziona.
È una regression production o una test-architecture regression?
Come la risolveresti senza disabilitare i test?
Una nuova release PDF.js cambia checksum.
Il validator fallisce.
Quali passi fai prima di aggiornare il checksum atteso?
Abbiamo:
button visible
ma non raggiungibile con tastiera.
Quale livello di test scegli?
regex source
VM
browser fixture
screenshot
e perché?
Il test suite passa ma Edge Store package manca di un locale.
Che tipo di test mancava?
Un test nato per impedire il ritorno di un bug già osservato.
La causa strutturale che rende possibile il sintomo.
Il caso più piccolo che conserva la failure.
Una sostituzione controllata di una dipendenza:
stub
fake
mock
spy
Creare intenzionalmente:
timeout
API error
missing data
abort
per testare recovery.
Test che protegge la struttura/relazioni del sistema.
Test eseguito sul prodotto costruito, non sul source tree.
Test non deterministico a parità apparente di input.
Test con dipendenze esterne fortemente controllate.
Confronto con un output di riferimento noto.
Copertura delle transizioni significative di una state machine.
Insieme delle proprietà del comportamento effettivamente protette.
Boundary dove una dipendenza può essere sostituita in modo controllato.
Regola che decide:
pass / fail
per un esperimento o test.
L’atto di osservare modifica il comportamento del sistema.
Il service worker mantenuto vivo da DevTools è un ottimo esempio.
Requirement espresso come codice che fallisce quando viene violato.
Node.js:
https://nodejs.org/api/assert.html
Node.js:
https://nodejs.org/api/vm.html
Ricorda: node:vm non è una sandbox per codice non fidato.
Chrome for Developers:
https://developer.chrome.com/docs/extensions/how-to/test/end-to-end-testing
https://developer.chrome.com/docs/extensions/how-to/test/puppeteer
Chrome for Developers:
https://developer.chrome.com/docs/extensions/how-to/test/test-serviceworker-termination-with-puppeteer
https://playwright.dev/docs/running-tests
https://playwright.dev/docs/test-projects
Mozilla:
https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext/
PlumePilot ci mostra che debugging e testing non sono fasi separate.
Il percorso reale è:
utente incontra comportamento inatteso
↓
costruiamo una riproduzione
↓
identifichiamo context + identity + timeline
↓
formuliamo ipotesi
↓
strumentiamo il boundary giusto
↓
troviamo la root cause
↓
correggiamo
↓
creiamo regression test
↓
formalizziamo l'invariante
I test del progetto usano strategie differenti:
Node + assert
VM contexts
fake clock
fake DOM
failure injection
source invariants
browser fixtures
EPUB fixtures
benchmarks
release artifact validators
manual live smoke
Nessuna è sufficiente da sola.
La lezione centrale è:
Un bug vale più della sua correzione: ci insegna una proprietà del sistema che prima non conoscevamo.
Se quella proprietà resta soltanto nella testa del maintainer:
può essere dimenticata
Se diventa un test:
entra nella memoria eseguibile del progetto
Dopo aver visto come una regression diventa un test, possiamo studiare come una modifica attraversa il progetto senza distruggere ciò che già funziona:
issue / idea
↓
branch
↓
commit
↓
test
↓
pull request
↓
review
↓
merge strategy
↓
version bump
↓
artifact
↓
store submission
↓
tag / release
Useremo casi reali di PlumePilot:
integration branch
stacked PR
squash merge
release branch
2.35.1 → 2.35.2
branch riallineato senza cambiare il tree
browser packages
store review
tagging
e vedremo perché Git history, release history e deployed artifact history sono tre cose correlate ma non identiche.
← 13 — Cross-browser · Indice · 15 — Git, PR e release engineering →