fix: require evidence for judgment answers and correct legacy banks
This commit is contained in:
@@ -780,3 +780,196 @@ test('large repeated alias banks preserve every canonical record and complete CS
|
||||
assert.equal(compacted.removed, 0);
|
||||
assert.equal(JSON.stringify(compacted.records), JSON.stringify(records));
|
||||
});
|
||||
|
||||
const legacyJudge = (overrides = {}) => fixture({
|
||||
type: 'judge', stem: '断续噪声对人体危害更大。',
|
||||
options: [{ id: '1', text: '正确', label: '正确' }, { id: '0', text: '错误', label: '错误' }],
|
||||
correctOptionIds: ['0'], answerTexts: ['错误'], questionIndex: 66, contentOccurrence: 0,
|
||||
questionIdAliases: ['old-question'],
|
||||
source: { kind: 'network', url: 'https://api.example.test/ote/upm/start', observedAt: '2026-10-29T00:00:00Z', batchId: 'old-list' },
|
||||
...overrides,
|
||||
});
|
||||
const officialJudgeSource = (suffix = 'answer') => ({ kind: 'network',
|
||||
url: `https://api.example.test/ote/upm/practice-id/users/_/${suffix}`, observedAt: '2026-10-30T00:00:00Z' });
|
||||
const judgeSnapshot = (record) => ({ reason: '同一题目出现不同或不一致的标准答案', stem: record.stem,
|
||||
options: record.options, answerTexts: record.answerTexts, correctOptionIds: record.correctOptionIds,
|
||||
source: record.answerSource || record.source, ...(record.answerEvidence ? { answerEvidence: record.answerEvidence } : {}) });
|
||||
|
||||
test('unverified legacy practice-list judge values 0 and 1 both become missing without guessing', () => {
|
||||
const input = [legacyJudge(), legacyJudge({ questionId: 'true-placeholder', correctOptionIds: ['1'], answerTexts: ['正确'],
|
||||
source: { kind: 'network', url: 'https://api.example.test/ote/upm/wrong/start?lang=zh', observedAt: '2026-10-29T00:00:00Z' } })];
|
||||
const raw = JSON.stringify(input);
|
||||
const result = core.repairLegacyJudgeAnswers(input);
|
||||
assert.equal(result.records.length, input.length);
|
||||
assert.equal(result.changed.length, 2);
|
||||
result.records.forEach((record, index) => {
|
||||
assert.equal(record.answerStatus, 'missing');
|
||||
assert.deepEqual(record.answerTexts, []);
|
||||
assert.deepEqual(record.correctOptionIds, []);
|
||||
assert.deepEqual(record.conflicts, []);
|
||||
assert.equal(record.answerEvidence, undefined);
|
||||
assert.equal(record.answerSource, undefined);
|
||||
assert.equal(record.stem, input[index].stem);
|
||||
assert.equal(record.questionIndex, input[index].questionIndex);
|
||||
assert.equal(record.contentOccurrence, input[index].contentOccurrence);
|
||||
assert.deepEqual(record.questionIdAliases, [...input[index].questionIdAliases, input[index].questionId]);
|
||||
assert.strictEqual(result.changed[index].original, input[index]);
|
||||
assert.equal(core.mapAnswers(record, input[index]).status, 'unmapped');
|
||||
});
|
||||
assert.equal(JSON.stringify(input), raw);
|
||||
assert.deepEqual(core.fromCSV(core.toCSV(result.records)), result.records);
|
||||
assert.equal(core.search(result.records, '错误', { field: 'answer' }).length, 0);
|
||||
const again = core.repairLegacyJudgeAnswers(result.records);
|
||||
assert.strictEqual(again.records, result.records);
|
||||
assert.deepEqual(again.changed, []);
|
||||
});
|
||||
|
||||
test('answer evidence validates and survives normalization, conflict snapshots, JSON and CSV', () => {
|
||||
for (const answerEvidence of ['submitted-list', 'answer-response', 'bound-dom', 'explicit-standard']) {
|
||||
const record = core.normalizeRecord(legacyJudge({ answerEvidence }));
|
||||
assert.equal(record.answerEvidence, answerEvidence);
|
||||
assert.deepEqual(core.normalizeRecord(JSON.parse(JSON.stringify(record))), record);
|
||||
assert.deepEqual(core.fromCSV(core.toCSV([record])), [record]);
|
||||
assert.strictEqual(core.repairLegacyJudgeAnswers([record]).records[0], record);
|
||||
const merged = core.mergeRecord(record, { ...record, answerStatus: 'missing', answerEvidence: undefined,
|
||||
answerTexts: [], correctOptionIds: [], source: { kind: 'dom', observedAt: '2026-10-30T01:00:00Z' } });
|
||||
assert.equal(merged.answerEvidence, answerEvidence);
|
||||
assert.deepEqual(merged.answerSource, record.answerSource);
|
||||
const repeated = core.mergeRecord(record, legacyJudge({ source: { ...record.source, observedAt: '2026-10-30T02:00:00Z' } }));
|
||||
assert.equal(repeated.answerEvidence, answerEvidence);
|
||||
assert.deepEqual(repeated.answerSource, record.answerSource);
|
||||
assert.equal(repeated.source.observedAt, '2026-10-30T02:00:00Z');
|
||||
const conflict = core.mergeRecord(record, legacyJudge({ answerEvidence: 'bound-dom', correctOptionIds: ['1'], answerTexts: ['正确'] }));
|
||||
assert.ok(conflict.conflicts.some(snapshot => snapshot.answerEvidence === answerEvidence));
|
||||
assert.ok(conflict.conflicts.some(snapshot => snapshot.answerEvidence === 'bound-dom'));
|
||||
}
|
||||
assert.throws(() => core.normalizeRecord(legacyJudge({ answerEvidence: 'unsubmitted-list' })), /无效答案证据/);
|
||||
});
|
||||
|
||||
test('manual standard imports, non-judge types and unrelated sources retain their answers', () => {
|
||||
const input = [
|
||||
legacyJudge({ source: { kind: 'csv-import', observedAt: '2026-10-29T00:00:00Z' } }),
|
||||
legacyJudge({ source: { kind: 'network', url: 'https://example.test/upm/startAgain', observedAt: '2026-10-29T00:00:00Z' } }),
|
||||
fixture({ source: legacyJudge().source }),
|
||||
legacyJudge({ source: officialJudgeSource() }),
|
||||
legacyJudge({ answerEvidence: 'bound-dom', source: { kind: 'dom', observedAt: '2026-10-29T00:00:00Z' } }),
|
||||
];
|
||||
const result = core.repairLegacyJudgeAnswers(input);
|
||||
assert.strictEqual(result.records, input);
|
||||
assert.deepEqual(result.changed, []);
|
||||
const csv = core.fromCSV('题型,题干,选项A,选项B,正确答案内容\r\n判断题,人工明确标准,正确,错误,正确\r\n');
|
||||
assert.equal(core.repairLegacyJudgeAnswers(csv).records[0].answerStatus, 'confirmed');
|
||||
});
|
||||
|
||||
test('DOM-inherited conflicts containing only unverified list values become missing', () => {
|
||||
const old = core.normalizeRecord(legacyJudge());
|
||||
const later = core.normalizeRecord(legacyJudge({ correctOptionIds: ['1'], answerTexts: ['正确'] }));
|
||||
const record = core.normalizeRecord({ ...later, answerStatus: 'conflict',
|
||||
source: { kind: 'dom', observedAt: '2026-10-30T00:00:00Z' }, answerSource: { kind: 'dom', observedAt: '2026-10-30T00:00:00Z' },
|
||||
conflicts: [judgeSnapshot(old), judgeSnapshot(later)] });
|
||||
const result = core.repairLegacyJudgeAnswers([record]);
|
||||
assert.equal(result.records[0].answerStatus, 'missing');
|
||||
assert.deepEqual(result.records[0].conflicts, []);
|
||||
assert.deepEqual(result.records[0].answerTexts, []);
|
||||
assert.equal(result.changed.length, 1);
|
||||
});
|
||||
|
||||
test('standalone old DOM judge answers without retained list history are quarantined with their raw originals', () => {
|
||||
const source = { kind: 'dom', url: 'https://practice.example.test/', observedAt: '2026-10-29T00:00:00Z' };
|
||||
const input = [legacyJudge({ source }), legacyJudge({ source, questionId: 'dom-true', correctOptionIds: ['1'], answerTexts: ['正确'] })];
|
||||
const raw = JSON.stringify(input);
|
||||
const result = core.repairLegacyJudgeAnswers(input);
|
||||
assert.equal(result.changed.length, 2);
|
||||
result.records.forEach((record, index) => {
|
||||
assert.equal(record.answerStatus, 'missing');
|
||||
assert.deepEqual(record.answerTexts, []);
|
||||
assert.deepEqual(record.correctOptionIds, []);
|
||||
assert.strictEqual(result.changed[index].original, input[index]);
|
||||
});
|
||||
assert.equal(JSON.stringify(input), raw);
|
||||
assert.deepEqual(core.repairLegacyJudgeAnswers(result.records).changed, []);
|
||||
const proven = input.map(record => core.normalizeRecord({ ...record, answerEvidence: 'bound-dom' }));
|
||||
assert.strictEqual(core.repairLegacyJudgeAnswers(proven).records, proven);
|
||||
});
|
||||
|
||||
test('standalone DOM provenance retains genuine receipts and recovers trusted snapshots without list history', () => {
|
||||
const dom = { kind: 'dom', url: 'https://practice.example.test/', observedAt: '2026-10-29T00:00:00Z' };
|
||||
const receipt = core.normalizeRecord(legacyJudge({ source: officialJudgeSource(), correctOptionIds: ['1'], answerTexts: ['正确'] }));
|
||||
const preserved = core.normalizeRecord({ ...receipt, source: dom });
|
||||
assert.strictEqual(core.repairLegacyJudgeAnswers([preserved]).records[0], preserved);
|
||||
const old = core.normalizeRecord(legacyJudge({ source: dom }));
|
||||
const conflict = { ...old, answerStatus: 'conflict', conflicts: [judgeSnapshot(old), judgeSnapshot(receipt)] };
|
||||
const repaired = core.repairLegacyJudgeAnswers([conflict]).records[0];
|
||||
assert.equal(repaired.answerStatus, 'confirmed');
|
||||
assert.equal(repaired.answerEvidence, 'answer-response');
|
||||
assert.deepEqual(repaired.answerTexts, ['正确']);
|
||||
assert.deepEqual(repaired.answerSource, receipt.source);
|
||||
assert.deepEqual(repaired.conflicts, []);
|
||||
const onlyDom = { ...old, answerStatus: 'conflict', conflicts: [judgeSnapshot(old), { ...judgeSnapshot(old), answerTexts: ['正确'], correctOptionIds: ['1'] }] };
|
||||
assert.equal(core.repairLegacyJudgeAnswers([onlyDom]).records[0].answerStatus, 'missing');
|
||||
});
|
||||
|
||||
test('a real answer receipt recovers the standard from legacy conflicts with atomic option IDs', () => {
|
||||
for (const suffix of ['answer', 'wrong/answer']) {
|
||||
const old = core.normalizeRecord(legacyJudge());
|
||||
const receipt = core.normalizeRecord(legacyJudge({
|
||||
options: [{ id: 'new-false', text: '错误', label: 'A' }, { id: 'new-true', text: '正确', label: 'B' }],
|
||||
correctOptionIds: ['new-true'], answerTexts: ['正确'], source: officialJudgeSource(suffix),
|
||||
}));
|
||||
const contaminated = { ...old, answerStatus: 'conflict', conflicts: [judgeSnapshot(old), judgeSnapshot(receipt)] };
|
||||
const result = core.repairLegacyJudgeAnswers([contaminated]);
|
||||
const repaired = result.records[0];
|
||||
assert.equal(repaired.answerStatus, 'confirmed');
|
||||
assert.equal(repaired.answerEvidence, 'answer-response');
|
||||
assert.deepEqual(repaired.answerTexts, ['正确']);
|
||||
assert.deepEqual(repaired.correctOptionIds, ['new-true']);
|
||||
assert.deepEqual(repaired.options, receipt.options);
|
||||
assert.deepEqual(repaired.answerSource, receipt.source);
|
||||
assert.deepEqual(repaired.source, old.source);
|
||||
assert.deepEqual(repaired.conflicts, []);
|
||||
assert.equal(core.mapAnswers(repaired, receipt).status, 'matched');
|
||||
assert.strictEqual(result.changed[0].original, contaminated);
|
||||
assert.deepEqual(core.repairLegacyJudgeAnswers(result.records).changed, []);
|
||||
}
|
||||
const receipt = core.normalizeRecord(legacyJudge({ source: officialJudgeSource() }));
|
||||
const observedAgain = core.mergeRecord(receipt, legacyJudge());
|
||||
assert.deepEqual(observedAgain.answerSource, receipt.source);
|
||||
assert.deepEqual(core.repairLegacyJudgeAnswers([observedAgain]).changed, []);
|
||||
});
|
||||
|
||||
test('proven submitted or bound answers replace placeholders while two proven answers remain conflicting', () => {
|
||||
const old = core.normalizeRecord(legacyJudge());
|
||||
const first = core.normalizeRecord(legacyJudge({ answerEvidence: 'submitted-list', correctOptionIds: ['1'], answerTexts: ['正确'] }));
|
||||
const second = core.normalizeRecord(legacyJudge({ answerEvidence: 'bound-dom' }));
|
||||
const good = core.repairLegacyJudgeAnswers([{ ...first, answerStatus: 'conflict', conflicts: [judgeSnapshot(old), judgeSnapshot(first)] }]).records[0];
|
||||
assert.equal(good.answerStatus, 'confirmed');
|
||||
assert.deepEqual(good.answerTexts, ['正确']);
|
||||
const conflict = core.repairLegacyJudgeAnswers([{ ...second, answerStatus: 'conflict',
|
||||
conflicts: [judgeSnapshot(old), judgeSnapshot(first), judgeSnapshot(second)] }]).records[0];
|
||||
assert.equal(conflict.answerStatus, 'conflict');
|
||||
assert.ok(conflict.conflicts.every(snapshot => snapshot.answerEvidence));
|
||||
assert.deepEqual(new Set(conflict.conflicts.flatMap(snapshot => snapshot.answerTexts)), new Set(['正确', '错误']));
|
||||
assert.equal(core.mapAnswers(conflict, first).status, 'ambiguous');
|
||||
});
|
||||
|
||||
test('conflict dedup retains independent standard provenance when its answer equals an old placeholder', () => {
|
||||
const old = core.normalizeRecord(legacyJudge());
|
||||
const trueReceipt = core.normalizeRecord(legacyJudge({ source: officialJudgeSource(), correctOptionIds: ['1'], answerTexts: ['正确'], answerEvidence: 'answer-response' }));
|
||||
const falseReceipt = core.normalizeRecord(legacyJudge({ source: officialJudgeSource(), answerEvidence: 'answer-response' }));
|
||||
const merged = core.mergeRecord(core.mergeRecord(old, trueReceipt), falseReceipt);
|
||||
assert.ok(merged.conflicts.some(snapshot => snapshot.source.url.endsWith('/start')));
|
||||
assert.ok(merged.conflicts.some(snapshot => snapshot.answerTexts[0] === '错误' && snapshot.answerEvidence === 'answer-response'));
|
||||
const inherited = { ...merged, answerSource: { kind: 'dom', observedAt: '2026-10-30T01:00:00Z' }, answerEvidence: undefined };
|
||||
const repaired = core.repairLegacyJudgeAnswers([inherited]).records[0];
|
||||
assert.equal(repaired.answerStatus, 'conflict');
|
||||
assert.ok(repaired.conflicts.every(snapshot => snapshot.answerEvidence === 'answer-response'));
|
||||
});
|
||||
|
||||
test('unknown network conflict evidence is retained conservatively rather than restored as a standard', () => {
|
||||
const old = core.normalizeRecord(legacyJudge());
|
||||
const unknown = { ...judgeSnapshot(old), source: { kind: 'network', url: 'https://example.test/unknown-standard', observedAt: '2026-10-30T00:00:00Z' } };
|
||||
const repaired = core.repairLegacyJudgeAnswers([{ ...old, answerStatus: 'conflict', conflicts: [judgeSnapshot(old), unknown] }]).records[0];
|
||||
assert.equal(repaired.answerStatus, 'conflict');
|
||||
assert.deepEqual(repaired.answerTexts, []);
|
||||
assert.deepEqual(repaired.conflicts, [unknown]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user