fix(corrections): race condition modele_status + logs diagnostiques

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-22 20:13:56 +03:00
parent 63bc43ddcf
commit 14d8d73991
2 changed files with 65 additions and 18 deletions

View file

@ -138,7 +138,9 @@ describe('correctionController.correctEE — Sprint 3.6a', () => {
expect(result.data.score).toBe(14) expect(result.data.score).toBe(14)
} }
// La persistance de la correction inclut les nouveaux champs + statuts pending // La persistance de la correction inclut les nouveaux champs.
// Les statuts ne sont PAS dans l'update principal (race condition — les
// jobs async les pilotent exclusivement). DEFAULT 'pending' côté migration.
const persisted = supabaseMock.updates.find( const persisted = supabaseMock.updates.find(
(u) => u.table === 'productions' && u.data.score !== undefined, (u) => u.table === 'productions' && u.data.score !== undefined,
) )
@ -147,13 +149,14 @@ describe('correctionController.correctEE — Sprint 3.6a', () => {
score: 14, score: 14,
nclc: 9, nclc: 9,
nclc_cible: 9, nclc_cible: 9,
exercices_status: 'pending',
modele_status: 'pending',
}) })
expect(persisted!.data.revelation).toBeDefined() expect(persisted!.data.revelation).toBeDefined()
expect(persisted!.data.diagnostic).toBeDefined() expect(persisted!.data.diagnostic).toBeDefined()
expect(persisted!.data.conseil_nclc).toBeDefined() expect(persisted!.data.conseil_nclc).toBeDefined()
expect(persisted!.data.erreurs_codes).toBeDefined() expect(persisted!.data.erreurs_codes).toBeDefined()
// Race condition : ces champs ne doivent PAS être touchés par l'update principal.
expect(persisted!.data.modele_status).toBeUndefined()
expect(persisted!.data.exercices_status).toBeUndefined()
}) })
it('modele_status passe à "ready" quand le job réussit', async () => { it('modele_status passe à "ready" quand le job réussit', async () => {

View file

@ -137,7 +137,13 @@ export async function correctEE(
} }
} }
// 4. Persister la correction (statuts modèle/exercices : pending — les jobs sont lancés juste après) // 4. Persister la correction.
// ⚠️ RACE CONDITION — ne PAS inclure `modele_status` ni `exercices_status`
// ici : runModeleJob (lancé en parallèle option b) peut avoir déjà terminé
// et écrit 'ready' avant que cet update ne s'exécute. Écraser avec 'pending'
// perdrait le résultat.
// Les colonnes *_status sont initialisées à 'pending' par la migration
// (DEFAULT) et gérées exclusivement par runModeleJob / runExercicesJob.
const { error: updateError } = await supabase const { error: updateError } = await supabase
.from('productions') .from('productions')
.update({ .update({
@ -149,8 +155,6 @@ export async function correctEE(
conseil_nclc: rapport.conseil_nclc, conseil_nclc: rapport.conseil_nclc,
erreurs_codes: rapport.erreurs_codes, erreurs_codes: rapport.erreurs_codes,
rapport: JSON.stringify(rapport), rapport: JSON.stringify(rapport),
exercices_status: 'pending',
modele_status: 'pending',
}) })
.eq('id', simulationId) .eq('id', simulationId)
@ -190,17 +194,35 @@ interface ModeleJobInput {
async function runModeleJob(input: ModeleJobInput): Promise<void> { async function runModeleJob(input: ModeleJobInput): Promise<void> {
const { simulationId, tache, sujet, texte, nclcObtenu } = input const { simulationId, tache, sujet, texte, nclcObtenu } = input
console.log('[runModeleJob] START', { simulationId, tache, nclcObtenu })
try { try {
const modele = await generateProductionModele({ tache, sujet, texte, nclcObtenu }) const modele = await generateProductionModele({ tache, sujet, texte, nclcObtenu })
await supabase console.log('[runModeleJob] DeepSeek OK, updating productions', {
simulationId,
modeleWordCount: modele.tcf_word_count,
})
const { error: updateErr, data: updateData } = await supabase
.from('productions') .from('productions')
.update({ modele, modele_status: 'ready' }) .update({ modele, modele_status: 'ready' })
.eq('id', simulationId) .eq('id', simulationId)
} catch { .select('id, modele_status')
await supabase console.log('[runModeleJob] update result', { simulationId, updateErr, updateData })
.from('productions') } catch (err) {
.update({ modele_status: 'error' }) const message = err instanceof Error ? err.message : String(err)
.eq('id', simulationId) const stack = err instanceof Error ? err.stack : undefined
console.error('[runModeleJob] CAUGHT ERROR', { simulationId, message, stack })
try {
const { error: fallbackErr } = await supabase
.from('productions')
.update({ modele_status: 'error' })
.eq('id', simulationId)
console.log('[runModeleJob] fallback update result', { simulationId, fallbackErr })
} catch (fallbackExc) {
console.error('[runModeleJob] FALLBACK UPDATE THREW', {
simulationId,
message: fallbackExc instanceof Error ? fallbackExc.message : String(fallbackExc),
})
}
} }
} }
@ -212,21 +234,43 @@ interface ExercicesJobInput {
async function runExercicesJob(input: ExercicesJobInput): Promise<void> { async function runExercicesJob(input: ExercicesJobInput): Promise<void> {
const { simulationId, tache, rapport } = input const { simulationId, tache, rapport } = input
console.log('[runExercicesJob] START', {
simulationId,
tache,
erreursCodesCount: rapport.erreurs_codes.length,
})
try { try {
const exercices = await generateExercices({ const exercices = await generateExercices({
tache, tache,
erreursCodes: rapport.erreurs_codes, erreursCodes: rapport.erreurs_codes,
criteres: rapport.criteres, criteres: rapport.criteres,
}) })
await supabase console.log('[runExercicesJob] DeepSeek OK, updating productions', {
simulationId,
exercicesCount: exercices.length,
})
const { error: updateErr, data: updateData } = await supabase
.from('productions') .from('productions')
.update({ exercices, exercices_status: 'ready' }) .update({ exercices, exercices_status: 'ready' })
.eq('id', simulationId) .eq('id', simulationId)
} catch { .select('id, exercices_status')
await supabase console.log('[runExercicesJob] update result', { simulationId, updateErr, updateData })
.from('productions') } catch (err) {
.update({ exercices_status: 'error' }) const message = err instanceof Error ? err.message : String(err)
.eq('id', simulationId) const stack = err instanceof Error ? err.stack : undefined
console.error('[runExercicesJob] CAUGHT ERROR', { simulationId, message, stack })
try {
const { error: fallbackErr } = await supabase
.from('productions')
.update({ exercices_status: 'error' })
.eq('id', simulationId)
console.log('[runExercicesJob] fallback update result', { simulationId, fallbackErr })
} catch (fallbackExc) {
console.error('[runExercicesJob] FALLBACK UPDATE THREW', {
simulationId,
message: fallbackExc instanceof Error ? fallbackExc.message : String(fallbackExc),
})
}
} }
} }