feat: productionize learning engine and classroom
This commit is contained in:
171
OpenMAIC/tests/app/api/ops-large-course-control-proxy.test.ts
Normal file
171
OpenMAIC/tests/app/api/ops-large-course-control-proxy.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
requireOpsAccess: vi.fn(),
|
||||
readCourse: vi.fn(),
|
||||
isRunning: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
runFramework: vi.fn(),
|
||||
runModules: vi.fn(),
|
||||
regenerateFramework: vi.fn(),
|
||||
regenerateModule: vi.fn(),
|
||||
isPublishing: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/ops-access', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/lib/server/ops-access')>()),
|
||||
requireOpsAccess: mocks.requireOpsAccess,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/course-framework/store', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/lib/course-framework/store')>()),
|
||||
readCourseRecordReconciled: mocks.readCourse,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/course-framework/runner', () => ({
|
||||
isCourseGenerationRunning: mocks.isRunning,
|
||||
cancelCourseGenerationJob: mocks.cancel,
|
||||
runCourseFrameworkGeneration: mocks.runFramework,
|
||||
runCourseModuleGeneration: mocks.runModules,
|
||||
regenerateCourseFramework: mocks.regenerateFramework,
|
||||
runCourseGenerationJob: mocks.regenerateModule,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/course-framework/publish-state', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/lib/course-framework/publish-state')>()),
|
||||
isCoursePublishing: mocks.isPublishing,
|
||||
}));
|
||||
|
||||
import { POST as startCourse } from '@/app/api/courses/[courseId]/start/route';
|
||||
import { POST as cancelCourse } from '@/app/api/courses/[courseId]/cancel/route';
|
||||
import { POST as resumeCourse } from '@/app/api/courses/[courseId]/resume/route';
|
||||
import { POST as regenerateCourseFramework } from '@/app/api/courses/[courseId]/framework/regenerate/route';
|
||||
import { POST as regenerateCourseModule } from '@/app/api/courses/[courseId]/modules/[index]/regenerate/route';
|
||||
|
||||
const COURSE = {
|
||||
id: 'large-course-1',
|
||||
ownerPrincipalId: 'works-owner-1',
|
||||
status: 'framework_ready',
|
||||
requirement: 'Build a course',
|
||||
framework: { courseTitle: 'Course', modules: [] },
|
||||
modules: [{ index: 2, title: 'Module 2', description: 'Two', status: 'failed' }],
|
||||
createdAt: '2026-08-16T00:00:00.000Z',
|
||||
updatedAt: '2026-08-16T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('Learning Ops large-course runner proxy', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'ops');
|
||||
vi.stubEnv('LEARNING_ENGINE_BASE_URL', 'http://learning-engine:3000/api/runtime/v1');
|
||||
vi.stubEnv('COURSE_PUBLISH_SERVER_BASE_URL', '');
|
||||
vi.stubEnv('LEARNING_ENGINE_TOKEN', 'engine-control-secret');
|
||||
mocks.requireOpsAccess.mockResolvedValue(null);
|
||||
mocks.readCourse.mockResolvedValue(COURSE);
|
||||
vi.stubGlobal('fetch', vi.fn(async () => Response.json(
|
||||
{ contractVersion: 2, jobId: COURSE.id, mode: 'large', status: 'generating' },
|
||||
{ status: 202 },
|
||||
)));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const cases = [
|
||||
{
|
||||
action: 'start',
|
||||
call: (request: NextRequest) => startCourse(request, {
|
||||
params: Promise.resolve({ courseId: COURSE.id }),
|
||||
}),
|
||||
},
|
||||
{
|
||||
action: 'cancel',
|
||||
call: (request: NextRequest) => cancelCourse(request, {
|
||||
params: Promise.resolve({ courseId: COURSE.id }),
|
||||
}),
|
||||
},
|
||||
{
|
||||
action: 'resume',
|
||||
call: (request: NextRequest) => resumeCourse(request, {
|
||||
params: Promise.resolve({ courseId: COURSE.id }),
|
||||
}),
|
||||
},
|
||||
{
|
||||
action: 'framework-regenerate',
|
||||
call: (request: NextRequest) => regenerateCourseFramework(request, {
|
||||
params: Promise.resolve({ courseId: COURSE.id }),
|
||||
}),
|
||||
},
|
||||
{
|
||||
action: 'module-regenerate',
|
||||
moduleIndex: 2,
|
||||
call: (request: NextRequest) => regenerateCourseModule(request, {
|
||||
params: Promise.resolve({ courseId: COURSE.id, index: '2' }),
|
||||
}),
|
||||
},
|
||||
] as const;
|
||||
|
||||
it.each(cases)('proxies $action to Engine without starting an Ops-local runner', async ({
|
||||
action,
|
||||
call,
|
||||
...testCase
|
||||
}) => {
|
||||
const response = await call(new NextRequest(
|
||||
`https://ops.example/learning-ops/api/courses/${COURSE.id}/${action}`,
|
||||
{ method: 'POST' },
|
||||
));
|
||||
|
||||
expect(response.status).toBe(202);
|
||||
expect(fetch).toHaveBeenCalledOnce();
|
||||
const [url, init] = vi.mocked(fetch).mock.calls[0]!;
|
||||
expect(url).toBe(
|
||||
`http://learning-engine:3000/api/runtime/v1/courses/jobs/${COURSE.id}/control`,
|
||||
);
|
||||
const headers = new Headers(init?.headers);
|
||||
expect(headers.get('Authorization')).toBe('Bearer engine-control-secret');
|
||||
expect(headers.get('X-Owner-User-Id')).toBe('works-owner-1');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({
|
||||
action,
|
||||
...('moduleIndex' in testCase ? { moduleIndex: testCase.moduleIndex } : {}),
|
||||
});
|
||||
expect(mocks.runFramework).not.toHaveBeenCalled();
|
||||
expect(mocks.runModules).not.toHaveBeenCalled();
|
||||
expect(mocks.regenerateFramework).not.toHaveBeenCalled();
|
||||
expect(mocks.regenerateModule).not.toHaveBeenCalled();
|
||||
expect(mocks.cancel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails closed instead of using a local runner when an Ops deployment has no Engine URL', async () => {
|
||||
vi.stubEnv('LEARNING_ENGINE_BASE_URL', '');
|
||||
vi.stubEnv('COURSE_PUBLISH_SERVER_BASE_URL', '');
|
||||
|
||||
const response = await startCourse(
|
||||
new NextRequest(`https://ops.example/api/courses/${COURSE.id}/start`, { method: 'POST' }),
|
||||
{ params: Promise.resolve({ courseId: COURSE.id }) },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
expect(mocks.runModules).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reuses the configured publish-server origin for existing deployments', async () => {
|
||||
vi.stubEnv('LEARNING_ENGINE_BASE_URL', '');
|
||||
vi.stubEnv('COURSE_PUBLISH_SERVER_BASE_URL', 'http://learning-engine:3000');
|
||||
|
||||
const response = await cancelCourse(
|
||||
new NextRequest(`https://ops.example/api/courses/${COURSE.id}/cancel`, { method: 'POST' }),
|
||||
{ params: Promise.resolve({ courseId: COURSE.id }) },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(202);
|
||||
expect(vi.mocked(fetch).mock.calls[0]?.[0]).toBe(
|
||||
`http://learning-engine:3000/api/runtime/v1/courses/jobs/${COURSE.id}/control`,
|
||||
);
|
||||
expect(mocks.cancel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user