fix(quiz): skip legacy questions with missing type instead of throwing
All checks were successful
Deploy API Server / build-and-unit (push) Successful in 37s
Deploy API Server / current-integration (push) Successful in 31s
Deploy API Server / backward-compat (push) Successful in 0s
Deploy API Server / deploy (push) Successful in 1m2s

Legacy Runtime test data may contain questions without an explicit
type field. For missing type -> skip with warning (consistent with
missing stem/answer handling). For non-empty illegal type -> throw
BadRequestException (fail-closed).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-24 20:02:15 +08:00
parent f1eb99b6fa
commit 5570c30e0e
2 changed files with 16 additions and 8 deletions

View File

@ -581,7 +581,7 @@ describe('RuntimeInternalService', () => {
});
it('persists quiz_generation output (creates quiz + questions)', async () => {
const qDto = { ...dto, validatedOutput: { questions: [{ stem: 'Q1?', answer: 'A1' }] } };
const qDto = { ...dto, validatedOutput: { questions: [{ type: 'choice', stem: 'Q1?', answer: 'A1' }] } };
const qJob = { ...job, jobType: 'quiz_generation' as const, targetType: 'knowledge_base' as const, targetId: 'kb1' };
mockAiRuntimeJob.findUnique.mockResolvedValue(qJob);
mockAiRuntimeResult.findFirst.mockResolvedValue(null);
@ -902,7 +902,7 @@ describe('RuntimeInternalService', () => {
await service.submitResult('j1', {
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
validatedOutput: { questions: [{ stem: 'Valid', answer: 'A' }, { stem: '', answer: '' }] },
validatedOutput: { questions: [{ type: 'choice', stem: 'Valid', answer: 'A' }, { stem: '', answer: '' }] },
attemptNo: 1, outputHash: 'h1',
});
@ -914,7 +914,7 @@ describe('RuntimeInternalService', () => {
await service.submitResult('j1', {
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
validatedOutput: { questions: [{ stem: '', answer: '' }] },
validatedOutput: { questions: [{ type: 'choice', stem: '', answer: '' }] },
attemptNo: 1, outputHash: 'h2',
});
@ -932,8 +932,8 @@ describe('RuntimeInternalService', () => {
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
validatedOutput: {
questions: [
{ stem: 'Duplicate?', answer: 'A' },
{ stem: 'Unique?', answer: 'B' },
{ type: 'choice', stem: 'Duplicate?', answer: 'A' },
{ type: 'choice', stem: 'Unique?', answer: 'B' },
],
},
attemptNo: 1, outputHash: 'h3',
@ -951,7 +951,7 @@ describe('RuntimeInternalService', () => {
await service.submitResult('j1', {
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
validatedOutput: { questions: [{ stem: 'Q?', answer: 'A' }] },
validatedOutput: { questions: [{ type: 'choice', stem: 'Q?', answer: 'A' }] },
attemptNo: 1, outputHash: 'h4',
});

View File

@ -481,11 +481,19 @@ export class RuntimeInternalService {
continue;
}
// M-QUIZ-01-CLEANUP-01: 缺失 type → 跳过 + 警告(兼容旧数据);非法非空 type → 拒绝
if (!q.type) {
this.logger.warn(`convertQuizCandidates: skipping question with missing type for job=${job.id}`);
continue;
}
if (!isValidQuestionType(q.type)) {
throw new BadRequestException(`Invalid question type: ${q.type}`);
}
await this.prisma.quizQuestion.create({
data: {
quizId: quiz.id,
// M-QUIZ-01-04: fail-closed — 非法 type 记录错误并拒绝
type: isValidQuestionType(q.type) ? q.type! : (() => { throw new BadRequestException(`Invalid question type: ${q.type}`); })(),
type: q.type,
stem,
options: q.options ?? undefined,
answer: q.answer ?? q.correctAnswer,