fix: retain question images and repair evidenced legacy records
This commit is contained in:
@@ -13,6 +13,8 @@ const ROOT = path.resolve(__dirname, '..');
|
||||
const RECORDS_KEY = 'huayi-qb.records.v1';
|
||||
const CONFIG_KEY = 'huayi-qb.config.v1';
|
||||
const DEDUP_BACKUP_KEY = 'huayi-qb.records.before-dedup.v1';
|
||||
const OPTION_REPAIR_BACKUP_KEY = 'huayi-qb.records.before-duplicate-option-repair.v1';
|
||||
const IMAGE_UPGRADE_BACKUP_KEY = 'huayi-qb.records.before-image-upgrade.v1';
|
||||
const QA_DIR = path.join(os.tmpdir(), 'huayi-qb-qa-v121');
|
||||
const BUNDLED = path.join(os.homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules');
|
||||
|
||||
@@ -22,6 +24,128 @@ function loadPlaywright() {
|
||||
catch { return require(path.join(BUNDLED, 'playwright')); }
|
||||
}
|
||||
|
||||
test('历史误报和图片补全:原始备份、设置导出、旧标签写回与失败保护', { timeout: 60000 }, async () => {
|
||||
// Use the current sources without building dist in a concurrently edited tree.
|
||||
// The adapter fixture supplies explicit snapshot evidence; real storage, UI,
|
||||
// imports, downloads and delayed writes still run in Chromium.
|
||||
const core = require('../src/core.js');
|
||||
const source = { kind: 'network', url: 'https://example.test/upm/start', observedAt: '2026-10-29T00:00:00Z', batchId: 'old-batch' };
|
||||
const makeLegacy = (questionId, stem) => {
|
||||
const record = core.normalizeRecord({ bankId: 'repair-bank', bankName: '修复验收题库', questionId, type: 'multiple', stem,
|
||||
options: [{ id: 'a', text: '降低' }, { id: 'b', text: '下降' }, { id: 'c', text: '增加' }, { id: 'd', text: '下降' }],
|
||||
correctOptionIds: ['a', 'b', 'c', 'd'], answerTexts: ['降低', '下降', '增加'], answerStatus: 'confirmed', source });
|
||||
return { ...record, answerStatus: 'conflict', conflicts: [
|
||||
{ reason: '正确选项ID与正确答案内容不一致', answerTexts: record.answerTexts, correctOptionIds: record.correctOptionIds },
|
||||
{ reason: '同一题目出现不同或不一致的标准答案', stem: record.stem, options: record.options,
|
||||
answerTexts: record.answerTexts, correctOptionIds: record.correctOptionIds, source }
|
||||
] };
|
||||
};
|
||||
const legacy = makeLegacy('old-false-conflict', '旧版重复文字答案误报');
|
||||
const realConflict = makeLegacy('real-conflict', '两个不同正确选项集合的真实冲突');
|
||||
realConflict.conflicts[1] = { ...realConflict.conflicts[1], correctOptionIds: ['a', 'b', 'c'] };
|
||||
const oldImage = core.normalizeRecord({ bankId: 'image-bank', bankName: '图片验收题库', questionId: 'old-image', type: 'single',
|
||||
stem: '根据溶解度曲线,应如何操作?', questionIndex: 0, contentOccurrence: 0,
|
||||
options: [{ id: 'a', text: '降低温度' }, { id: 'b', text: '提高温度' }], correctOptionIds: ['b'], answerStatus: 'confirmed', source });
|
||||
const oldOther = core.normalizeRecord({ ...oldImage, questionId: 'old-other', questionIndex: 1, stem: '另一道普通题目' });
|
||||
const newSource = { ...source, observedAt: '2026-10-29T01:00:00Z', batchId: 'new-batch' };
|
||||
const snapshot = [oldImage, oldOther].map((record, index) => core.normalizeRecord({ ...record,
|
||||
questionId: `new-image-${index}`, stem: record.stem + (index ? '' : '[图片:https://example.test/curve.png]'),
|
||||
source: newSource, answerSource: newSource }));
|
||||
const bank = { bankId: 'image-bank', bankName: '图片验收题库', questionCount: 2, loading: false };
|
||||
const stubs = `const QBAdapter={create(options){window.__qaReceive=options.onQuestions;return {start(){},stop(){},setCopyEnabled(){},getCurrentQuestion(){return null},getCurrentBank(){return ${JSON.stringify(bank)}}}}};
|
||||
const QBAuto={create(){return {getState(){return {enabled:false,busy:false,answeredCount:0,skippedCount:0,message:''}},setEnabled(){},answerCurrent(){return Promise.resolve()}}}};`;
|
||||
const appSource = `(function(){${fs.readFileSync(path.join(ROOT, 'src/core.js'), 'utf8')}\n${stubs}\n${fs.readFileSync(path.join(ROOT, 'src/app.js'), 'utf8')}\n})();`;
|
||||
const { chromium } = loadPlaywright();
|
||||
const browser = await chromium.launch(browserOptions());
|
||||
const createContext = async (seed, failBackup = false) => {
|
||||
const context = await browser.newContext({ acceptDownloads: true });
|
||||
await context.route('**/*', route => route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><html><head><meta charset="utf-8"></head><body><p>本地持久化验收</p></body></html>' }));
|
||||
await context.addInitScript({ content: `(${installGMShim.toString()})();
|
||||
GM_addValueChangeListener=(key,callback)=>{window.__qaRemoteChange=callback;return 1};
|
||||
if(localStorage.getItem(${JSON.stringify(RECORDS_KEY)})===null){GM_setValue(${JSON.stringify(RECORDS_KEY)},${JSON.stringify(seed)});GM_setValue(${JSON.stringify(CONFIG_KEY)},{collecting:false});}
|
||||
${failBackup ? `const originalSet=GM_setValue;GM_setValue=(key,value)=>{if(key===${JSON.stringify(OPTION_REPAIR_BACKUP_KEY)})throw new Error('fixture backup denied');return originalSet(key,value)};` : ''}
|
||||
${appSource}` });
|
||||
return context;
|
||||
};
|
||||
let context;
|
||||
try {
|
||||
context = await createContext([legacy, realConflict, oldImage, oldOther]);
|
||||
const page = await context.newPage();
|
||||
await page.goto('https://www.shhuayi-elearning.cn/repair-fixture');
|
||||
await page.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
assert.equal(await page.locator('#qb-panel').isVisible(), false);
|
||||
await waitFor(async () => (await storageRecords(page)).find(record => record.questionId === legacy.questionId)?.answerStatus === 'confirmed', '加载时应修复误报并写入');
|
||||
assert.equal((await storageRecords(page)).find(record => record.questionId === realConflict.questionId).answerStatus, 'conflict');
|
||||
const backup = await page.evaluate(key => GM_getValue(key, null), OPTION_REPAIR_BACKUP_KEY);
|
||||
assert.deepEqual(backup.records, [legacy], '真实冲突不应被修复或混入误报备份');
|
||||
await activate(page, 'settings');
|
||||
const backupDownloadWaiting = page.waitForEvent('download');
|
||||
await page.locator('[data-action="export-option-repair-backup"]').click();
|
||||
const backupDownload = await backupDownloadWaiting;
|
||||
const backupPath = await backupDownload.path();
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(backupPath, 'utf8')).records, [legacy]);
|
||||
|
||||
const imported = makeLegacy('later-import', '后来导入的另一道误报');
|
||||
await importFile(page, 'legacy-import.json', JSON.stringify({ schemaVersion: 1, records: [imported] }));
|
||||
assert.equal((await storageRecords(page)).find(record => record.questionId === imported.questionId).answerStatus, 'confirmed');
|
||||
assert.deepEqual((await page.evaluate(key => GM_getValue(key, null), OPTION_REPAIR_BACKUP_KEY)).records, [legacy, imported], '后续导入修复必须追加原始版本');
|
||||
|
||||
await page.evaluate(({ snapshot, bankId }) => window.__qaReceive(snapshot, { fullSnapshot: true, pageQuestionCount: 2, currentBankId: bankId }), { snapshot, bankId: bank.bankId });
|
||||
await waitFor(async () => !(await storageRecords(page)).some(record => record.questionId === oldImage.questionId), '完整图片快照应移除已备份旧纯文字记录');
|
||||
const afterImage = await storageRecords(page);
|
||||
assert.equal(afterImage.filter(record => record.bankId === bank.bankId).length, 2);
|
||||
assert.ok(afterImage.find(record => record.questionId === snapshot[0].questionId).questionIdAliases.includes(oldImage.questionId));
|
||||
const imageBackup = await page.evaluate(key => GM_getValue(key, null), IMAGE_UPGRADE_BACKUP_KEY);
|
||||
assert.deepEqual(imageBackup.records, [oldImage]);
|
||||
assert.equal(imageBackup.migrations.length, 1);
|
||||
await activate(page, 'settings');
|
||||
const imageDownloadWaiting = page.waitForEvent('download');
|
||||
await page.locator('[data-action="export-image-upgrade-backup"]').click();
|
||||
const imageDownload = await imageDownloadWaiting;
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(await imageDownload.path(), 'utf8')).records, [oldImage]);
|
||||
|
||||
await page.evaluate(({ stale, key }) => { GM_setValue(key, stale); window.__qaRemoteChange(key, [], stale, true); }, { stale: [...afterImage, oldImage], key: RECORDS_KEY });
|
||||
await waitFor(async () => (await storageRecords(page)).filter(record => record.bankId === bank.bankId).length === 2, '旧标签写回不能复活已补全的原始版本');
|
||||
await page.reload();
|
||||
await page.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
assert.equal((await storageRecords(page)).filter(record => record.bankId === bank.bankId).length, 2);
|
||||
assert.ok((await storageRecords(page)).find(record => record.questionId === snapshot[0].questionId).stem.includes('[图片:'));
|
||||
|
||||
const changedOld = { ...oldImage, correctOptionIds: ['a'], answerTexts: ['降低温度'] };
|
||||
const current = await storageRecords(page);
|
||||
await page.evaluate(({ stale, key }) => { GM_setValue(key, stale); window.__qaRemoteChange(key, [], stale, true); }, { stale: [...current, changedOld], key: RECORDS_KEY });
|
||||
await page.waitForTimeout(350);
|
||||
assert.ok((await storageRecords(page)).some(record => record.questionId === oldImage.questionId && record.answerTexts.includes('降低温度')),
|
||||
'相同旧key但答案已变化的记录不能按退休证据删除');
|
||||
await context.close(); context = null;
|
||||
|
||||
const rawImage = { ...oldImage }, rawOther = { ...oldOther };
|
||||
delete rawImage.contentOccurrence; delete rawOther.contentOccurrence;
|
||||
context = await createContext([rawImage, rawOther]);
|
||||
const rawPage = await context.newPage();
|
||||
await rawPage.goto('https://www.shhuayi-elearning.cn/repair-fixture');
|
||||
await rawPage.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
await rawPage.evaluate(({ snapshot, bankId }) => window.__qaReceive(snapshot, { fullSnapshot: true, pageQuestionCount: 2, currentBankId: bankId }), { snapshot, bankId: bank.bankId });
|
||||
await waitFor(async () => (await storageRecords(rawPage)).length === 2 && !(await storageRecords(rawPage)).some(record => record.questionId === rawImage.questionId),
|
||||
'内存已补occ但GM原记录未补时,也不能复活旧纯文字题');
|
||||
const rawBackup = await rawPage.evaluate(key => GM_getValue(key, null), IMAGE_UPGRADE_BACKUP_KEY);
|
||||
assert.ok(rawBackup.records.some(record => JSON.stringify(record) === JSON.stringify(rawImage)), '必须原样保留真实GM中的旧对象');
|
||||
assert.equal(rawBackup.migrations.length, 2, '内存和GM中的两个原始元数据版本均有明确退休证据');
|
||||
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');
|
||||
await blocked.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
await blocked.waitForTimeout(350);
|
||||
assert.deepEqual(await storageRecords(blocked), [legacy], '独立备份写入失败时必须保持原存储');
|
||||
assert.equal(await blocked.evaluate(key => GM_getValue(key, null), OPTION_REPAIR_BACKUP_KEY), null);
|
||||
} finally {
|
||||
if (context) await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
});
|
||||
|
||||
function browserOptions() {
|
||||
const executablePath = process.env.QB_CHROMIUM_EXECUTABLE || process.env.QB_CHROMIUM_PATH;
|
||||
if (executablePath) return { headless: true, executablePath };
|
||||
|
||||
Reference in New Issue
Block a user