feat: publish verified five-bank question dataset
This commit is contained in:
+55
-4
@@ -42,9 +42,12 @@ test('multiple answer plan accepts every explicitly correct occurrence of repeat
|
||||
assert.deepEqual(planAnswer([subset], question, core).labels, ['A', 'D']);
|
||||
});
|
||||
|
||||
test('repeated text still skips partial answers, text-only answers, ID contradictions and duplicate labels', () => {
|
||||
test('repeated text skips partial answers without exact identities, text-only answers, ID contradictions and duplicate labels', () => {
|
||||
const question = repeatedMultiple({ answerStatus: 'missing', correctOptionIds: [], answerTexts: [] });
|
||||
assert.equal(planAnswer([repeatedMultiple({ correctOptionIds: ['b'], answerTexts: ['下降'] })], question, core).status, 'ambiguous');
|
||||
const partial = repeatedMultiple({ correctOptionIds: ['b'], answerTexts: ['下降'] });
|
||||
assert.equal(planAnswer([partial], question, core).status, 'ready');
|
||||
const fresh = { ...question, options: question.options.map(option => ({ ...option, id: `new-${option.id}` })) };
|
||||
assert.equal(planAnswer([partial], fresh, core).status, 'ambiguous');
|
||||
assert.equal(planAnswer([repeatedMultiple({ correctOptionIds: [], answerTexts: ['下降'] })], question, core).status, 'ambiguous');
|
||||
const changed = repeatedMultiple();
|
||||
[changed.options[0].id, changed.options[1].id] = [changed.options[1].id, changed.options[0].id];
|
||||
@@ -52,6 +55,36 @@ test('repeated text still skips partial answers, text-only answers, ID contradic
|
||||
question.options[3].label = 'B';
|
||||
assert.equal(planAnswer([repeatedMultiple()], question, core).status, 'ambiguous');
|
||||
});
|
||||
|
||||
function repeatedSingle(extra = {}) {
|
||||
return single({ options: [{ id: 'a', text: '其他产物甲', label: 'A' }, { id: 'b', text: '其他产物乙', label: 'B' },
|
||||
{ id: 'c', text: '生成Cu和Fe2+', label: 'C' }, { id: 'd', text: '生成Cu和Fe2+', label: 'D' }],
|
||||
correctOptionIds: ['c'], answerTexts: ['生成Cu和Fe2+'], ...extra });
|
||||
}
|
||||
|
||||
test('repeated single-choice text maps by complete exact option IDs after labels reorder', () => {
|
||||
const record = repeatedSingle();
|
||||
const question = repeatedSingle({ answerStatus: 'missing', correctOptionIds: [], answerTexts: [] });
|
||||
assert.deepEqual(planAnswer([record], question, core).labels, ['C']);
|
||||
question.options = [question.options[3], question.options[2], question.options[0], question.options[1]]
|
||||
.map((option, index) => ({ ...option, label: 'ABCD'[index] }));
|
||||
const plan = planAnswer([record], question, core);
|
||||
assert.equal(plan.status, 'ready');
|
||||
assert.deepEqual(plan.labels, ['B']);
|
||||
assert.deepEqual(plan.answerTexts, ['生成Cu和Fe2+']);
|
||||
});
|
||||
|
||||
test('repeated single-choice text remains ambiguous when full ID identity cannot be proved', () => {
|
||||
const record = repeatedSingle();
|
||||
const question = repeatedSingle({ answerStatus: 'missing', correctOptionIds: [], answerTexts: [] });
|
||||
const fresh = { ...question, options: question.options.map(option => ({ ...option, id: `new-${option.id}` })) };
|
||||
assert.equal(planAnswer([record], fresh, core).status, 'ambiguous');
|
||||
const missingID = { ...question, options: question.options.map(option => ({ ...option, id: option.id === 'a' ? '' : option.id })) };
|
||||
assert.equal(planAnswer([record], missingID, core).status, 'ambiguous');
|
||||
const changed = { ...question, options: question.options.map(option => ({ ...option, id: option.id === 'a' ? 'c' : option.id === 'c' ? 'a' : option.id })) };
|
||||
assert.equal(planAnswer([record], changed, core).status, 'ambiguous');
|
||||
assert.equal(planAnswer([repeatedSingle({ correctOptionIds: [] })], question, core).status, 'ambiguous');
|
||||
});
|
||||
test('judgement uses explicit correct and incorrect content, including zero ID', () => {
|
||||
const record = { bankId: 'j', type: 'judge', stem: '这项说法正确吗?', options: [{ id: '1', text: '正确' }, { id: '0', text: '错误' }], correctOptionIds: ['0'], answerStatus: 'confirmed' };
|
||||
const question = { ...record, options: [{ id: '0', text: '错误', label: '错误' }, { id: '1', text: '正确', label: '正确' }] };
|
||||
@@ -79,7 +112,8 @@ test('without bank identity, contradictory cross-bank matches are ambiguous', ()
|
||||
});
|
||||
test('different complete option sets, duplicate option text and wrong option IDs are refused', () => {
|
||||
assert.equal(planAnswer([single()], current({ options: [{ id: 'a', text: '降低温度' }, { id: 'b', text: '提高温度' }] }), core).status, 'missing');
|
||||
assert.equal(planAnswer([single()], current({ options: [{ text: '提高温度' }, { text: '提高温度' }] }), core).status, 'ambiguous');
|
||||
const duplicate = single({ options: [{ id: 'a', text: '提高温度' }, { id: 'b', text: '提高温度' }] });
|
||||
assert.equal(planAnswer([duplicate], current({ options: [{ text: '提高温度' }, { text: '提高温度' }] }), core).status, 'ambiguous');
|
||||
assert.equal(planAnswer([single()], current({ options: [{ id: 'b', text: '保持不变', label: 'A' }, { id: 'a', text: '降低温度', label: 'B' }, { id: 'c', text: '提高温度', label: 'C' }] }), core).status, 'ambiguous');
|
||||
});
|
||||
test('normalization preserves negatives, signs, units and superscripts', () => {
|
||||
@@ -141,13 +175,30 @@ test('automatic selection chooses all correct repeated checkboxes and clears an
|
||||
|
||||
test('automatic selection never clicks an ambiguous repeated-text subset', async () => {
|
||||
const page = checkboxPage({ texts: ['下降', '增加', '下降'] });
|
||||
page.record.correctOptionIds = ['0']; page.record.answerTexts = ['下降'];
|
||||
page.record.options.forEach(option => { option.id = `saved-${option.id}`; });
|
||||
page.record.correctOptionIds = ['saved-0']; page.record.answerTexts = ['下降'];
|
||||
const engine = create({ window: page.win, document: page.doc, core, getRecords: () => [page.record] });
|
||||
const result = await engine.answerCurrent();
|
||||
assert.equal(result.status, 'skipped'); assert.deepEqual(page.clicks, []);
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('single-choice clicks the correct repeated option only with complete exact IDs', async () => {
|
||||
for (const freshIDs of [false, true]) {
|
||||
const page = checkboxPage({ texts: ['其他产物甲', '其他产物乙', '生成Cu和Fe2+', '生成Cu和Fe2+'] });
|
||||
page.inputs.forEach(input => { input.type = 'radio'; });
|
||||
page.record.type = 'single';
|
||||
if (freshIDs) page.record.options.forEach(option => { option.id = `saved-${option.id}`; });
|
||||
page.record.correctOptionIds = [freshIDs ? 'saved-2' : '2']; page.record.answerTexts = ['生成Cu和Fe2+'];
|
||||
const engine = create({ window: page.win, document: page.doc, core, getRecords: () => [page.record] });
|
||||
const result = await engine.answerCurrent();
|
||||
assert.equal(result.status, freshIDs ? 'skipped' : 'answered');
|
||||
assert.deepEqual(page.clicks, freshIDs ? [] : [2]);
|
||||
assert.deepEqual(page.inputs.map(input => input.checked), freshIDs ? [false, false, false, false] : [false, false, true, false]);
|
||||
engine.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('automatic selection reads image stems and choices consistently with network records', async () => {
|
||||
const { extractQuestions } = require('../src/adapter.js');
|
||||
for (const richLabels of [false, true]) {
|
||||
|
||||
+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);
|
||||
|
||||
@@ -133,6 +133,33 @@ test('历史误报和图片补全:原始备份、设置导出、旧标签写
|
||||
assert.equal(rawBackup.migrations.length, 2, '内存和GM中的两个原始元数据版本均有明确退休证据');
|
||||
await context.close(); context = null;
|
||||
|
||||
const oldMath = { ...oldImage, questionId: 'old-math', stem: '传热速率=系数×推动力,误差±2。' };
|
||||
const newMath = core.normalizeRecord({ ...snapshot[0], questionId: 'new-math', stem: '传热速率=系数×推动力,误差±2。' });
|
||||
const existingModern = core.normalizeRecord({ ...newMath, questionId: 'existing-modern-math', questionIdAliases: ['older-modern-math'],
|
||||
source: { ...newSource, batchId: 'earlier-modern-batch' } });
|
||||
context = await createContext([oldMath, existingModern, oldOther]);
|
||||
const mathPage = await context.newPage();
|
||||
await mathPage.goto('https://www.shhuayi-elearning.cn/repair-fixture');
|
||||
await mathPage.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
await mathPage.evaluate(({ snapshot, bankId }) => window.__qaReceive(snapshot, { fullSnapshot: true, pageQuestionCount: 2, currentBankId: bankId }),
|
||||
{ snapshot: [newMath, snapshot[1]], bankId: bank.bankId });
|
||||
await waitFor(async () => (await storageRecords(mathPage)).length === 2, '实体补全应修复已经同时保存新旧两种文字的题库');
|
||||
const mathRecords = await storageRecords(mathPage);
|
||||
const canonicalMath = mathRecords.find(record => record.stem === newMath.stem);
|
||||
assert.equal(canonicalMath.questionId, existingModern.questionId, '合并可保留已有别名记录的canonical ID');
|
||||
assert.ok(canonicalMath.questionIdAliases.includes(newMath.questionId));
|
||||
assert.ok(canonicalMath.questionIdAliases.includes(oldMath.questionId));
|
||||
const mathBackup = await mathPage.evaluate(key => GM_getValue(key, null), IMAGE_UPGRADE_BACKUP_KEY);
|
||||
assert.ok(mathBackup.records.some(record => record.questionId === oldMath.questionId && record.stem === oldMath.stem));
|
||||
assert.ok(mathBackup.migrations.every(item => item.kind === 'math-entity'));
|
||||
await mathPage.evaluate(({ stale, key }) => { GM_setValue(key, stale); window.__qaRemoteChange(key, [], stale, true); },
|
||||
{ stale: [...mathRecords, oldMath], key: RECORDS_KEY });
|
||||
await waitFor(async () => (await storageRecords(mathPage)).length === 2, '别名合并改变replacement key后,旧实体版本仍不得复活');
|
||||
await mathPage.reload();
|
||||
await mathPage.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
assert.equal((await storageRecords(mathPage)).length, 2);
|
||||
await context.close(); context = null;
|
||||
|
||||
context = await createContext([legacy], true);
|
||||
const blocked = await context.newPage();
|
||||
await blocked.goto('https://www.shhuayi-elearning.cn/repair-fixture');
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const core = require('../src/core.js');
|
||||
const { EXPECTED_BANKS } = require('../scripts/merge-question-banks.cjs');
|
||||
const { selectSnapshot, prepare, writeOutput } = require('../scripts/select-bank-snapshots.cjs');
|
||||
|
||||
const older = '2026-10-29T10:00:00.000Z', latest = '2026-10-29T11:00:00.000Z';
|
||||
function record(bankName, index, time = latest, overrides = {}) {
|
||||
const type = bankName.includes('判断') ? 'judge' : bankName.includes('多选') ? 'multiple' : 'single';
|
||||
return core.normalizeRecord({ bankId: `bank-${EXPECTED_BANKS.indexOf(bankName)}`, bankName,
|
||||
questionId: `${time}-${index}`, questionIndex: index, questionIdAliases: [`legacy-${index}`], contentOccurrence: index,
|
||||
type, stem: '同文题也必须按题目位置保留。',
|
||||
options: [{ id: 'a', text: '选项甲', label: 'A' }, { id: 'b', text: '选项乙', label: 'B' }],
|
||||
correctOptionIds: ['b'], answerStatus: 'confirmed', source: { kind: 'network', batchId: `${time}:1`, observedAt: time }, ...overrides });
|
||||
}
|
||||
const input = records => ({ records, source: { filename: 'raw.csv', sha256: 'raw-hash', inputRecords: records.length } });
|
||||
|
||||
test('latest explicit answer batch wins without collapsing repeated positions or aliases', () => {
|
||||
const rows = [record(EXPECTED_BANKS[0], 0, older), record(EXPECTED_BANKS[0], 0), record(EXPECTED_BANKS[0], 1)];
|
||||
rows[1].source = { kind: 'dom', observedAt: '2026-10-29T12:00:00.000Z' };
|
||||
const selected = selectSnapshot(input(rows), 2);
|
||||
assert.equal(selected.audit.selectedBatchId, `${latest}:1`);
|
||||
assert.equal(selected.audit.excludedRecords, 1);
|
||||
assert.deepEqual(selected.audit.excludedKeys, [rows[0].key]);
|
||||
assert.deepEqual(selected.records.map(row => row.questionIndex), [0, 1]);
|
||||
assert.deepEqual(selected.records[0].questionIdAliases, rows[1].questionIdAliases);
|
||||
assert.equal(selected.records[0].stem, selected.records[1].stem);
|
||||
});
|
||||
|
||||
test('an incomplete newest batch fails rather than selecting an older complete batch', () => {
|
||||
const rows = [record(EXPECTED_BANKS[0], 0, older), record(EXPECTED_BANKS[0], 1, older), record(EXPECTED_BANKS[0], 0)];
|
||||
assert.throws(() => selectSnapshot(input(rows), 2), /最新批次 1 条.*不会回退/s);
|
||||
});
|
||||
|
||||
test('newest batch rejects duplicate indexes, IDs, wrong types and unconfirmed answers', () => {
|
||||
const changes = [
|
||||
{ questionIndex: 0 }, { questionId: `${latest}-0` }, { type: 'single' }, { answerStatus: 'conflict' },
|
||||
{ questionId: '' }, { correctOptionIds: ['not-an-option'] }
|
||||
];
|
||||
for (const change of changes) {
|
||||
const rows = [record(EXPECTED_BANKS[0], 0), record(EXPECTED_BANKS[0], 1, latest, change)];
|
||||
assert.throws(() => selectSnapshot(input(rows), 2), /校验失败/);
|
||||
}
|
||||
const noBatch = record(EXPECTED_BANKS[0], 0);
|
||||
noBatch.answerSource = noBatch.source = { kind: 'network', observedAt: latest };
|
||||
assert.throws(() => selectSnapshot(input([noBatch]), 1), /没有带明确批次/);
|
||||
const tie = record(EXPECTED_BANKS[0], 1);
|
||||
tie.answerSource = tie.source = { kind: 'network', observedAt: latest, batchId: `${latest}:2` };
|
||||
assert.throws(() => selectSnapshot(input([record(EXPECTED_BANKS[0], 0), tie]), 1), /无法唯一确定最新组/);
|
||||
});
|
||||
|
||||
function fixtureDirectory(t) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'qb-snapshot-test-'));
|
||||
t.after(() => {
|
||||
const resolved = path.resolve(root), temp = path.resolve(os.tmpdir());
|
||||
assert.equal(path.dirname(resolved), temp);
|
||||
assert.ok(path.basename(resolved).startsWith('qb-snapshot-test-'));
|
||||
fs.rmSync(resolved, { recursive: true, force: true });
|
||||
});
|
||||
const manifestPath = path.join(root, 'counts.json');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify({ banks: EXPECTED_BANKS.map(bankName => ({ bankName, questionCount: 2 })) }));
|
||||
const files = EXPECTED_BANKS.map((bankName, index) => {
|
||||
const filename = path.join(root, `raw-${index}.csv`);
|
||||
fs.writeFileSync(filename, core.toCSV([record(bankName, 0, older), record(bankName, 0), record(bankName, 1)]));
|
||||
return filename;
|
||||
});
|
||||
return { files, manifestPath, outDir: path.join(root, 'output'), force: false };
|
||||
}
|
||||
|
||||
test('all five are validated before any output and raw CSV bytes stay unchanged', t => {
|
||||
const options = fixtureDirectory(t);
|
||||
const before = options.files.map(filename => fs.readFileSync(filename));
|
||||
const prepared = prepare(options);
|
||||
assert.equal(prepared.audit.selectedRecords, 10);
|
||||
assert.equal(prepared.audit.excludedRecords, 5);
|
||||
assert.equal(prepared.files.length, 6);
|
||||
const written = writeOutput(options, prepared);
|
||||
assert.equal(written.length, 6);
|
||||
assert.deepEqual(options.files.map(filename => fs.readFileSync(filename)), before);
|
||||
for (const filename of written.filter(name => name.endsWith('.csv'))) assert.equal(core.fromCSV(fs.readFileSync(filename, 'utf8')).length, 2);
|
||||
const audit = JSON.parse(fs.readFileSync(path.join(options.outDir, '导出批次核对.json'), 'utf8'));
|
||||
assert.ok(audit.banks.every(bank => /^[a-f0-9]{64}$/.test(bank.rawSha256)));
|
||||
});
|
||||
|
||||
test('failure in the fifth newest batch leaves no output directory', t => {
|
||||
const options = fixtureDirectory(t), lastName = EXPECTED_BANKS.at(-1);
|
||||
fs.writeFileSync(options.files.at(-1), core.toCSV([record(lastName, 0, older), record(lastName, 1, older), record(lastName, 0)]));
|
||||
assert.throws(() => prepare(options), /不会回退较旧批次/);
|
||||
assert.equal(fs.existsSync(options.outDir), false);
|
||||
});
|
||||
|
||||
test('destination conflicts are checked before writing the first bank', t => {
|
||||
const options = fixtureDirectory(t), prepared = prepare(options);
|
||||
fs.mkdirSync(options.outDir);
|
||||
fs.writeFileSync(path.join(options.outDir, '导出批次核对.json'), 'existing audit');
|
||||
assert.throws(() => writeOutput(options, prepared), /输出已存在/);
|
||||
assert.equal(fs.existsSync(path.join(options.outDir, '分题库')), false);
|
||||
assert.equal(writeOutput({ ...options, force: true }, prepared).length, 6);
|
||||
});
|
||||
Reference in New Issue
Block a user