108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import {
|
|
acquireCourseGenerationActivity,
|
|
ClassroomSourceMutationInProgressError,
|
|
CourseMutationInProgressError,
|
|
CoursePublishInProgressError,
|
|
isCoursePublishing,
|
|
runClassroomSourceDeleteExclusive,
|
|
runClassroomSourcesPublishExclusive,
|
|
runCoursePublishExclusive,
|
|
runCourseRegistryPublishExclusive,
|
|
} from '@/lib/course-framework/publish-state';
|
|
|
|
function deferred() {
|
|
let resolve!: () => void;
|
|
const promise = new Promise<void>((done) => {
|
|
resolve = done;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
|
|
describe('course source mutation gate', () => {
|
|
test('atomically rejects generation while an ops publication is active', async () => {
|
|
const hold = deferred();
|
|
const publishing = runCoursePublishExclusive('course-publish-first', () => hold.promise);
|
|
|
|
expect(isCoursePublishing('course-publish-first')).toBe(true);
|
|
expect(() => acquireCourseGenerationActivity('course-publish-first')).toThrow(
|
|
CourseMutationInProgressError,
|
|
);
|
|
|
|
hold.resolve();
|
|
await publishing;
|
|
expect(isCoursePublishing('course-publish-first')).toBe(false);
|
|
});
|
|
|
|
test('atomically rejects publication while generation is active', async () => {
|
|
const release = acquireCourseGenerationActivity('course-generation-first');
|
|
await expect(
|
|
runCoursePublishExclusive('course-generation-first', async () => undefined),
|
|
).rejects.toBeInstanceOf(CoursePublishInProgressError);
|
|
release();
|
|
|
|
await expect(
|
|
runCoursePublishExclusive('course-generation-first', async () => 'published'),
|
|
).resolves.toBe('published');
|
|
});
|
|
|
|
test('keeps remote ops source and server registry gates in separate namespaces', async () => {
|
|
const registryEntered = deferred();
|
|
const registryRelease = deferred();
|
|
const sourcePublish = runCoursePublishExclusive('remote-course', async () => {
|
|
const registryPublish = runCourseRegistryPublishExclusive('remote-course', async () => {
|
|
registryEntered.resolve();
|
|
await registryRelease.promise;
|
|
});
|
|
await registryEntered.promise;
|
|
registryRelease.resolve();
|
|
await registryPublish;
|
|
});
|
|
|
|
await expect(sourcePublish).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('classroom source publish/delete gate', () => {
|
|
test('rejects source deletion throughout an active publication', async () => {
|
|
const hold = deferred();
|
|
const publishing = runClassroomSourcesPublishExclusive(
|
|
['classroom-published-source'],
|
|
() => hold.promise,
|
|
);
|
|
|
|
await expect(
|
|
runClassroomSourceDeleteExclusive('classroom-published-source', async () => undefined),
|
|
).rejects.toBeInstanceOf(ClassroomSourceMutationInProgressError);
|
|
|
|
hold.resolve();
|
|
await publishing;
|
|
await expect(
|
|
runClassroomSourceDeleteExclusive('classroom-published-source', async () => 'deleted'),
|
|
).resolves.toBe('deleted');
|
|
});
|
|
|
|
test('claims a multi-module publication atomically', async () => {
|
|
const holdDelete = deferred();
|
|
const deleting = runClassroomSourceDeleteExclusive(
|
|
'classroom-module-2',
|
|
() => holdDelete.promise,
|
|
);
|
|
|
|
await expect(
|
|
runClassroomSourcesPublishExclusive(
|
|
['classroom-module-1', 'classroom-module-2'],
|
|
async () => undefined,
|
|
),
|
|
).rejects.toBeInstanceOf(ClassroomSourceMutationInProgressError);
|
|
|
|
// The failed batch must not leave its earlier source claimed.
|
|
await expect(
|
|
runClassroomSourceDeleteExclusive('classroom-module-1', async () => 'deleted'),
|
|
).resolves.toBe('deleted');
|
|
|
|
holdDelete.resolve();
|
|
await deleting;
|
|
});
|
|
});
|