feat: générateur d'exercices + planning de révision IA
- Générateur d'exercices : bouton dans menu note → IA crée 5 exercices - Niveaux variés (facile/moyen/difficile) avec emojis 🟢🟡🔴 - Corrigés détaillés dans des toggles (cliquer pour révéler) - Callout warning pour le niveau - Notes créées dans le même carnet - Planning de révision : bouton dans barre carnet → IA crée planning - Choix date d'examen - Répétition espacée (première lecture → revoir → révision globale) - Rappels automatiques ajoutés aux notes (9h le jour J) - Vue chronologique avec activités et notes par jour - Services : exercise-generator.service.ts + study-planner.service.ts - Endpoints : /api/ai/generate-exercises + /api/ai/study-plan - i18n FR/EN complet
This commit is contained in:
@@ -1,99 +1,69 @@
|
||||
/**
|
||||
* Test du ChunkIndexingService — dedup, stale deletion, upsert.
|
||||
* Mocke l'embedding (pas d'appel API), utilise la vraie DB.
|
||||
*/
|
||||
import { test, expect, describe, beforeAll, afterAll, beforeEach } from 'vitest'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { ChunkIndexingService } from '../../lib/ai/services/chunk-indexing.service'
|
||||
import { embeddingService } from '../../lib/ai/services/embedding.service'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const testNoteId = 'test-chunk-000001'
|
||||
|
||||
function test(name: string, fn: () => Promise<void>) {
|
||||
return fn()
|
||||
.then(() => console.log(` ✓ ${name}`))
|
||||
.catch((err: any) => {
|
||||
console.error(` ✗ ${name}: ${err.message}`)
|
||||
process.exitCode = 1
|
||||
describe('US-CHUNK-2 : Indexation incrémentale avec dedup', () => {
|
||||
let originalEmbedText: any
|
||||
|
||||
beforeAll(async () => {
|
||||
originalEmbedText = embeddingService.embedText
|
||||
embeddingService.embedText = async (text: string) => {
|
||||
const hash = crypto.createHash('md5').update(text).digest()
|
||||
return Array.from({ length: 1536 }, (_, i) => hash[i % 16] / 255)
|
||||
}
|
||||
|
||||
await prisma.note.upsert({
|
||||
where: { id: testNoteId },
|
||||
create: {
|
||||
id: testNoteId,
|
||||
title: 'Test Note for Chunk Indexing',
|
||||
content: '<p>Test</p>',
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
|
||||
// Mock l'embedding service : vecteur déterministe basé sur le hash du contenu
|
||||
const originalEmbedText =
|
||||
require('../../lib/ai/services/embedding.service').embeddingService.embedText
|
||||
|
||||
function mockEmbedding() {
|
||||
const svc = require('../../lib/ai/services/embedding.service').embeddingService
|
||||
svc.embedText = async (text: string) => {
|
||||
const crypto = require('crypto')
|
||||
const hash = crypto.createHash('md5').update(text).digest()
|
||||
return Array.from({ length: 1536 }, (_, i) => hash[i % 16] / 255)
|
||||
}
|
||||
}
|
||||
|
||||
function restoreEmbedding() {
|
||||
const svc = require('../../lib/ai/services/embedding.service').embeddingService
|
||||
svc.embedText = originalEmbedText
|
||||
}
|
||||
|
||||
async function ensureTestNote() {
|
||||
await prisma.note.upsert({
|
||||
where: { id: testNoteId },
|
||||
create: {
|
||||
id: testNoteId,
|
||||
title: 'Test Note for Chunk Indexing',
|
||||
content: '<p>Test</p>',
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
await prisma.noteEmbeddingChunk.deleteMany({ where: { noteId: testNoteId } })
|
||||
}
|
||||
afterAll(async () => {
|
||||
embeddingService.embedText = originalEmbedText
|
||||
await prisma.noteEmbeddingChunk.deleteMany({ where: { noteId: testNoteId } })
|
||||
await prisma.note.delete({ where: { id: testNoteId } }).catch(() => {})
|
||||
})
|
||||
|
||||
async function removeTestNote() {
|
||||
await prisma.note.delete({ where: { id: testNoteId } }).catch(() => {})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
mockEmbedding()
|
||||
await ensureTestNote()
|
||||
await cleanup()
|
||||
|
||||
console.log('\n=== US-CHUNK-2 : Indexation incrémentale avec dedup ===\n')
|
||||
|
||||
const service = new ChunkIndexingService()
|
||||
beforeEach(async () => {
|
||||
await prisma.noteEmbeddingChunk.deleteMany({ where: { noteId: testNoteId } })
|
||||
})
|
||||
|
||||
const longContent = Array.from({ length: 8 }, (_, i) =>
|
||||
`Section ${i} de la note de test. `.repeat(60).trim(),
|
||||
).join('\n\n')
|
||||
|
||||
await test('première indexation → tous les fragments sont nouveaux', async () => {
|
||||
test('première indexation → tous les fragments sont nouveaux', async () => {
|
||||
const service = new ChunkIndexingService()
|
||||
const result = await service.indexNote(testNoteId, 'Note de test', longContent)
|
||||
|
||||
if (result.newFragments === 0)
|
||||
throw new Error(`attendu newFragments > 0, reçu ${result.newFragments}`)
|
||||
if (result.deleted !== 0)
|
||||
throw new Error(`attendu deleted=0, reçu ${result.deleted}`)
|
||||
if (result.skipped !== 0)
|
||||
throw new Error(`attendu skipped=0, reçu ${result.skipped}`)
|
||||
|
||||
console.log(` → ${result.newFragments} nouveaux, ${result.totalFragments} total`)
|
||||
expect(result.newFragments).toBeGreaterThan(0)
|
||||
expect(result.deleted).toBe(0)
|
||||
expect(result.skipped).toBe(0)
|
||||
})
|
||||
|
||||
await test('deuxième indexation (même contenu) → tout skipped, 0 nouveau', async () => {
|
||||
test('deuxième indexation (même contenu) → tout skipped, 0 nouveau', async () => {
|
||||
const service = new ChunkIndexingService()
|
||||
await service.indexNote(testNoteId, 'Note de test', longContent)
|
||||
const result = await service.indexNote(testNoteId, 'Note de test', longContent)
|
||||
|
||||
if (result.newFragments !== 0)
|
||||
throw new Error(`attendu 0 nouveau, reçu ${result.newFragments}`)
|
||||
if (result.skipped === 0)
|
||||
throw new Error(`attendu skipped > 0, reçu ${result.skipped}`)
|
||||
if (result.deleted !== 0)
|
||||
throw new Error(`attendu deleted=0, reçu ${result.deleted}`)
|
||||
|
||||
console.log(` → ${result.skipped} skip, 0 nouveau ✓`)
|
||||
expect(result.newFragments).toBe(0)
|
||||
expect(result.skipped).toBeGreaterThan(0)
|
||||
expect(result.deleted).toBe(0)
|
||||
})
|
||||
|
||||
await test('modification d\'une section → 1 nouveau, reste skip', async () => {
|
||||
test('modification d\'une section → 1 nouveau, reste skip', async () => {
|
||||
const service = new ChunkIndexingService()
|
||||
await service.indexNote(testNoteId, 'Note de test', longContent)
|
||||
|
||||
const sections = Array.from({ length: 8 }, (_, i) =>
|
||||
`Section ${i === 3 ? 'MODIFIÉE' : i} de la note de test. `.repeat(60).trim(),
|
||||
)
|
||||
@@ -101,15 +71,12 @@ async function main() {
|
||||
|
||||
const result = await service.indexNote(testNoteId, 'Note de test', modified)
|
||||
|
||||
if (result.newFragments === 0)
|
||||
throw new Error(`attendu au moins 1 nouveau fragment, reçu ${result.newFragments}`)
|
||||
if (result.deleted === 0)
|
||||
throw new Error(`attendu au moins 1 stale supprimé, reçu ${result.deleted}`)
|
||||
|
||||
console.log(` → ${result.newFragments} nouveau(x), ${result.skipped} skip, ${result.deleted} supprimé(s)`)
|
||||
expect(result.newFragments).toBeGreaterThanOrEqual(1)
|
||||
expect(result.deleted).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
await test('suppression d\'une section → fragments stale nettoyés', async () => {
|
||||
test('suppression d\'une section → fragments stale nettoyés', async () => {
|
||||
const service = new ChunkIndexingService()
|
||||
const sections = Array.from({ length: 8 }, (_, i) =>
|
||||
`Section ${i} de la note de test. `.repeat(60).trim(),
|
||||
)
|
||||
@@ -126,15 +93,12 @@ async function main() {
|
||||
where: { noteId: testNoteId },
|
||||
})
|
||||
|
||||
if (afterCount >= beforeCount)
|
||||
throw new Error(`attendu count < ${beforeCount}, reçu ${afterCount}`)
|
||||
if (result.deleted === 0)
|
||||
throw new Error(`attendu deleted > 0, reçu ${result.deleted}`)
|
||||
|
||||
console.log(` → ${beforeCount} → ${afterCount} fragments (${result.deleted} supprimés)`)
|
||||
expect(afterCount).toBeLessThan(beforeCount)
|
||||
expect(result.deleted).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
await test('note vide → tous les fragments supprimés', async () => {
|
||||
test('note vide → tous les fragments supprimés', async () => {
|
||||
const service = new ChunkIndexingService()
|
||||
await service.indexNote(testNoteId, 'Note de test', longContent)
|
||||
const result = await service.indexNote(testNoteId, '', '')
|
||||
|
||||
@@ -142,37 +106,27 @@ async function main() {
|
||||
where: { noteId: testNoteId },
|
||||
})
|
||||
|
||||
if (count !== 0) throw new Error(`attendu 0 fragments, reçu ${count}`)
|
||||
console.log(` → tous les fragments supprimés ✓`)
|
||||
expect(count).toBe(0)
|
||||
})
|
||||
|
||||
await test('deleteNoteChunks → supprime tout', async () => {
|
||||
test('deleteNoteChunks → supprime tout', async () => {
|
||||
const service = new ChunkIndexingService()
|
||||
await service.indexNote(testNoteId, 'Note de test', longContent)
|
||||
await service.deleteNoteChunks(testNoteId)
|
||||
|
||||
const count = await prisma.noteEmbeddingChunk.count({
|
||||
where: { noteId: testNoteId },
|
||||
})
|
||||
if (count !== 0) throw new Error(`attendu 0, reçu ${count}`)
|
||||
console.log(` → 0 fragment restant ✓`)
|
||||
expect(count).toBe(0)
|
||||
})
|
||||
|
||||
await test('hasChunks → détection correcte', async () => {
|
||||
test('hasChunks → détection correcte', async () => {
|
||||
const service = new ChunkIndexingService()
|
||||
const before = await service.hasChunks(testNoteId)
|
||||
if (before) throw new Error('attendu false avant indexation')
|
||||
expect(before).toBe(false)
|
||||
|
||||
await service.indexNote(testNoteId, 'Note de test', longContent)
|
||||
const after = await service.hasChunks(testNoteId)
|
||||
if (!after) throw new Error('attendu true après indexation')
|
||||
|
||||
console.log(` → false avant, true après ✓`)
|
||||
expect(after).toBe(true)
|
||||
})
|
||||
|
||||
await cleanup()
|
||||
restoreEmbedding()
|
||||
await prisma.$disconnect()
|
||||
|
||||
console.log('\n=== Tests terminés ===')
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user