大交通检索修复

This commit is contained in:
andy committed 2026-09-16 11:50:50 +08:00
1 parent 364ef4ae30
commit 8a0f02c9be
29 files changed
+1175 -33

No files matched your search

+125
View File
@@ -344,6 +344,131 @@ test('explicit ticket filter never drops a keyword or defaults to the first ambi
assert.equal(duplicate.api.testResolve(duplicate.form, duplicate.input).ok, false, 'replacement filter must not disambiguate original rows');
});
const timedTicketRows = [
'270◆测试新资源◆拉邦-万荣 D83 18:11-19:02◆公司现付',
'271◆测试新资源◆拉邦-万荣【C91 12:52-13:44】◆公司现付',
'272◆测试新资源◆拉邦-万象【C91 12:28-14:31】◆公司现付',
'273◆测试新资源◆拉邦-万荣 C911 12:52-13:44◆公司现付',
'274◆另一票务◆拉邦-万荣 C91 12:52-13:44◆公司现付'
].join('◇');
for (const mode of ['create', 'update']) test(`transport ${mode} distinguishes route and train before an adjacent timetable`, async () => {
for (const [query, id] of [
['拉邦-万荣 C91', '271'],
['拉邦-万象 C91', '272'],
['拉邦-万荣 c91', '271'],
['拉邦-万荣 C91 12:52-13:44', '271'],
['拉邦-万象 C91 12:28-14:31', '272']
]) {
const f = filteredTransportFixture(mode, timedTicketRows);
f.input.data.arrangement.ticket_description = query;
const resolved = f.api.testResolve(f.form, f.input);
assert.equal(resolved.ok, true, JSON.stringify({ query, resolved }));
const op = JSON.parse(JSON.stringify(resolved.operation));
const replacement = mode === 'update' ? op.data.arrangement.changes : op.data.arrangement;
assert.equal(replacement.supplier.id, id);
assert.equal(resolved.evidence.candidates, undefined, 'successful lookups do not add candidate details');
assert.equal(f.api.testResource(f.form, op).matched, true);
const applied = await f.api.testApply(f.form, op);
assert.deepEqual(Array.from(applied.blockers), [], JSON.stringify(applied));
assert.equal(f.selections(), 1);
assert.equal(f.fields.get(`danweiid${mode === 'create' ? 0 : 1}`).value, id);
assert.equal(f.api.testExpected(f.form, op).blockers.length, 0);
}
});
test('description normalization preserves original numeric boundaries and existing formatting compatibility', () => {
const { api } = filteredTransportFixture('create');
for (const [item, query, expected] of [
['C91 12:52-13:44', 'C91', true],
['【C91】12:52-13:44', 'c91', true],
['C91\t12:52-13:44', 'C91', true],
['C91\u00a012:52-13:44', 'C91', true],
['C911 12:52-13:44', 'C91', false],
['C9112:52-13:44', 'C91', false],
['D888 12:52-13:44', 'D88', false],
['115 120', '15', false],
['15 120', '15', true],
['【15座】 VIP 120', '15座vip', true],
['C91 12:52-13:44', '【】', false]
]) {
assert.equal(api.matchesTicketDescription(item, query), expected, `${item} / ${query}`);
assert.equal(api.matchesVehicleType(item, query), expected, `shared vehicle filter: ${item} / ${query}`);
}
});
test('route-only or train-only filters block with only the matching supplier candidates and no mutation', () => {
for (const mode of ['create', 'update']) for (const [query, count] of [['拉邦-万荣', 3], ['C91', 2]]) {
const f = filteredTransportFixture(mode, timedTicketRows);
f.input.data.arrangement.ticket_description = query;
const before = [...f.fields.values()].map(c => c.value);
const resolved = f.api.testResolve(f.form, f.input);
assert.equal(resolved.ok, false);
assert.ok(resolved.blockers.includes(`arrangement_resource_candidate_count:${count}`));
assert.equal(resolved.evidence.candidates.length, count);
assert.equal(resolved.evidence.value_redacted, false);
for (const candidate of resolved.evidence.candidates) {
assert.deepEqual(Object.keys(candidate).sort(), ['item', 'resource']);
assert.equal(candidate.resource, '测试新资源');
if (query === 'C91') assert.doesNotMatch(candidate.item, /C911|D83/);
}
assert.equal(f.selections(), 0);
assert.deepEqual([...f.fields.values()].map(c => c.value), before);
}
const single = filteredTransportFixture('create', '271◆测试新资源◆拉邦-万荣【C91 12:52-13:44】◆公司现付');
single.input.data.arrangement.ticket_description = '拉邦-万荣';
assert.equal(single.api.testResolve(single.form, single.input).ok, true, 'route alone is sufficient when unique');
});
test('ambiguous timetable candidates stay bounded and cannot default to an identical first row', () => {
for (const mode of ['create', 'update']) {
const rows = Array.from({ length: 7 }, (_, i) => `${800 + i}◆测试新资源◆C91 12:52-13:44 ${'长'.repeat(150)}\n◆私有列`).join('◇');
const f = filteredTransportFixture(mode, rows);
f.input.data.arrangement.ticket_description = 'C91';
const resolved = f.api.testResolve(f.form, f.input);
assert.equal(resolved.ok, false);
assert.ok(resolved.blockers.includes('arrangement_resource_candidate_count:7'));
assert.equal(resolved.evidence.candidates.length, 5);
for (const row of resolved.evidence.candidates) assert.ok(row.item.length <= 120);
assert.doesNotMatch(JSON.stringify(resolved.evidence.candidates), /私有列|800|801|\\n/);
assert.equal(f.selections(), 0);
}
});
test('transport route failures explain zero matches, unavailable data and bounded ambiguous candidates', () => {
const source = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
const start = source.indexOf('function lifecycleRouteFailure(');
const end = source.indexOf('\nasync function executeLifecycleOperation', start);
const failure = vm.runInNewContext(`(${source.slice(start, end).trim()})`);
for (const mode of ['create', 'update']) {
const f = filteredTransportFixture(mode, timedTicketRows);
f.input.data.arrangement.ticket_description = 'C91';
const resolved = f.api.testResolve(f.form, f.input);
const result = failure(f.input, { blockers: resolved.blockers, preflight: { resource_resolution: resolved.evidence } });
assert.equal(result.errorCode, 'arrangement_resource_not_unique');
assert.match(result.message, /2 条大交通候选/);
assert.match(result.message, /拉邦-万荣.*12:52-13:44/);
assert.match(result.message, /拉邦-万象.*12:28-14:31/);
assert.match(result.message, /车票说明.*时刻/);
assert.match(result.message, /未写入 ERP/);
assert.doesNotMatch(result.message, /C911|另一票务|未能按精确引用|271|272/);
const absent = failure(f.input, { blockers: ['arrangement_resource_candidate_count:0'] });
assert.equal(absent.errorCode, 'arrangement_resource_not_found');
assert.match(absent.message, /没有找到同时匹配结算单位和车票说明/);
const unloaded = failure(f.input, { blockers: ['arrangement_resource_candidate_data_missing', 'arrangement_resource_candidate_count:0'] });
assert.equal(unloaded.errorCode, 'arrangement_resource_lookup_not_ready');
assert.doesNotMatch(unloaded.message, /没有找到/);
}
const report = { blockers: ['arrangement_resource_candidate_count:7'], preflight: { resource_resolution: { candidates:
Array.from({length: 7}, (_, i) => ({ resource: '票务\n公司', item: `候选${i} ${'长'.repeat(150)}`, id: 'secret-id', raw: 'secret-raw' }))
} } };
const bounded = failure({ action: 'arrangement_transport' }, report);
assert.match(bounded.message, /仅显示前 5 条/);
assert.match(bounded.message, /票务 公司/);
assert.doesNotMatch(bounded.message, /候选5|候选6|secret/);
assert.ok(bounded.message.length < 1500);
});
test('changed ticket description or candidates block before any native call or mutation', async () => {
for (const mode of ['create', 'update']) for (const mutation of ['description', 'candidate', 'duplicate']) {
const f = filteredTransportFixture(mode);