Files
openmaic/OpenMAIC/tests/chat/quiz-results-for-store-state.test.ts
T

122 lines
3.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/lib/quiz/runtime', () => ({
loadQuizAttemptState: vi.fn(),
}));
import {
buildQuizResultsForStoreState,
didActiveSceneRemainUnchanged,
} from '@/lib/chat/quiz-results-for-store-state';
import { loadQuizAttemptState } from '@/lib/quiz/runtime';
describe('quiz results for chat store state', () => {
beforeEach(() => {
vi.mocked(loadQuizAttemptState).mockReset();
});
afterEach(() => {
vi.useRealTimers();
});
it('omits an explicitly reviewed quiz when the grader returned no results', async () => {
vi.mocked(loadQuizAttemptState).mockResolvedValue({
attemptId: 'attempt-1',
state: {
sessionId: 'attempt-1',
status: 'completed',
phase: 'reviewed',
answers: { q1: 'A' },
results: [],
},
});
await expect(
buildQuizResultsForStoreState([{ id: 'quiz-1', type: 'quiz', stageId: 'stage-1' }], 'quiz-1'),
).resolves.toBeUndefined();
expect(loadQuizAttemptState).toHaveBeenCalledWith({
stageId: 'stage-1',
sceneId: 'quiz-1',
});
});
it('includes non-empty reviewed results for the active quiz', async () => {
const results = [{ questionId: 'q1', correct: true, status: 'correct' as const, earned: 1 }];
vi.mocked(loadQuizAttemptState).mockResolvedValue({
attemptId: 'attempt-1',
state: {
sessionId: 'attempt-1',
status: 'completed',
phase: 'reviewed',
answers: { q1: 'A' },
results,
},
});
await expect(
buildQuizResultsForStoreState([{ id: 'quiz-1', type: 'quiz', stageId: 'stage-1' }], 'quiz-1'),
).resolves.toEqual({ sceneId: 'quiz-1', answers: { q1: 'A' }, results });
});
it('omits quiz context when RuntimeStore is unavailable', async () => {
vi.mocked(loadQuizAttemptState).mockRejectedValue(new Error('indexedDB unavailable'));
await expect(
buildQuizResultsForStoreState([{ id: 'quiz-1', type: 'quiz', stageId: 'stage-1' }], 'quiz-1'),
).resolves.toBeUndefined();
});
it('times out a stalled RuntimeStore read instead of blocking chat', async () => {
vi.useFakeTimers();
vi.mocked(loadQuizAttemptState).mockReturnValue(new Promise(() => {}));
const reading = buildQuizResultsForStoreState(
[{ id: 'quiz-1', type: 'quiz', stageId: 'stage-1' }],
'quiz-1',
);
await vi.runAllTimersAsync();
await expect(reading).resolves.toBeUndefined();
});
it('retains quiz results when the active scene object was reallocated in place', () => {
// A store update may replace the scene object (same id) during the async
// quiz read; the learner never left the scene, so their graded answers
// must still reach the outgoing request. The scene ID is the boundary.
const before = {
id: 'quiz-1',
type: 'quiz',
stageId: 'stage-1',
content: { questions: ['old'] },
};
const after = { ...before, content: { questions: ['new'] } };
expect(didActiveSceneRemainUnchanged([before], 'quiz-1', [after], 'quiz-1')).toBe(true);
});
it('rejects quiz results when the active scene left the deck during the read', () => {
const before = { id: 'quiz-1', type: 'quiz', stageId: 'stage-1', content: {} };
expect(didActiveSceneRemainUnchanged([before], 'quiz-1', [], 'quiz-1')).toBe(false);
});
it('retains quiz results when unrelated scenes changed during the read', () => {
const quiz = {
id: 'quiz-1',
type: 'quiz',
stageId: 'stage-1',
content: { questions: ['same'] },
};
const editedSlide = { id: 'slide-1', title: 'edited' };
expect(
didActiveSceneRemainUnchanged(
[quiz, { id: 'slide-1' }],
'quiz-1',
[quiz, editedSlide],
'quiz-1',
),
).toBe(true);
});
});