fix: 修复匹配详情提示及公积金可选路径问题
修复匹配详情中“用户自填未经核验”等来源说明,清理未参与项的橙色注意提示,修正贷款统计口径文案;补充公积金完整可选路径提取逻辑,确保明确无额外时长要求的公积金分支可正确识别并通过。
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
// 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;
|
||||
})();
|
||||
Reference in New Issue
Block a user