feat(plan5): openaiClient — génération descriptions GPT-4o (TDD)

This commit is contained in:
2026-06-09 09:42:26 +02:00
parent 3aa770321c
commit ed091711c8
2 changed files with 92 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import { generateDescriptions } from './openaiClient'
const mockFetch = jest.fn()
global.fetch = mockFetch
const mockResponse = (content: string) => ({
ok: true,
json: async () => ({ choices: [{ message: { content } }] }),
})
describe('generateDescriptions', () => {
beforeEach(() => {
process.env.OPENAI_API_KEY = 'sk-test'
mockFetch.mockReset()
})
it('retourne shortDesc et longDesc depuis OpenAI', async () => {
mockFetch
.mockResolvedValueOnce(mockResponse('<p>Description courte</p>'))
.mockResolvedValueOnce(mockResponse('<p>Description longue</p>'))
const result = await generateDescriptions({
title: 'Perceuse', rawDescription: 'Une perceuse', comment: '', vendor: 'BOSCH', famille: 'OUTILLAGE',
})
expect(result.shortDesc).toBe('<p>Description courte</p>')
expect(result.longDesc).toBe('<p>Description longue</p>')
expect(mockFetch).toHaveBeenCalledTimes(2)
})
it('lance les deux requêtes en parallèle', async () => {
mockFetch.mockResolvedValue(mockResponse('<p>ok</p>'))
await generateDescriptions({ title: 'T', rawDescription: '', comment: '', vendor: '', famille: '' })
expect(mockFetch).toHaveBeenCalledTimes(2)
})
it('lance une erreur si OPENAI_API_KEY manquant', async () => {
delete process.env.OPENAI_API_KEY
await expect(generateDescriptions({ title: 'T', rawDescription: '', comment: '', vendor: '', famille: '' }))
.rejects.toThrow('OPENAI_API_KEY')
})
it('lance une erreur si la réponse OpenAI est non-ok', async () => {
mockFetch.mockResolvedValue({ ok: false, status: 429, text: async () => 'rate limited' })
await expect(generateDescriptions({ title: 'T', rawDescription: '', comment: '', vendor: '', famille: '' }))
.rejects.toThrow('429')
})
})
+46
View File
@@ -0,0 +1,46 @@
interface DescriptionInput {
title: string
rawDescription: string
comment: string
vendor: string
famille: string
}
interface Descriptions {
shortDesc: string
longDesc: string
}
async function callOpenAI(prompt: string): Promise<string> {
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) throw new Error('OPENAI_API_KEY requis')
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ model: 'gpt-4o', messages: [{ role: 'user', content: prompt }], temperature: 0.7 }),
})
if (!res.ok) {
const err = await res.text()
throw new Error(`OpenAI API error: ${res.status}${err}`)
}
const data = await res.json()
return (data.choices?.[0]?.message?.content ?? '').trim()
}
function buildPrompt(input: DescriptionInput, length: number): string {
return `A partir des caractéristiques suivantes, crée un texte descriptif de ${length} caractères en faisant attention au SEO pour qu'il soit en rapport avec la catégorie ${input.famille} et le produit ${input.title} :
Nom = ${input.title}
Description : ${input.rawDescription}
Commentaire : ${input.comment}
Marque : ${input.vendor}
Formate ce texte en HTML.
Ta réponse doit commencer directement par une balise HTML (comme <p> ou <h1>), sans bloc \`\`\`html, sans aucun mot ou encadré autour.`
}
export async function generateDescriptions(input: DescriptionInput): Promise<Descriptions> {
const [shortDesc, longDesc] = await Promise.all([
callOpenAI(buildPrompt(input, 400)),
callOpenAI(buildPrompt(input, 1000)),
])
return { shortDesc, longDesc }
}