Files
airz-uniapp/scripts/test-archive-display.cjs
T
hujun e5406c23a6 fix: 修复匹配详情提示及公积金可选路径问题
修复匹配详情中“用户自填未经核验”等来源说明,清理未参与项的橙色注意提示,修正贷款统计口径文案;补充公积金完整可选路径提取逻辑,确保明确无额外时长要求的公积金分支可正确识别并通过。
2026-10-19 16:11:59 +08:00

272 lines
21 KiB
JavaScript

// Exercise real archive adapters/page scripts with synthetic memory responses; no network, DB or build.
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const assert = require('node:assert/strict');
const ts = require('D:/Develop/HBuilderX/plugins/uniapp-uts-v1/node_modules/@dcloudio/uni-uts-v1/lib/typescript/lib/typescript.js');
const root = path.resolve(__dirname, '..');
const amountKeys = ['totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit'];
class Json {
constructor(values = {}) { for (const [key, value] of Object.entries(values)) this[key] = wrap(value); }
getString(key) { return typeof this[key] === 'string' ? this[key] : null; }
getNumber(key) { return typeof this[key] === 'number' ? this[key] : null; }
getBoolean(key) { return typeof this[key] === 'boolean' ? this[key] : null; }
getArray(key) { return Array.isArray(this[key]) ? this[key] : null; }
}
function wrap(value) { return Array.isArray(value) ? value.map(wrap) : value && typeof value === 'object' && !(value instanceof Json) ? new Json(value) : value; }
const plain = value => JSON.parse(JSON.stringify(value));
function harness() {
const modules = {}; const queue = []; const calls = []; const hooks = {}; const navigation = [];
let session = new Json({ token: 'synthetic-archive-owner' });
const api = {
getSession: () => session,
async requestJson(route, method, params) {
calls.push({ route, method, params: plain(params) });
const response = queue.shift(); assert.ok(response, 'response queued for ' + route);
assert.equal(route, response.route); assert.equal(method, 'GET');
if (response.params !== undefined) assert.deepEqual(plain(params), response.params);
if (response.error) throw response.error;
return wrap(response.promise ? await response.promise : response.data);
}
};
function compile(source, filename, extras = {}) {
const exports = {};
const prepared = source.replace(/const (labels|fieldNames): UTSJSONObject = (.*)/g, 'const $1: UTSJSONObject = new UTSJSONObject($2)');
const compiled = ts.transpileModule(prepared, { reportDiagnostics: true, compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } });
assert.equal((compiled.diagnostics || []).filter(item => item.category === ts.DiagnosticCategory.Error).length, 0, filename + ' transpiles');
vm.runInNewContext(compiled.outputText, { exports, UTSJSONObject: Json, Error, Number, Date, Math, Promise, Array,
require(name) {
if (name.endsWith('.uvue')) return {};
if (name.endsWith('/api.uts') || name === './api.uts') return api;
if (name.endsWith('/layout.uts')) return { usePageLayout: () => ({ pageStyle: () => '', contentStyle: () => '' }) };
return load(name.startsWith('@/') ? name.slice(2) : path.posix.normalize(path.posix.dirname(filename) + '/' + name));
}, setInterval: () => 1, clearInterval: () => {}, ...extras
}, { filename });
return exports;
}
function load(filename) {
if (!modules[filename]) modules[filename] = compile(fs.readFileSync(path.join(root, filename), 'utf8'), filename);
return modules[filename];
}
function page() {
const filename = 'packages/finance/archive/archive.uvue';
const source = fs.readFileSync(path.join(root, filename), 'utf8').match(/<script setup lang="uts">([\s\S]*?)<\/script>/)[1];
const exposure = '{detail,sections,completeness,completenessFailed,error,readCompleteness,retryCompleteness,refresh,clearView,archiveCompleteness:typeof archiveCompleteness==="undefined"?null:archiveCompleteness}';
return compile(source + '\nexports.page=' + exposure, filename, {
ref: value => ({ value }), computed: callback => ({ get value() { return callback(); } }),
onLoad: callback => { hooks.load = callback; }, onShow: callback => { hooks.show = callback; },
onHide: callback => { hooks.hide = callback; }, onUnload: callback => { hooks.unload = callback; },
uni: { redirectTo: options => navigation.push(options.url), navigateTo: options => navigation.push(options.url) }
}).page;
}
function enqueue(route, data, params) { queue.push({ route, data, params }); }
function defer(route, params) {
let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
queue.push({ route, promise, params }); return { resolve, reject };
}
return { load, page, hooks, calls, queue, navigation, enqueue, defer, fail: (route, error = new Error('synthetic read failed')) => queue.push({ route, error }), setSession: token => { session = token == null ? null : new Json({ token }); } };
}
function detailResponse(overrides = {}) {
const form = { name: '合成资料', phone: '199****0009', city: '广东省', provinceCode: '440000', cityCode: null,
occupation: '合成职业', monthlyIncome: '20000.00', loanCountScope: 'all_loans', creditLoanCount: 0, creditCardCount: 0,
house: 'no', car: 'no', totalDebtBalance: null, monthlyLoanPayment: null, creditCardTotalLimit: null, creditCardUsedLimit: null, ...overrides.form };
return { assessmentId: '1', revisionId: '2', revisionNo: 1, type: 'personal', status: 'succeeded', statusLabel: '分析完成',
message: '', taskId: 'synthetic-task', stage: 'completed', canRetry: false, hasReport: true, publishedRevisionId: '2',
submittedAt: '2026-01-01T00:00:00Z', inputVersion: 3, moneyUnit: 'yuan', ...overrides, form };
}
function reportResponse(overrides = {}) {
return { assessmentId: '1', revisionId: '2', resultVersion: 2,
completeness: { status: 'evaluated', percent: 100, items: amountKeys.map(fieldKey => ({ fieldKey, state: 'not_applicable' })), ...overrides.completeness },
...Object.fromEntries(Object.entries(overrides).filter(([key]) => key !== 'completeness')) };
}
async function fetchDetail(h, response = detailResponse()) {
h.enqueue('/assessments/' + response.assessmentId, response, { revisionId: response.revisionId });
return h.load('packages/finance/services/records.uts').fetchAssessmentDetail(response.assessmentId, response.revisionId);
}
async function fetchCompleteness(h, response = reportResponse()) {
h.enqueue('/assessments/1/report', response, { revisionId: '2' });
return h.load('packages/finance/services/records.uts').fetchArchiveCompleteness('1', '2');
}
const settle = async () => { for (let i = 0; i < 20; i++) await Promise.resolve(); };
function queuePageReads(h, detail = detailResponse()) {
const id = detail.assessmentId; const revision = detail.revisionId;
h.enqueue('/assessments/' + id, detail, { revisionId: revision });
h.enqueue('/assessments', { items: [detail], hasMore: false, nextCursor: '' }, { limit: '20', cursor: '', type: detail.type });
h.enqueue('/assessments/' + id + '/revisions', { items: [detail], hasMore: false, nextCursor: '' }, { cursor: '' });
}
async function openPage(h, report = reportResponse(), options = {}) {
const detail = options.detail || detailResponse(); const view = h.page();
h.hooks.load({ assessmentId: detail.assessmentId, revisionId: detail.revisionId });
queuePageReads(h, detail);
let pending = null;
if (detail.status === 'succeeded') {
if (options.deferred) pending = h.defer('/assessments/' + detail.assessmentId + '/report', { revisionId: detail.revisionId });
else if (options.error) h.fail('/assessments/' + detail.assessmentId + '/report');
else h.enqueue('/assessments/' + detail.assessmentId + '/report', report, { revisionId: detail.revisionId });
}
h.hooks.show(); await settle();
return { view, pending };
}
function amountValues(sections) {
return plain(sections.find(item => item.id === 'assets').fields.filter(item => amountKeys.includes(item.fieldKey)).map(item => item.value));
}
let passed = 0, failed = 0;
async function test(name, callback) {
try { await callback(); passed++; console.log('PASS ' + name); }
catch (error) { failed++; console.error('FAIL ' + name + '\n' + error.stack); }
}
(async () => {
await test('same-revision saved not_applicable evidence changes only missing amount display', async () => {
const h = harness(); const records = h.load('packages/finance/services/records.uts');
const detail = await fetchDetail(h); const before = JSON.stringify(detail);
const completeness = await fetchCompleteness(h);
assert.ok(Array.isArray(completeness.notApplicableFields), 'saved report must expose applicable archive display evidence');
assert.deepEqual(plain(completeness.notApplicableFields), amountKeys);
const sections = records.archiveSections(detail, completeness);
const fields = sections.find(item => item.id === 'assets').fields;
assert.deepEqual(plain(fields.filter(item => amountKeys.includes(item.fieldKey)).map(item => item.value)), ['不适用', '不适用', '不适用', '不适用']);
assert.equal(sections.find(item => item.id === 'assets').provided, 4);
assert.equal(JSON.stringify(detail), before); assert.equal(h.calls.length, 2);
assert.ok(detail.fields.filter(item => amountKeys.includes(item.fieldKey)).every(item => item.isMissing && item.value === '未填写'));
});
await test('explicit zero and positive saved amounts remain visible despite conflicting display markers', async () => {
const h = harness(); const records = h.load('packages/finance/services/records.uts');
const detail = await fetchDetail(h, detailResponse({ form: { totalDebtBalance: '0.00', monthlyLoanPayment: '12345.67', creditCardTotalLimit: '50000.00', creditCardUsedLimit: null } }));
const before = JSON.stringify(detail); const completeness = await fetchCompleteness(h);
assert.deepEqual(amountValues(records.archiveSections(detail, completeness)), ['0 万元', '1.234567 万元', '5 万元', '不适用']);
assert.equal(JSON.stringify(detail), before);
assert.equal(detail.sections.find(item => item.id === 'assets').provided, 7);
assert.ok(detail.fields.filter(item => ['totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit'].includes(item.fieldKey)).every(item => item.isMissing === false));
});
await test('missing origin flag rather than Chinese placeholder wording controls display adaptation', async () => {
const h = harness(); const records = h.load('packages/finance/services/records.uts'); const detail = await fetchDetail(h);
const fields = detail.sections.find(item => item.id === 'assets').fields;
fields.find(item => item.fieldKey === 'totalDebtBalance').value = '尚未提供';
const completeness = await fetchCompleteness(h);
assert.equal(records.archiveSections(detail, completeness).find(item => item.id === 'assets').fields.find(item => item.fieldKey === 'totalDebtBalance').value, '不适用');
assert.equal(fields.find(item => item.fieldKey === 'totalDebtBalance').value, '尚未提供');
});
await test('partial saved markers affect only matching whitelist field keys, not counts or unrelated blanks', async () => {
const h = harness(); const records = h.load('packages/finance/services/records.uts'); const detail = await fetchDetail(h);
const completeness = await fetchCompleteness(h, reportResponse({ completeness: { items: [
{ fieldKey: 'totalDebtBalance', state: 'not_applicable' }, { fieldKey: 'creditLoanCount', state: 'not_applicable' },
{ fieldKey: 'company', state: 'not_applicable' }, { fieldKey: 'monthlyLoanPayment', state: 'missing' }
] } }));
assert.deepEqual(plain(completeness.notApplicableFields), ['totalDebtBalance']);
assert.deepEqual(amountValues(records.archiveSections(detail, completeness)), ['不适用', '未填写', '未填写', '未填写']);
assert.equal(records.archiveSections(detail, completeness).find(item => item.id === 'assets').fields.find(item => item.fieldKey === 'creditLoanCount').value, '0');
});
await test('old loan scope and count zero do not infer a saved marker or recompute report completeness', async () => {
const h = harness(); const records = h.load('packages/finance/services/records.uts');
const detail = await fetchDetail(h, detailResponse({ form: { loanCountScope: undefined } }));
const completeness = await fetchCompleteness(h, reportResponse({ completeness: { percent: 60, items: [] } }));
assert.equal(completeness.percent, 60); assert.deepEqual(plain(completeness.notApplicableFields), []);
assert.deepEqual(amountValues(records.archiveSections(detail, completeness)), ['未填写', '未填写', '未填写', '未填写']);
});
await test('absent or nonarray saved items never supply display evidence', async () => {
for (const items of [undefined, null, {}, 'not_applicable']) {
const h = harness(); const result = await fetchCompleteness(h, reportResponse({ completeness: { items } }));
assert.deepEqual(plain(result.notApplicableFields), []); assert.equal(result.percent, 100);
}
});
await test('v1 and unevaluated reports supply no new not-applicable display evidence', async () => {
for (const response of [reportResponse({ resultVersion: 1 }), reportResponse({ completeness: { status: 'not_evaluated' } })]) {
const h = harness(); const result = await fetchCompleteness(h, response);
assert.equal(result.percent, null); assert.deepEqual(plain(result.notApplicableFields), []);
assert.equal(result.assessmentId, '1'); assert.equal(result.revisionId, '2');
}
});
await test('duplicate keys including conflicting or identical markers are excluded independently', async () => {
const h = harness(); const result = await fetchCompleteness(h, reportResponse({ completeness: { items: [
{ fieldKey: 'totalDebtBalance', state: 'not_applicable' }, { fieldKey: 'totalDebtBalance', state: 'provided' },
{ fieldKey: 'monthlyLoanPayment', state: 'not_applicable' }, { fieldKey: 'monthlyLoanPayment', state: 'not_applicable' },
{ fieldKey: 'creditCardTotalLimit', state: 'not_applicable' }, { fieldKey: 'creditCardUsedLimit', state: 'invalid' },
null, 0, 'unknown', { label: '欠款总余额', state: 'not_applicable' }
] } }));
assert.deepEqual(plain(result.notApplicableFields), ['creditCardTotalLimit']);
});
await test('different assessment or revision evidence cannot change the current archive', async () => {
const h = harness(); const records = h.load('packages/finance/services/records.uts'); const detail = await fetchDetail(h);
const result = await fetchCompleteness(h);
for (const evidence of [null, { ...result, assessmentId: '3' }, { ...result, revisionId: '4' }]) {
assert.deepEqual(amountValues(records.archiveSections(detail, evidence)), ['未填写', '未填写', '未填写', '未填写']);
}
assert.equal(h.calls.length, 2);
h.enqueue('/assessments/1/report', reportResponse({ revisionId: '4' }), { revisionId: '2' });
await assert.rejects(records.fetchArchiveCompleteness('1', '2'), /报告版本不一致/);
});
await test('display adaptation copies changed amount fields and preserves original values and provided counts', async () => {
const h = harness(); const records = h.load('packages/finance/services/records.uts'); const detail = await fetchDetail(h);
const result = await fetchCompleteness(h); const before = JSON.stringify(detail);
const display = records.archiveSections(detail, result);
assert.notEqual(display, detail.sections); assert.notEqual(display[0], detail.sections[0]);
const changed = display.find(item => item.id === 'assets').fields.find(item => item.fieldKey === 'totalDebtBalance');
const originalField = detail.sections.find(item => item.id === 'assets').fields.find(item => item.fieldKey === 'totalDebtBalance');
assert.notEqual(display[0].fields, detail.sections[0].fields); assert.notEqual(changed, originalField);
assert.deepEqual(plain(display.map(item => item.provided)), plain(detail.sections.map(item => item.provided)));
changed.value = 'changed-display-only'; assert.equal(JSON.stringify(detail), before);
});
await test('archive page uses its existing report read to present markers with no additional requests', async () => {
const h = harness(); const { view } = await openPage(h);
assert.equal(view.error.value, ''); assert.equal(view.completeness.value, 100);
assert.deepEqual(amountValues(view.sections.value), ['不适用', '不适用', '不适用', '不适用']);
assert.deepEqual(amountValues(view.detail.value.sections), ['未填写', '未填写', '未填写', '未填写']);
assert.equal(h.calls.length, 4); assert.equal(h.calls.filter(item => item.route.endsWith('/report')).length, 1);
assert.equal(h.queue.length, 0); h.hooks.hide(); assert.equal(view.archiveCompleteness.value, null);
});
await test('failed report read leaves raw amounts and manual retry restores only saved display markers', async () => {
const h = harness(); const { view } = await openPage(h, null, { error: true });
assert.equal(view.completenessFailed.value, true); assert.equal(view.archiveCompleteness.value, null);
assert.deepEqual(amountValues(view.sections.value), ['未填写', '未填写', '未填写', '未填写']);
h.enqueue('/assessments/1/report', reportResponse(), { revisionId: '2' }); view.retryCompleteness(); await settle();
assert.equal(view.completenessFailed.value, false); assert.deepEqual(amountValues(view.sections.value), ['不适用', '不适用', '不适用', '不适用']);
h.fail('/assessments/1/report'); view.retryCompleteness(); await settle();
assert.equal(view.completenessFailed.value, true); assert.equal(view.archiveCompleteness.value, null);
assert.deepEqual(amountValues(view.sections.value), ['未填写', '未填写', '未填写', '未填写']); assert.equal(h.calls.length, 6);
h.hooks.hide();
});
await test('late earlier report response cannot replace a newer manual retry result', async () => {
const h = harness(); const { view, pending } = await openPage(h, null, { deferred: true });
const newer = h.defer('/assessments/1/report', { revisionId: '2' }); view.retryCompleteness();
newer.resolve(reportResponse({ completeness: { items: [{ fieldKey: 'monthlyLoanPayment', state: 'not_applicable' }] } })); await settle();
assert.deepEqual(amountValues(view.sections.value), ['未填写', '不适用', '未填写', '未填写']);
pending.resolve(reportResponse()); await settle();
assert.deepEqual(amountValues(view.sections.value), ['未填写', '不适用', '未填写', '未填写']); h.hooks.hide();
});
await test('late failed earlier read cannot clear a newer successful display result', async () => {
const h = harness(); const { view, pending } = await openPage(h, null, { deferred: true });
h.enqueue('/assessments/1/report', reportResponse(), { revisionId: '2' }); view.retryCompleteness(); await settle();
pending.reject(new Error('synthetic old read failure')); await settle();
assert.equal(view.completenessFailed.value, false); assert.deepEqual(amountValues(view.sections.value), ['不适用', '不适用', '不适用', '不适用']); h.hooks.hide();
});
await test('changing account before a delayed report resolves clears old archive data', async () => {
const h = harness(); const { view, pending } = await openPage(h, null, { deferred: true });
h.setSession('synthetic-other-owner'); pending.resolve(reportResponse()); await settle();
assert.equal(view.detail.value, null); assert.equal(view.archiveCompleteness.value, null);
assert.ok(view.sections.value.every(item => item.fields.length === 0)); h.hooks.hide();
});
await test('hide before report completion prevents display evidence and personal detail from reappearing', async () => {
const h = harness(); const { view, pending } = await openPage(h, null, { deferred: true });
h.hooks.hide(); pending.resolve(reportResponse()); await settle();
assert.equal(view.detail.value, null); assert.equal(view.archiveCompleteness.value, null);
assert.equal(view.completeness.value, null);
});
await test('late report cannot attach after current detail changes assessment or revision', async () => {
for (const response of [detailResponse({ assessmentId: '3' }), detailResponse({ revisionId: '4', publishedRevisionId: '4' })]) {
const h = harness(); const { view, pending } = await openPage(h, null, { deferred: true });
view.detail.value = await fetchDetail(h, response); pending.resolve(reportResponse()); await settle();
assert.equal(view.archiveCompleteness.value, null);
assert.deepEqual(amountValues(view.sections.value), ['未填写', '未填写', '未填写', '未填写']); h.hooks.hide();
}
});
await test('processing or failed assessments do not fetch or infer report display markers', async () => {
for (const status of ['processing', 'failed']) {
const h = harness(); const { view } = await openPage(h, null, { detail: detailResponse({ status, hasReport: false, publishedRevisionId: '' }) });
assert.equal(view.archiveCompleteness.value, null); assert.equal(h.calls.length, 3);
assert.deepEqual(amountValues(view.sections.value), ['未填写', '未填写', '未填写', '未填写']); h.hooks.hide();
}
});
console.log(`${failed ? 'FAIL' : 'PASS'} ${passed} archive display checks; ${failed} failures; no network/database/model/build.`);
process.exitCode = failed ? 1 : 0;
})();