import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' import { Hono } from 'hono' import { createGenerateRouter } from './generate' describe('generate route', () => { let originalFetch: typeof fetch beforeEach(() => { originalFetch = globalThis.fetch }) afterEach(() => { globalThis.fetch = originalFetch }) it('returns 400 when topic is missing', async () => { const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) expect(res.status).toBe(400) }) it('returns 500 when DEEPSEEK_API_KEY is not configured', async () => { const app = new Hono().route('/api/generate', createGenerateRouter(undefined)) const res = await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: 'CSS 弹性布局' }), }) expect(res.status).toBe(500) }) it('returns parsed markdown on success', async () => { globalThis.fetch = mock(async () => new Response( JSON.stringify({ choices: [{ message: { content: '# CSS 弹性布局 教学设计' } }] }), { status: 200 }, ), ) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: 'CSS 弹性布局' }), }) expect(res.status).toBe(200) const body = (await res.json()) as { filename: string; markdown: string } expect(body.filename).toBe('CSS 弹性布局.md') expect(body.markdown).toContain('# CSS 弹性布局 教学设计') }) it('returns 502 when Deepseek responds with an error status', async () => { globalThis.fetch = mock(async () => new Response('', { status: 401 })) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: 'CSS 弹性布局' }), }) expect(res.status).toBe(502) }) it('returns 502 when fetch throws', async () => { globalThis.fetch = mock(async () => { throw new Error('network error') }) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: 'CSS 弹性布局' }), }) expect(res.status).toBe(502) }) it('returns 400 when theme is missing on outline', async () => { const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate/outline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) expect(res.status).toBe(400) }) it('uses the requested count in the outline prompt', async () => { let sentBody = '' globalThis.fetch = mock(async (_url: unknown, init: { body?: string } = {}) => { sentBody = init.body ?? '' return new Response( JSON.stringify({ choices: [{ message: { content: 'A\nB\nC' } }] }), { status: 200 }, ) }) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate/outline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ theme: 'Web 前端', count: 25 }), }) expect(res.status).toBe(200) expect(sentBody).toContain('共约25条') const body = (await res.json()) as { titles: string[] } expect(body.titles).toEqual(['A', 'B', 'C']) }) it('defaults the outline count to 18 and clamps out-of-range values', async () => { const captured: string[] = [] globalThis.fetch = mock(async (_url: unknown, init: { body?: string } = {}) => { captured.push(init.body ?? '') return new Response( JSON.stringify({ choices: [{ message: { content: 'A' } }] }), { status: 200 }, ) }) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) await app.request('/api/generate/outline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ theme: 'Web 前端' }), }) await app.request('/api/generate/outline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ theme: 'Web 前端', count: 999 }), }) expect(captured[0]).toContain('共约18条') expect(captured[1]).toContain('共约50条') }) it('returns 502 when Deepseek response has no content', async () => { globalThis.fetch = mock(async () => new Response(JSON.stringify({ choices: [] }), { status: 200 })) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: 'CSS 弹性布局' }), }) expect(res.status).toBe(502) }) it('does not add content to the prompt when topic is sent alone', async () => { let sentBody = '' globalThis.fetch = mock(async (_url: unknown, init: { body?: string } = {}) => { sentBody = init.body ?? '' return new Response( JSON.stringify({ choices: [{ message: { content: '# x 教学设计' } }] }), { status: 200 }, ) }) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: 'CSS 弹性布局' }), }) expect(sentBody).not.toContain('作为编写依据') expect(sentBody).toContain('请围绕主题') expect(sentBody).toContain('生成一份教案。') }) it('injects chapter content into the prompt when content is provided', async () => { let sentBody = '' globalThis.fetch = mock(async (_url: unknown, init: { body?: string } = {}) => { sentBody = init.body ?? '' return new Response( JSON.stringify({ choices: [{ message: { content: '# x 教学设计' } }] }), { status: 200 }, ) }) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: 'CSS 弹性布局', content: '弹性盒模型的正文内容片段' }), }) expect(res.status).toBe(200) expect(sentBody).toContain('作为编写依据') expect(sentBody).toContain('弹性盒模型的正文内容片段') }) it('returns 400 when entries are missing on lessons-from-book', async () => { const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate/lessons-from-book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) expect(res.status).toBe(400) }) it('parses lessons with sourceIndexes from lessons-from-book', async () => { globalThis.fetch = mock(async () => new Response( JSON.stringify({ choices: [ { message: { content: '{"lessons":[{"title":"C# 入门——搭建环境","sourceIndexes":[0]},{"title":"变量与类型","sourceIndexes":[1,2]}]}', }, }, ], }), { status: 200 }, ), ) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate/lessons-from-book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entries: [ { index: 0, title: '第1章 C# 入门', charCount: 5000 }, { index: 1, title: '第2章 变量', charCount: 3000 }, { index: 2, title: '第3章 类型', charCount: 2000 }, ], }), }) expect(res.status).toBe(200) const body = (await res.json()) as { lessons: { title: string; sourceIndexes: number[] }[] } expect(body.lessons).toEqual([ { title: 'C# 入门——搭建环境', sourceIndexes: [0] }, { title: '变量与类型', sourceIndexes: [1, 2] }, ]) }) it('strips code fences and drops out-of-range sourceIndexes on lessons-from-book', async () => { globalThis.fetch = mock(async () => new Response( JSON.stringify({ choices: [ { message: { content: '```json\n{"lessons":[{"title":"课时一","sourceIndexes":[0,9]}]}\n```', }, }, ], }), { status: 200 }, ), ) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate/lessons-from-book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entries: [{ index: 0, title: '第1章', charCount: 100 }] }), }) expect(res.status).toBe(200) const body = (await res.json()) as { lessons: { title: string; sourceIndexes: number[] }[] } expect(body.lessons).toEqual([{ title: '课时一', sourceIndexes: [0] }]) }) it('returns 502 when lessons-from-book JSON is malformed', async () => { globalThis.fetch = mock(async () => new Response( JSON.stringify({ choices: [{ message: { content: '这不是 JSON' } }] }), { status: 200 }, ), ) as unknown as typeof fetch const app = new Hono().route('/api/generate', createGenerateRouter('test-key')) const res = await app.request('/api/generate/lessons-from-book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entries: [{ index: 0, title: '第1章', charCount: 100 }] }), }) expect(res.status).toBe(502) }) })