fix: require evidence for judgment answers and correct legacy banks

This commit is contained in:
suanyuit-cyber
2026-10-30 03:34:38 +08:00
parent 4bf2de1bad
commit 31a2640adb
21 changed files with 4875 additions and 11110 deletions
+138 -1
View File
@@ -64,7 +64,7 @@ test('selected, submitContent, userAnswer and top-level answer/answers are not s
test('judge zero and string zero mean incorrect, never truthy correct', () => {
for (const judgeAnswer of [0, '0', 1, '1', null, '-1']) {
const [q] = extractQuestions({ listQues: [{ id: 'j1', quesType: 2, content: '压力越高越安全。', judgeAnswer }] });
const [q] = extractQuestions({ questionList: [{ id: 'j1', quesType: 2, content: '压力越高越安全。', judgeAnswer }] });
const valid = judgeAnswer === 0 || judgeAnswer === '0' || judgeAnswer === 1 || judgeAnswer === '1';
assert.equal(q.answerStatus, valid ? 'confirmed' : 'missing');
assert.deepEqual(q.answerTexts, valid ? [String(judgeAnswer) === '1' ? '正确' : '错误'] : []);
@@ -738,3 +738,140 @@ test('observer ignores unrelated and selection mutations and batches relevant wo
assert.ok(reads > baseline, '合批后仍检查真实题目更新');
page.adapter.stop();
});
test('unsubmitted listQues judgment placeholders 0 and 1 remain missing without evidence', () => {
for (const judgeAnswer of [0, '0', 1, '1']) {
for (const submitQuesAnswers of [undefined, []]) {
const [record] = extractQuestions({ listQues: [{ id: 'j-unanswered', quesType: 2,
content: '未作答题的占位字段不是标准答案', judgeAnswer, submitQuesAnswers, isAnswered: 1 }] });
assert.equal(record.answerStatus, 'missing');
assert.deepEqual(record.answerTexts, []);
assert.deepEqual(record.correctOptionIds, []);
assert.equal(record.answerEvidence, undefined);
}
}
});
test('submitted practice judgments use judgeAnswer rather than the student submitted choice', () => {
for (const [judgeAnswer, submitted, expected] of [[0, '1', '错误'], [1, '0', '正确'], ['0', '1', '错误'], ['1', '0', '正确']]) {
const [record] = extractQuestions({ listQues: [{ id: 'j-answered', quesType: 2,
content: '已提交后可用标准反馈', judgeAnswer, submitQuesAnswers: [submitted] }] });
assert.equal(record.answerStatus, 'confirmed');
assert.deepEqual(record.answerTexts, [expected]);
assert.equal(record.answerEvidence, 'submitted-list');
}
});
test('an explicit answered resultInfo can confirm a practice judgment independently of placeholders', () => {
const [record] = extractQuestions({ listQues: [{ id: 'j-result', quesType: 2, content: '明确答后结果',
judgeAnswer: 0, submitQuesAnswers: [], resultInfo: { judgeAnswer: 1 } }] });
assert.deepEqual(record.answerTexts, ['正确']);
assert.equal(record.answerEvidence, 'submitted-list');
const [empty] = extractQuestions({ listQues: [{ id: 'j-empty', quesType: 2, content: '空结果对象也不是标准答案',
judgeAnswer: 0, resultInfo: {} }] });
assert.equal(empty.answerStatus, 'missing');
assert.equal(empty.answerEvidence, undefined);
});
test('explicit questionList standards retain 0/1 semantics while parent hidden flags win', () => {
const raw = { id: 'j-standard', quesType: 2, content: '显式题库标准答案', judgeAnswer: 0 };
const [standard] = extractQuestions({ questionList: [raw] }, { url: 'https://api.yunxuetang.cn/ote/pra/p1/questions' });
assert.deepEqual(standard.answerTexts, ['错误']);
assert.equal(standard.answerEvidence, 'explicit-standard');
for (const parent of [{ allowViewSingleQuesAnswer: false }, { info: { allowViewSingleQuesAnswer: 0 } }]) {
const [hidden] = extractQuestions({ ...parent, data: { questionList: [{ ...raw, allowViewSingleQuesAnswer: true }] } });
assert.equal(hidden.answerStatus, 'missing');
assert.equal(hidden.answerEvidence, undefined);
}
});
test('a direct upm start payload cannot bypass the practice-list evidence gate', async () => {
const page = fakePage(async () => new Response(JSON.stringify({ id: 'j-direct', quesType: 2,
content: '起始接口直接对象仍需已答证据', judgeAnswer: 1 }), { headers: { 'content-type': 'application/json' } }));
page.adapter.start();
await page.win.fetch('/upm/wrong/start?praId=p1'); await flush(); await flush();
assert.equal(page.received.at(-1).answerStatus, 'missing');
assert.equal(page.received.at(-1).answerEvidence, undefined);
page.adapter.stop();
});
test('same-question judgment answer receipts confirm standards and anonymize user URL segments', () => {
const page = fakePage();
page.adapter.start();
const initial = new page.win.XMLHttpRequest();
initial.open('GET', '/upm/start?praId=p1'); initial.send();
initial.respond({ listQues: [{ id: 'j1', quesType: 2, content: '断续噪声的判断示例', judgeAnswer: 0, submitQuesAnswers: [] }] });
assert.equal(page.received.at(-1).answerStatus, 'missing');
const answer = new page.win.XMLHttpRequest();
answer.open('PUT', '/upm/p1/users/private-user/wrong/answer');
answer.send(JSON.stringify({ answers: [{ quesId: 'j1', answer: ['0'] }] }));
answer.respond([{ id: 'receipt-id', judgeAnswer: 1, corrected: false }]);
const record = page.received.at(-1);
assert.equal(record.questionId, 'j1');
assert.deepEqual(record.answerTexts, ['正确']);
assert.equal(record.answerEvidence, 'answer-response');
assert.equal(record.source.url, 'https://www.shhuayi-elearning.cn/upm/p1/users/_/wrong/answer');
page.adapter.stop();
});
test('DOM standard feedback carries bound or known trusted answer evidence', async () => {
const raw = single();
raw.choiceItems.forEach(option => delete option.answer);
const page = bankPage(async () => new Response(JSON.stringify({ listQues: [raw] }), { headers: { 'content-type': 'application/json' } }));
page.adapter.start();
await page.win.fetch('/upm/start?praId=bank-a'); await flush(); await flush();
page.state.feedback = [{ heading: '正确答案', answer: 'B' }];
assert.equal(page.adapter.getCurrentQuestion().answerStatus, 'missing', '未验证网络占位题不能替裸DOM反馈提供证据');
page.state.feedback = [{ heading: '正确答案', answer: 'B', questionId: 'q1' }];
assert.equal(page.adapter.getCurrentQuestion().answerEvidence, 'bound-dom');
const answer = new page.win.XMLHttpRequest();
answer.open('PUT', '/upm/bank-a/users/u1/answer');
answer.send(JSON.stringify({ answers: [{ quesId: 'q1', answer: ['a1'] }] }));
answer.respond([{ answers: ['a2'] }]);
page.state.feedback = [{ heading: '正确答案', answer: 'B' }];
assert.equal(page.adapter.getCurrentQuestion().answerEvidence, 'answer-response', '与可信同题反馈一致时保留原始证据');
page.adapter.stop();
});
test('an older answer receipt cannot attach its answer to a newer same-id full snapshot', () => {
const page = fakePage();
page.adapter.start();
const load = content => {
const request = new page.win.XMLHttpRequest();
request.open('GET', '/upm/start?praId=p1'); request.send();
request.respond({ listQues: [{ id: 'same-id', quesType: 2, content, judgeAnswer: 0, submitQuesAnswers: [] }] });
};
load('旧版本题干');
const answer = new page.win.XMLHttpRequest();
answer.open('PUT', '/upm/p1/users/u1/answer');
answer.send(JSON.stringify({ answers: [{ quesId: 'same-id', answer: ['0'] }] }));
load('重新加载后的新版本题干');
const count = page.received.length;
answer.respond([{ judgeAnswer: 1 }]);
assert.equal(page.received.length, count, '旧回执不能用较新缓存题干拼接成已确认答案');
assert.equal(page.received.at(-1).stem, '重新加载后的新版本题干');
assert.equal(page.received.at(-1).answerStatus, 'missing');
page.adapter.stop();
});
test('a newer snapshot of a different bank does not discard an independent answer receipt', () => {
const page = fakePage();
page.adapter.start();
const first = new page.win.XMLHttpRequest();
first.open('GET', '/upm/start?praId=p1'); first.send();
first.respond({ listQues: [{ id: 'q1', quesType: 2, content: '第一题库题目', judgeAnswer: 0, submitQuesAnswers: [] }] });
const answer = new page.win.XMLHttpRequest();
answer.open('PUT', '/upm/p1/users/u1/answer');
answer.send(JSON.stringify({ answers: [{ quesId: 'q1', answer: ['0'] }] }));
page.doc.title = '第二题库';
const second = new page.win.XMLHttpRequest();
second.open('GET', '/upm/start?praId=p2'); second.send();
second.respond({ listQues: [{ id: 'q1', quesType: 2, content: '第二题库题目', judgeAnswer: 0, submitQuesAnswers: [] }] });
answer.respond([{ judgeAnswer: 1 }]);
const received = page.received.at(-1);
assert.equal(received.bankId, 'p1');
assert.equal(received.stem, '第一题库题目');
assert.equal(received.answerEvidence, 'answer-response');
assert.deepEqual(received.answerTexts, ['正确']);
page.adapter.stop();
});