/** * 一封信 → 调用入参 —— 测试台起单前那两步的**接缝**。 * * 两步各自有测试(channel 的抽键、trigger 的装配),但**串起来**是新的: * 通道抽出来的键叫 `keys.customerCode`,触发器的映射也得照这个名字取 —— * 中间任何一头改了字段名,单还是能起,只是入参悄悄变空,流走到 G1 才莫名其妙地停。 * 所以这条测试盯的就是这个名字对不对得上。 */ import { describe, expect, it } from 'vitest'; import { evaluate as evalSfeel } from '@sfeel'; import { extractFields } from '@modules/channel'; import { assembleInput, scopeOf } from '@modules/trigger'; import type { TriggerConfig } from '@modules/trigger'; /** 与种子里 `ch-rfq-mail` 同一套抽键规则 */ const EXTRACT = [ { key: 'customerCode', regex: '\\bC\\d{3}\\b', source: 'both' as const }, { key: 'rfqRef', regex: 'RFQ-[A-Z0-9]+(?:-[A-Z0-9]+)*', source: 'both' as const }, ]; /** 与种子里「客户RFQ询价」同一套装配映射 */ const TRIGGER = { id: 'trg-rfq-mail', name: '客户RFQ询价', processPackageId: 'unified-quote', enabled: true, spec: { kind: 'event', eventType: 'data', rule: 'true' }, mapping: [ { source: { from: 'expression', sfeel: 'keys.customerCode' }, target: 'customerCode' }, { source: { from: 'expression', sfeel: 'keys.rfqRef' }, target: 'rfqRef' }, { source: { from: 'expression', sfeel: 'subject' }, target: 'subject' }, { source: { from: 'expression', sfeel: 'body' }, target: 'body' }, ], } as TriggerConfig; const evaluator = (expression: string, scope: Readonly>): unknown => { try { return evalSfeel(expression, scope); } catch { return undefined; } }; function assemble(subject: string, body = '正文'): ReturnType { const keys = extractFields({ subject, body, from: 'buyer@x.com', to: ['rfq@we.com'] }, EXTRACT); const scope = scopeOf({ type: 'data', keys, source: 'channel', occurredAt: '2026-07-29T00:00:00.000Z', payload: { subject, body, from: 'buyer@x.com', channelId: 'ch-rfq-mail' }, }); return assembleInput(TRIGGER, scope, ['customerCode', 'rfqRef'], evaluator); } describe('一封信 → 调用入参', () => { it('主题里带客户码与询价单号:两个必填都装得上', () => { const out = assemble('【询价】C014 / RFQ-2607-001 工控主板 BOM 12 项'); expect(out.kind).toBe('ready'); if (out.kind !== 'ready') return; expect(out.data['customerCode']).toBe('C014'); expect(out.data['rfqRef']).toBe('RFQ-2607-001'); expect(out.data['subject']).toContain('工控主板'); }); it('正文里带也算 —— 抽键两处都看(source: both)', () => { const out = assemble('询价', '客户 C014,单号 RFQ-2607-002,请报价'); expect(out.kind).toBe('ready'); if (out.kind !== 'ready') return; expect(out.data['rfqRef']).toBe('RFQ-2607-002'); }); it('抽不全:如实列出缺哪个,但**带着已有的照样能起** —— 宽进,收敛在活动', () => { const out = assemble('随便问问,有没有便宜的板子'); expect(out.kind).toBe('partial'); if (out.kind !== 'partial') return; expect(out.fields).toEqual(expect.arrayContaining(['customerCode', 'rfqRef'])); // 缺的键不以 undefined 占位 —— 下游按「键在不在」判缺不缺 expect('customerCode' in out.data).toBe(false); expect(out.data['subject'], '抽得到的照带').toBeDefined(); }); });