feat: publish verified five-bank question dataset
This commit is contained in:
+77
-4
@@ -34,9 +34,18 @@ test('same ID with different content is not treated as the answer', () => {
|
||||
assert.equal(core.mapAnswers(record, current).status, 'unmapped');
|
||||
});
|
||||
|
||||
test('duplicate option content is ambiguous even when ID exists', () => {
|
||||
test('duplicate option content maps only with complete unchanged option ID identities', () => {
|
||||
const record = core.normalizeRecord(fixture({ options: [{ id: 'x', text: '提高温度', label: 'A' }, { id: 'y', text: '提高温度', label: 'B' }] }));
|
||||
assert.equal(core.mapAnswers(record, record).status, 'ambiguous');
|
||||
assert.deepEqual(core.mapAnswers(record, record), { status: 'matched', labels: ['B'], answerTexts: ['提高温度'] });
|
||||
const reordered = { ...record, options: [...record.options].reverse().map((option, index) => ({ ...option, label: 'AB'[index] })) };
|
||||
assert.deepEqual(core.mapAnswers(record, reordered).labels, ['A']);
|
||||
const newIds = { ...reordered, options: reordered.options.map((option, index) => ({ ...option, id: `new-${index}` })) };
|
||||
assert.equal(core.mapAnswers(record, newIds).status, 'ambiguous');
|
||||
const oneChangedId = { ...reordered, options: reordered.options.map((option, index) => index ? { ...option, id: 'new-id' } : option) };
|
||||
assert.equal(core.mapAnswers(record, oneChangedId).status, 'ambiguous');
|
||||
assert.equal(core.mapAnswers({ ...record, correctOptionIds: [] }, record).status, 'ambiguous');
|
||||
assert.equal(core.mapAnswers(record, { ...record, options: record.options.map(option => ({ ...option, id: '' })) }).status, 'ambiguous');
|
||||
assert.ok(core.toCSV([record]).includes('"B"'), 'self export should preserve the observed correct letter');
|
||||
});
|
||||
|
||||
const repeatedMultiple = (overrides = {}) => fixture({
|
||||
@@ -73,10 +82,12 @@ test('all correct occurrences of repeated multiple-choice text map after IDs and
|
||||
assert.deepEqual(core.mapAnswers(subset, current).labels, ['A', 'D']);
|
||||
});
|
||||
|
||||
test('partly correct or text-only repeated options remain ambiguous', () => {
|
||||
test('partly correct repeated options require complete IDs; text-only repeated options remain ambiguous', () => {
|
||||
const partial = core.normalizeRecord(repeatedMultiple({ correctOptionIds: ['b'], answerTexts: ['下降'] }));
|
||||
assert.equal(partial.answerStatus, 'confirmed');
|
||||
assert.equal(core.mapAnswers(partial, partial).status, 'ambiguous');
|
||||
assert.deepEqual(core.mapAnswers(partial, partial).labels, ['B']);
|
||||
const changedIds = { ...partial, options: partial.options.map((option, index) => ({ ...option, id: `new-${index}` })) };
|
||||
assert.equal(core.mapAnswers(partial, changedIds).status, 'ambiguous');
|
||||
const textOnly = core.normalizeRecord(repeatedMultiple({ correctOptionIds: [], answerTexts: ['下降'] }));
|
||||
assert.equal(core.mapAnswers(textOnly, textOnly).status, 'ambiguous');
|
||||
});
|
||||
@@ -240,6 +251,68 @@ test('image upgrade refuses partial, ambiguous or semantically changed evidence'
|
||||
});
|
||||
});
|
||||
|
||||
function mathEntityFixture() {
|
||||
const value = imageUpgradeFixture();
|
||||
value.existing[0].stem = '传热速率=系数×推动力,允许偏差±2。';
|
||||
value.incoming[0].stem = '传热速率=系数×推动力,允许偏差±2。';
|
||||
return value;
|
||||
}
|
||||
|
||||
test('complete source snapshot repairs only the evidenced times and plusmn spellings', () => {
|
||||
const { existing, incoming, context } = mathEntityFixture();
|
||||
const original = JSON.stringify([existing, incoming]);
|
||||
assert.equal(core.mapAnswers(existing[0], incoming[0]).status, 'unmapped', 'ordinary comparisons must not decode literal entities');
|
||||
const result = core.upgradeLegacyMathEntities(existing, incoming, context);
|
||||
assert.equal(result.upgraded.length, 1);
|
||||
assert.equal(result.upgraded[0].kind, 'math-entity');
|
||||
assert.equal(result.existing.length, 1);
|
||||
assert.ok(result.incoming[0].questionIdAliases.includes(existing[0].questionId));
|
||||
assert.equal(JSON.stringify([existing, incoming]), original);
|
||||
assert.equal(core.compactRecords(core.upsert(result.existing, result.incoming)).records.length, 2);
|
||||
const alreadyDuplicated = core.upgradeLegacyMathEntities([...existing, incoming[0]], incoming, context);
|
||||
assert.equal(alreadyDuplicated.upgraded.length, 1, 'a previous failed migration containing both representations must recover');
|
||||
assert.equal(core.compactRecords(core.upsert(alreadyDuplicated.existing, alreadyDuplicated.incoming)).records.length, 2);
|
||||
const sameId = mathEntityFixture(); sameId.incoming[0].questionId = sameId.existing[0].questionId;
|
||||
assert.equal(core.upgradeLegacyMathEntities(sameId.existing, sameId.incoming, context).upgraded.length, 1);
|
||||
|
||||
const options = imageUpgradeFixture();
|
||||
options.incoming[0].stem = options.existing[0].stem;
|
||||
options.existing[0].options[0].text = '12+1×2×2';
|
||||
options.incoming[0].options[0].text = '12+1×2×2';
|
||||
assert.equal(core.upgradeLegacyMathEntities(options.existing, options.incoming, context).upgraded.length, 1, 'non-correct option text can be completed without changing the standard answer');
|
||||
});
|
||||
|
||||
test('math entity completion retains literal entities, missing positions and real differences', async t => {
|
||||
const cases = [
|
||||
['incomplete snapshot', value => { value.incoming.pop(); }],
|
||||
['unverified page count', value => { value.context.expectedCount = 3; }],
|
||||
['not a full snapshot', value => { value.context.fullSnapshot = false; }],
|
||||
['wrong bank', value => { value.context.bankId = 'other-bank'; }],
|
||||
['old position missing', value => { delete value.existing[0].questionIndex; }],
|
||||
['different position', value => { value.existing[0].questionIndex = 1; }],
|
||||
['new positions overlap', value => { value.incoming[1].questionIndex = 0; }],
|
||||
['different option ID identity', value => { value.incoming[0].options[0].id = 'replacement-id'; }],
|
||||
['different standard answer', value => { value.incoming[0].correctOptionIds = ['x']; value.incoming[0].answerTexts = ['降低温度']; }],
|
||||
['unknown entity outside the whitelist', value => { value.existing[0].stem = '系数÷推动力'; value.incoming[0].stem = '系数÷推动力'; }],
|
||||
['literal entity remains in the new question', value => { value.incoming[0].stem = value.existing[0].stem; }],
|
||||
['part of the entity text intentionally remains literal', value => { value.incoming[0].stem = '传热速率=系数×推动力,允许偏差±2。'; }],
|
||||
['different operator', value => { value.incoming[0].stem = '传热速率=系数x推动力,允许偏差±2。'; }],
|
||||
['different image content', value => { value.existing[0].stem += '[图片:https://example.test/a.png]'; value.incoming[0].stem += '[图片:https://example.test/b.png]'; }],
|
||||
['same legacy spelling occurs twice', value => { value.existing.push({ ...value.existing[0], questionId: 'second-literal', questionIndex: 4 }); }],
|
||||
['separate modern question at a different position', value => { value.existing.push({ ...value.incoming[0], questionIndex: 4 }); }],
|
||||
['a true second question intentionally uses literal spelling', value => { value.incoming[1] = { ...value.existing[0], questionId: 'literal-second', questionIndex: 1, source: value.incoming[1].source }; }],
|
||||
['old source is not a network list', value => { value.existing[0].source.kind = 'dom'; }],
|
||||
['mixed incoming batches', value => { value.incoming[1].source.batchId = 'different'; }],
|
||||
['same batch is not evidence of an upgrade', value => { value.existing[0].source.batchId = value.incoming[0].source.batchId; }]
|
||||
];
|
||||
for (const [name, mutate] of cases) await t.test(name, () => {
|
||||
const value = mathEntityFixture(); mutate(value);
|
||||
const result = core.upgradeLegacyMathEntities(value.existing, value.incoming, value.context);
|
||||
assert.equal(result.upgraded.length, 0);
|
||||
assert.equal(result.existing.length, value.existing.length);
|
||||
});
|
||||
});
|
||||
|
||||
test('same stem but different option sets never auto-map', () => {
|
||||
const record = core.normalizeRecord(fixture());
|
||||
const current = reorder(record);
|
||||
|
||||
Reference in New Issue
Block a user