Archived
用户:「创建活动的启动参数里面不应该能选到同样的两个芯片」。 查下来这不只是没意义,是**把单子做死**:sub_eval.part 的主键是 eval_ref+internal_pn (role 不在主键里),S1 配方铸的两行撞成同一个主键、后一笔覆盖前一笔,两行收成一行 —— sub_eval_part_exists_status_found = 2 永不成立,S1 收不了步,而件上那两行人加不出来 (行源 mine:,没有「+加一行」),唯一出路是终止重开。与「两格留空」是同一条链。 平台加互斥声明 x-distinct(契约 distinctGroup)——与 x-link 正相反:那个是引导,这个是墙。 · 前端:别的格占着的候选这一格不列(自己填着的那个永远留着,否则 select 当场变空白), 并说清是谁占了哪个;走「新增」自己写一个同样的 → 小红字 + 启动按不下去; · 服务端 validateLaunchParams 独立拦(脚本 / API 绕不过),400 点名后填的那格; · 「算不算同一个」= 去空白 + 不分大小写(launchDistinctKey,前后端同一把尺)—— 只挡精确相等的话 lm2596s-5.0 照样进去,混一颗垃圾主数据出来。 W05 两格挂 x-distinct: 'part',2.14.0 已装库核过。 测:服务端 6 条 + 前端 8 条(含联动与互斥合起来的真形状)+ 包内 1 条,三处变异验证过; 四道门禁绿(2548 测)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
354 lines
19 KiB
TypeScript
354 lines
19 KiB
TypeScript
/**
|
|
* 启动参数表:从清单 `input.data` 派生 + 服务端校验。
|
|
* 夹具照 W05 / E04 的清单形状手写(apps 不得 import activities —— depcruise 在守)。
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import { controlOf, fieldSpecsOf, launchParamOf, launchParamsOf, validateLaunchParams } from '../activity-ops/launch-params.ts';
|
|
|
|
/** W05 的四参 + 选填文件,按 2026-08-27 的清单写法 */
|
|
const W05_INPUT = {
|
|
type: 'object',
|
|
required: ['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns'],
|
|
properties: {
|
|
customerCode: {
|
|
type: 'string',
|
|
title: '客户',
|
|
description: '台账里有就直接选;新客户自己填',
|
|
examples: ['CU-0142'],
|
|
'x-control': 'combo',
|
|
'x-options': { ledger: 'customer.profile', value: 'customer_code', label: 'name' },
|
|
},
|
|
customerName: { type: 'string', title: '客户名称', examples: ['深圳恒星电源有限公司'] },
|
|
requestDescription: { type: 'string', title: '客户需求', 'x-control': 'textarea', examples: ['工业电源项目在用 LM2596S-5.0'] },
|
|
originalInternalPn: { type: 'string', title: '原内部料号', 'x-control': 'combo', 'x-options': { ledger: 'parts.profile', value: 'internal_pn' } },
|
|
candidateInternalPns: { type: 'array', title: '候选内部料号', items: { type: 'string' }, examples: [['XL2596S-5.0', 'LM2596S-ADJ']] },
|
|
datasheets: { type: 'array', format: 'file', title: 'datasheet', items: { type: 'object' } },
|
|
},
|
|
};
|
|
|
|
/** E04:两级下拉 + 数组(元素形状不声明)+ 选填说明 */
|
|
const E04_INPUT = {
|
|
type: 'object',
|
|
required: ['trigger'],
|
|
properties: {
|
|
trigger: { type: 'string', enum: ['monthly', 'event'], enumNames: ['月度例行', '事件驱动'], title: '触发方式', default: 'monthly' },
|
|
eventType: { type: 'string', enum: ['new_customer', 'major_deal'], title: '事件类型', showWhen: { field: 'trigger', equals: 'event' } },
|
|
customers: { type: 'array', title: '本轮涉及的客户', examples: [['CU-0142']] },
|
|
note: { type: 'string', title: '本轮说明' },
|
|
budget: { type: 'integer', title: '预算' },
|
|
},
|
|
};
|
|
|
|
const noResolve = async () => undefined;
|
|
|
|
describe('fieldSpecsOf / controlOf', () => {
|
|
it('保属性序;必填从 required[] 认;x-control / x-options / description 都读到', () => {
|
|
const specs = fieldSpecsOf(W05_INPUT);
|
|
expect(specs.map((s) => s.key)).toEqual(['customerCode', 'customerName', 'requestDescription', 'originalInternalPn', 'candidateInternalPns', 'datasheets']);
|
|
expect(specs[0]).toMatchObject({ required: true, control: 'combo', description: '台账里有就直接选;新客户自己填', optionsSource: { ledger: 'customer.profile', value: 'customer_code', label: 'name' } });
|
|
// label 列没写就用 value 列当标签
|
|
expect(specs[3]?.optionsSource).toEqual({ ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn' });
|
|
expect(specs[1]?.required).toBe(false);
|
|
});
|
|
|
|
it('控件推断:文件不上表单、字符串数组默认 list、enum 是 select、其余 text;数组标了 combo/select 就摆成单选', () => {
|
|
const specs = fieldSpecsOf(W05_INPUT);
|
|
expect(specs.map((s) => controlOf(s))).toEqual(['combo', 'text', 'textarea', 'combo', 'list', undefined]);
|
|
const e04 = fieldSpecsOf(E04_INPUT);
|
|
expect(e04.map((s) => controlOf(s))).toEqual(['select', 'select', 'list', 'text', 'text']);
|
|
// 数组字段标 combo → 摆成单选(提交时后端包成数组);标 list 或不标 → list
|
|
expect(controlOf({ ...specs[4]!, control: 'combo' })).toBe('combo');
|
|
expect(controlOf({ ...specs[4]!, control: 'list' })).toBe('list');
|
|
expect(controlOf({ ...specs[4]!, itemType: 'object' })).toBeUndefined();
|
|
expect(controlOf({ ...specs[1]!, type: 'object' })).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('x-order', () => {
|
|
it('声明了次序的按数字排,没声明的殿后保持原序(jsonb 不保键序,书写顺序靠不住)', () => {
|
|
const specs = fieldSpecsOf({
|
|
properties: {
|
|
customerCode: { type: 'string', 'x-order': 1 },
|
|
customerName: { type: 'string', 'x-order': 2 },
|
|
originalInternalPn: { type: 'string', 'x-order': 4 },
|
|
requestDescription: { type: 'string', 'x-order': 3 },
|
|
zz: { type: 'string' },
|
|
aa: { type: 'string' },
|
|
},
|
|
});
|
|
expect(specs.map((s) => s.key)).toEqual(['customerCode', 'customerName', 'requestDescription', 'originalInternalPn', 'zz', 'aa']);
|
|
});
|
|
});
|
|
|
|
describe('launchParamsOf', () => {
|
|
it('W05:五参(datasheets 省略);combo 候选来自 resolver;placeholder 取 examples[0]', async () => {
|
|
const asked: string[] = [];
|
|
const params = await launchParamsOf(W05_INPUT, async (src) => {
|
|
asked.push(src.ledger);
|
|
return src.ledger === 'customer.profile' ? [{ value: 'CU-0142', label: 'Itelma' }] : undefined;
|
|
});
|
|
expect(params.map((p) => p.key)).toEqual(['customerCode', 'customerName', 'requestDescription', 'originalInternalPn', 'candidateInternalPns']);
|
|
expect(asked).toEqual(['customer.profile', 'parts.profile']);
|
|
expect(params[0]).toEqual({
|
|
key: 'customerCode',
|
|
label: '客户',
|
|
type: 'combo',
|
|
required: true,
|
|
desc: '台账里有就直接选;新客户自己填',
|
|
placeholder: 'CU-0142',
|
|
options: [{ value: 'CU-0142', label: 'Itelma' }],
|
|
});
|
|
// 读不到候选的 combo 仍是 combo(自由填),只是没有 options
|
|
expect(params[3]).toEqual({ key: 'originalInternalPn', label: '原内部料号', type: 'combo', required: true });
|
|
expect(params[4]).toMatchObject({ type: 'list', placeholder: 'XL2596S-5.0', required: true });
|
|
expect(params[2]).toMatchObject({ type: 'textarea', placeholder: '工业电源项目在用 LM2596S-5.0' });
|
|
});
|
|
|
|
it('E04:enum → select 带人读名与 default;没 enumNames 拿值当标签;数组元素形状不声明也当 list', async () => {
|
|
const params = await launchParamsOf(E04_INPUT, noResolve);
|
|
expect(params[0]).toEqual({
|
|
key: 'trigger',
|
|
label: '触发方式',
|
|
type: 'select',
|
|
required: true,
|
|
options: [{ value: 'monthly', label: '月度例行' }, { value: 'event', label: '事件驱动' }],
|
|
default: 'monthly',
|
|
});
|
|
expect(params[1]?.options).toEqual([{ value: 'new_customer', label: 'new_customer' }, { value: 'major_deal', label: 'major_deal' }]);
|
|
expect(params[2]).toMatchObject({ type: 'list', placeholder: 'CU-0142' });
|
|
});
|
|
|
|
it('x-control:select 却一个候选都没有 → 降成 text(只能选却无可选等于锁死)', () => {
|
|
const spec = fieldSpecsOf({ properties: { k: { type: 'string', 'x-control': 'select' } } })[0]!;
|
|
expect(launchParamOf(spec, undefined)).toEqual({ key: 'k', label: 'k', type: 'text' });
|
|
expect(launchParamOf(spec, [])?.type).toBe('text');
|
|
});
|
|
});
|
|
|
|
describe('数组字段摆成单选(2026-08-27 用户定:候选料号与原料号同款下拉,只选一颗)', () => {
|
|
const INPUT = {
|
|
required: ['candidateInternalPns'],
|
|
properties: {
|
|
candidateInternalPns: {
|
|
type: 'array',
|
|
title: '候选内部料号',
|
|
items: { type: 'string' },
|
|
examples: [['XL2596S-5.0', 'LM2596S-ADJ']],
|
|
default: ['LM2596S-ADJ'],
|
|
'x-control': 'combo',
|
|
'x-options': { ledger: 'parts.profile', value: 'internal_pn' },
|
|
},
|
|
tags: { type: 'array', items: { type: 'string' }, enum: ['a', 'b'], 'x-control': 'select' },
|
|
},
|
|
};
|
|
|
|
it('数组 + x-control:combo → combo 控件,占位与预填取第一颗,候选照读', async () => {
|
|
const params = await launchParamsOf(INPUT, async () => [{ value: 'LM2596S-ADJ', label: 'LM2596S-ADJ' }]);
|
|
expect(params[0]).toEqual({
|
|
key: 'candidateInternalPns',
|
|
label: '候选内部料号',
|
|
type: 'combo',
|
|
required: true,
|
|
placeholder: 'XL2596S-5.0',
|
|
options: [{ value: 'LM2596S-ADJ', label: 'LM2596S-ADJ' }],
|
|
default: 'LM2596S-ADJ',
|
|
});
|
|
expect(params[1]).toMatchObject({ type: 'select', options: [{ value: 'a', label: 'a' }, { value: 'b', label: 'b' }] });
|
|
});
|
|
|
|
it('提交单个字符串包成一元数组;传数组也照收;空的按必填拦;select 逐项对候选', async () => {
|
|
const specs = fieldSpecsOf(INPUT);
|
|
const params = await launchParamsOf(INPUT, noResolve);
|
|
expect(validateLaunchParams(params, { candidateInternalPns: 'XL2596S-5.0' }, specs)).toEqual({ ok: true, data: { candidateInternalPns: ['XL2596S-5.0'] } });
|
|
expect(validateLaunchParams(params, { candidateInternalPns: ['XL2596S-5.0', ' LM2596S-ADJ '] }, specs)).toEqual({ ok: true, data: { candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'] } });
|
|
const empty = validateLaunchParams(params, { candidateInternalPns: ' ' }, specs);
|
|
expect(empty).toEqual({ ok: false, issues: [{ key: 'candidateInternalPns', message: '必填' }] });
|
|
const off = validateLaunchParams(params, { candidateInternalPns: 'x', tags: ['a', 'z'] }, specs);
|
|
expect(off).toEqual({ ok: false, issues: [{ key: 'tags', message: '不在候选里(单选只能选候选)' }] });
|
|
expect(validateLaunchParams(params, { candidateInternalPns: 'x', tags: 'b' }, specs)).toEqual({ ok: true, data: { candidateInternalPns: ['x'], tags: ['b'] } });
|
|
});
|
|
});
|
|
|
|
describe('validateLaunchParams', () => {
|
|
it('必填空 / 未知键 / 类型不符 / select 不在候选 各点名一格;combo 候选之外合法', async () => {
|
|
const specs = fieldSpecsOf(W05_INPUT);
|
|
const params = await launchParamsOf(W05_INPUT, async () => [{ value: 'CU-0142', label: 'Itelma' }]);
|
|
const bad = validateLaunchParams(params, { customerCode: 'CU-NEW', requestDescription: '', candidateInternalPns: [' '], oops: 'x' }, specs);
|
|
expect(bad.ok).toBe(false);
|
|
if (!bad.ok) {
|
|
expect(bad.issues.map((i) => i.key).sort()).toEqual(['candidateInternalPns', 'oops', 'originalInternalPn', 'requestDescription']);
|
|
}
|
|
const typed = validateLaunchParams(params, { customerCode: ['CU-1'], requestDescription: 'x', originalInternalPn: 'p', candidateInternalPns: 'PN-1' }, specs);
|
|
expect(typed.ok).toBe(false);
|
|
// 标量格传数组是错;list 格传单个字符串**照收**(包成一元数组)
|
|
if (!typed.ok) expect(typed.issues).toEqual([{ key: 'customerCode', message: '要传字符串' }]);
|
|
const wrapped = validateLaunchParams(params, { customerCode: 'CU-1', requestDescription: 'x', originalInternalPn: 'p', candidateInternalPns: ' PN-1 ' }, specs);
|
|
expect(wrapped).toMatchObject({ ok: true, data: { candidateInternalPns: ['PN-1'] } });
|
|
|
|
const good = validateLaunchParams(
|
|
params,
|
|
{ customerCode: 'CU-NEW', customerName: '', requestDescription: ' 客户要求验证替代料 ', originalInternalPn: 'LM2596S-5.0', candidateInternalPns: ['XL2596S-5.0', '', ' LM2596S-ADJ '] },
|
|
specs,
|
|
);
|
|
expect(good).toEqual({
|
|
ok: true,
|
|
// 可选空串不落入参;list 逐项去空白、丢空项;combo 候选之外原样收
|
|
data: { customerCode: 'CU-NEW', requestDescription: '客户要求验证替代料', originalInternalPn: 'LM2596S-5.0', candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'] },
|
|
});
|
|
});
|
|
|
|
it('select 只认候选;整数按清单类型转回数字', async () => {
|
|
const specs = fieldSpecsOf(E04_INPUT);
|
|
const params = await launchParamsOf(E04_INPUT, noResolve);
|
|
const bad = validateLaunchParams(params, { trigger: 'weekly' }, specs);
|
|
expect(bad.ok).toBe(false);
|
|
if (!bad.ok) expect(bad.issues[0]).toMatchObject({ key: 'trigger', message: expect.stringContaining('候选') });
|
|
expect(validateLaunchParams(params, { trigger: 'event', budget: '12' }, specs)).toEqual({ ok: true, data: { trigger: 'event', budget: 12 } });
|
|
const nan = validateLaunchParams(params, { trigger: 'event', budget: '1.5' }, specs);
|
|
expect(nan.ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
/* ── 按分组键联动(2026-08-31 用户定:原料号 / 候选料号按芯片类型互相收窄)────────────
|
|
派生层只负责把两样东西送到前端:①候选带上它的分组键与人读名(`x-options.group`);
|
|
②这一格属于哪个联动组(`x-link`)。收窄怎么摆是前端的事,**服务端不据此立墙**。 */
|
|
describe('x-options.group / x-link', () => {
|
|
const LINKED = {
|
|
type: 'object',
|
|
required: ['originalInternalPn', 'candidateInternalPn'],
|
|
properties: {
|
|
originalInternalPn: {
|
|
type: 'string',
|
|
title: '原内部料号',
|
|
'x-control': 'combo',
|
|
'x-options': {
|
|
ledger: 'parts.profile',
|
|
value: 'internal_pn',
|
|
label: 'internal_pn',
|
|
group: { column: 'chip_type_id', ledger: 'parts.chip_type', value: 'type_id', label: 'type_name' },
|
|
},
|
|
'x-link': 'chip-type',
|
|
'x-order': 1,
|
|
},
|
|
candidateInternalPn: {
|
|
type: 'string',
|
|
title: '候选内部料号',
|
|
'x-control': 'combo',
|
|
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn', group: 'chip_type_id' },
|
|
'x-link': 'chip-type',
|
|
'x-order': 2,
|
|
},
|
|
},
|
|
};
|
|
|
|
it('分组键两种写法都认:整份声明(带字典)与只写列名;x-link 读成联动组名', () => {
|
|
const specs = fieldSpecsOf(LINKED);
|
|
expect(specs[0]?.optionsSource?.group).toEqual({ column: 'chip_type_id', dict: { ledger: 'parts.chip_type', value: 'type_id', label: 'type_name' } });
|
|
// 只写列名 = 有分组键、没有人读名字典
|
|
expect(specs[1]?.optionsSource?.group).toEqual({ column: 'chip_type_id', dict: undefined });
|
|
expect(specs.map((s) => s.linkGroup)).toEqual(['chip-type', 'chip-type']);
|
|
});
|
|
|
|
it('分组键写残(没有 column,或字典缺 ledger/value)就当没声明 —— 候选照出,只是不联动', () => {
|
|
const bad = fieldSpecsOf({
|
|
properties: {
|
|
a: { type: 'string', 'x-control': 'combo', 'x-options': { ledger: 'l.o', value: 'v', group: {} } },
|
|
b: { type: 'string', 'x-control': 'combo', 'x-options': { ledger: 'l.o', value: 'v', group: { column: 'c', ledger: 'd.t' } } },
|
|
},
|
|
});
|
|
expect(bad[0]?.optionsSource).toEqual({ ledger: 'l.o', value: 'v', label: 'v', group: undefined });
|
|
// 字典写残 → 只留分组键,人读名没有(不炸)
|
|
expect(bad[1]?.optionsSource?.group).toEqual({ column: 'c', dict: undefined });
|
|
});
|
|
|
|
it('候选带 group / groupLabel 原样交给前端;有候选才给 linkGroup(一个候选都没有,联动无从谈起)', async () => {
|
|
const params = await launchParamsOf(LINKED, async (src) =>
|
|
src.group === undefined
|
|
? [{ value: 'AMS1117-3.3', label: 'AMS1117-3.3' }]
|
|
: [
|
|
{ value: 'LM2596S-5.0', label: 'LM2596S-5.0', group: '1', groupLabel: 'DC-DC 降压稳压器' },
|
|
{ value: 'OLD-001', label: 'OLD-001', group: '' },
|
|
],
|
|
);
|
|
expect(params[0]).toMatchObject({
|
|
key: 'originalInternalPn',
|
|
linkGroup: 'chip-type',
|
|
options: [
|
|
{ value: 'LM2596S-5.0', label: 'LM2596S-5.0', group: '1', groupLabel: 'DC-DC 降压稳压器' },
|
|
{ value: 'OLD-001', label: 'OLD-001', group: '' },
|
|
],
|
|
});
|
|
// 候选读不到 → 仍是 combo,但不摆联动组(前端没有可收窄的东西)
|
|
const empty = await launchParamsOf(LINKED, noResolve);
|
|
expect(empty[0]?.linkGroup).toBeUndefined();
|
|
expect(empty[0]?.type).toBe('combo');
|
|
});
|
|
|
|
it('服务端不据分组立墙:两格填不同类型的料号照样收(类型一致归活动的收步判据管)', async () => {
|
|
const params = await launchParamsOf(LINKED, async () => [
|
|
{ value: 'LM2596S-5.0', label: 'LM2596S-5.0', group: '1' },
|
|
{ value: 'AMS1117-3.3', label: 'AMS1117-3.3', group: '2' },
|
|
]);
|
|
const out = validateLaunchParams(params, { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' }, fieldSpecsOf(LINKED));
|
|
expect(out).toEqual({ ok: true, data: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' } });
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 互斥组(2026-08-31 用户定:「不应该能选到同样的两个芯片」)。
|
|
* 与 `x-link` 正相反 —— **这一条是墙**:W05 两格同一颗料会把单子做死(两行撞同一个主键收成一行,
|
|
* S1 的 `exists_status_found = 2` 永不成立,而件上那两行人加不出来),而唯一能改的时机就是起单这一刻。
|
|
*/
|
|
describe('x-distinct', () => {
|
|
const DISTINCT = {
|
|
type: 'object',
|
|
required: ['originalInternalPn', 'candidateInternalPn'],
|
|
properties: {
|
|
originalInternalPn: { type: 'string', title: '原内部料号', 'x-control': 'combo', 'x-options': { ledger: 'parts.profile', value: 'internal_pn' }, 'x-distinct': 'part', 'x-order': 1 },
|
|
candidateInternalPn: { type: 'string', title: '候选内部料号', 'x-control': 'combo', 'x-options': { ledger: 'parts.profile', value: 'internal_pn' }, 'x-distinct': 'part', 'x-order': 2 },
|
|
/* 同一张表上不在这一组的格子:填成一样也与它无关 */
|
|
note: { type: 'string', title: '备注' },
|
|
},
|
|
};
|
|
const paramsOf = async () => launchParamsOf(DISTINCT, async () => [{ value: 'LM2596S-5.0', label: 'LM2596S-5.0' }, { value: 'XL2596S-5.0', label: 'XL2596S-5.0' }]);
|
|
|
|
it('读得到组名;**不以有候选为前提** —— 自由输入的两格同样归它管(墙管的是值,不是候选)', async () => {
|
|
expect(fieldSpecsOf(DISTINCT).map((s) => s.distinctGroup)).toEqual(['part', 'part', undefined]);
|
|
const bare = await launchParamsOf({ properties: { a: { type: 'string', 'x-distinct': 'g' }, b: { type: 'string', 'x-distinct': '' } } }, noResolve);
|
|
expect(bare.map((p) => [p.type, p.distinctGroup])).toEqual([
|
|
['text', 'g'],
|
|
['text', undefined],
|
|
]);
|
|
});
|
|
|
|
it('两格同一个值 → 拒,点名**后填的那格**,话里指向它撞上了谁', async () => {
|
|
const out = validateLaunchParams(await paramsOf(), { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'LM2596S-5.0' }, fieldSpecsOf(DISTINCT));
|
|
expect(out).toEqual({ ok: false, issues: [{ key: 'candidateInternalPn', message: '与「原内部料号」填的是同一个 —— 这两格不能一样,换一个再启动' }] });
|
|
});
|
|
|
|
it('**去空白 + 不分大小写**:只挡精确相等的话,`lm2596s-5.0` 会照样进去,混一颗垃圾主数据出来', async () => {
|
|
const out = validateLaunchParams(await paramsOf(), { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: ' lm2596s-5.0 ' }, fieldSpecsOf(DISTINCT));
|
|
expect(out.ok).toBe(false);
|
|
});
|
|
|
|
it('两颗不同的料照收;不在这一组的格子填成一样也不管', async () => {
|
|
const out = validateLaunchParams(
|
|
await paramsOf(),
|
|
{ originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0', note: 'LM2596S-5.0' },
|
|
fieldSpecsOf(DISTINCT),
|
|
);
|
|
expect(out).toEqual({ ok: true, data: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0', note: 'LM2596S-5.0' } });
|
|
});
|
|
|
|
it('空的不算撞上 —— 两格都没填该报的是必填,不是「填了同一个」', async () => {
|
|
const out = validateLaunchParams(await paramsOf(), { originalInternalPn: '', candidateInternalPn: '' }, fieldSpecsOf(DISTINCT));
|
|
expect(out).toEqual({ ok: false, issues: [{ key: 'originalInternalPn', message: '必填' }, { key: 'candidateInternalPn', message: '必填' }] });
|
|
});
|
|
|
|
it('数组格不参与(契约写死的边界:本期没有这种形状)', async () => {
|
|
const arrays = { properties: { a: { type: 'array', items: { type: 'string' }, title: 'A', 'x-distinct': 'g' }, b: { type: 'array', items: { type: 'string' }, title: 'B', 'x-distinct': 'g' } } };
|
|
const out = validateLaunchParams(await launchParamsOf(arrays, noResolve), { a: ['X'], b: ['X'] }, fieldSpecsOf(arrays));
|
|
expect(out).toEqual({ ok: true, data: { a: ['X'], b: ['X'] } });
|
|
});
|
|
});
|