import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { NextRequest } from 'next/server'; const mocks = vi.hoisted(() => ({ readJob: vi.fn(), readClassroom: vi.fn(), finalizeSingle: vi.fn(), finalizeLarge: vi.fn(), })); vi.mock('@/lib/server/classroom-job-store', () => ({ isValidClassroomJobId: () => true, readClassroomGenerationJob: mocks.readJob, })); vi.mock('@/lib/server/classroom-storage', () => ({ buildRequestOrigin: () => 'https://engine.example', readClassroom: mocks.readClassroom, })); vi.mock('@/lib/course-framework/store', () => ({ readCourseRecordReconciled: vi.fn(), })); vi.mock('@/lib/makelore-course/finalize', () => ({ finalizeSingleLearningCourse: mocks.finalizeSingle, finalizeLargeLearningCourse: mocks.finalizeLarge, frozenLearningFinalizeResponse: vi.fn(), })); import { FrozenLearningCourseMutationError } from '@/lib/makelore-course/immutability'; import { POST } from '@/app/api/runtime/v1/courses/jobs/[jobId]/finalize/route'; describe('private Learning Engine finalize route', () => { beforeEach(() => { vi.clearAllMocks(); vi.stubEnv('LEARNING_ENGINE_TOKEN', 'engine-secret'); mocks.readJob.mockResolvedValue({ id: 'job-1', ownerPrincipalId: 'owner-1', status: 'succeeded', result: { classroomId: 'course-1' }, input: { interactiveMode: true, enableTTS: true }, }); mocks.readClassroom.mockResolvedValue({ id: 'course-1', stage: {}, scenes: [] }); }); afterEach(() => { vi.unstubAllEnvs(); }); it('returns a stable conflict when an immutable course source no longer matches', async () => { mocks.finalizeSingle.mockRejectedValue(new FrozenLearningCourseMutationError('course-1')); const request = new NextRequest( 'https://engine.example/api/runtime/v1/courses/jobs/job-1/finalize', { method: 'POST', headers: { Authorization: 'Bearer engine-secret', 'X-Owner-User-Id': 'owner-1', }, }, ); const response = await POST(request, { params: Promise.resolve({ jobId: 'job-1' }) }); expect(response.status).toBe(409); await expect(response.json()).resolves.toMatchObject({ error: 'course_frozen' }); }); });