fix(quiz): skip legacy questions with missing type instead of throwing
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:
parent
f1eb99b6fa
commit
5570c30e0e
@ -581,7 +581,7 @@ describe('RuntimeInternalService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('persists quiz_generation output (creates quiz + questions)', async () => {
|
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' };
|
const qJob = { ...job, jobType: 'quiz_generation' as const, targetType: 'knowledge_base' as const, targetId: 'kb1' };
|
||||||
mockAiRuntimeJob.findUnique.mockResolvedValue(qJob);
|
mockAiRuntimeJob.findUnique.mockResolvedValue(qJob);
|
||||||
mockAiRuntimeResult.findFirst.mockResolvedValue(null);
|
mockAiRuntimeResult.findFirst.mockResolvedValue(null);
|
||||||
@ -902,7 +902,7 @@ describe('RuntimeInternalService', () => {
|
|||||||
|
|
||||||
await service.submitResult('j1', {
|
await service.submitResult('j1', {
|
||||||
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
|
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',
|
attemptNo: 1, outputHash: 'h1',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -914,7 +914,7 @@ describe('RuntimeInternalService', () => {
|
|||||||
|
|
||||||
await service.submitResult('j1', {
|
await service.submitResult('j1', {
|
||||||
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
|
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
|
||||||
validatedOutput: { questions: [{ stem: '', answer: '' }] },
|
validatedOutput: { questions: [{ type: 'choice', stem: '', answer: '' }] },
|
||||||
attemptNo: 1, outputHash: 'h2',
|
attemptNo: 1, outputHash: 'h2',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -932,8 +932,8 @@ describe('RuntimeInternalService', () => {
|
|||||||
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
|
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
|
||||||
validatedOutput: {
|
validatedOutput: {
|
||||||
questions: [
|
questions: [
|
||||||
{ stem: 'Duplicate?', answer: 'A' },
|
{ type: 'choice', stem: 'Duplicate?', answer: 'A' },
|
||||||
{ stem: 'Unique?', answer: 'B' },
|
{ type: 'choice', stem: 'Unique?', answer: 'B' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
attemptNo: 1, outputHash: 'h3',
|
attemptNo: 1, outputHash: 'h3',
|
||||||
@ -951,7 +951,7 @@ describe('RuntimeInternalService', () => {
|
|||||||
|
|
||||||
await service.submitResult('j1', {
|
await service.submitResult('j1', {
|
||||||
runtimeInstanceId: 'rt-1', schemaVersion: 'ov1', status: 'succeeded',
|
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',
|
attemptNo: 1, outputHash: 'h4',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -481,11 +481,19 @@ export class RuntimeInternalService {
|
|||||||
continue;
|
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({
|
await this.prisma.quizQuestion.create({
|
||||||
data: {
|
data: {
|
||||||
quizId: quiz.id,
|
quizId: quiz.id,
|
||||||
// M-QUIZ-01-04: fail-closed — 非法 type 记录错误并拒绝
|
type: q.type,
|
||||||
type: isValidQuestionType(q.type) ? q.type! : (() => { throw new BadRequestException(`Invalid question type: ${q.type}`); })(),
|
|
||||||
stem,
|
stem,
|
||||||
options: q.options ?? undefined,
|
options: q.options ?? undefined,
|
||||||
answer: q.answer ?? q.correctAnswer,
|
answer: q.answer ?? q.correctAnswer,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user