feat(loop-engine): 配方数据源展开 —— once / rows / each,嵌套两层封顶

里层的 where 认得出外层那一行:外层字段还是空的时候一行都不命中 ——
「人挑了类型、按了保存,参数格才出来」不是特判,是这一条的自然结果。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-29 03:24:58 +08:00
co-authored by Claude Opus 5
parent f950a4419d
commit 58ff4f1b90
2 changed files with 114 additions and 0 deletions
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { expandSource } from '../internal/domain/auto-source.ts';
const condition = (): boolean => false;
const ctxWith = (scope: Record<string, unknown>) => ({ scope, lets: {}, condition });
describe('expandSource', () => {
it('once:恰一行,行是空的', () => {
expect(expandSource({ once: true }, ctxWith({}))).toEqual([{ row: {} }]);
});
it('rows:字面几行,每格现算', () => {
const out = expandSource(
{ rows: [{ pn: { in: 'a' }, role: 'original' }, { pn: { in: 'b' }, role: 'candidate' }] },
{ scope: { a: 'LM2596S-5.0', b: 'XL2596S-5.0' }, lets: {}, condition },
);
expect(out).toEqual([
{ row: { pn: 'LM2596S-5.0', role: 'original' } },
{ row: { pn: 'XL2596S-5.0', role: 'candidate' } },
]);
});
it('each:按地址逐行', () => {
const out = expandSource({ each: 'mine:sub_eval.part' }, ctxWith({ 'mine:sub_eval.part': [{ pn: 'A' }, { pn: 'B' }] }));
expect(out.map((e) => e.row)).toEqual([{ pn: 'A' }, { pn: 'B' }]);
});
it('each + where:等值过滤,比值按字符串', () => {
const out = expandSource(
{ each: 'mine:sub_eval.part', where: { exists_status: 'absent' } },
ctxWith({ 'mine:sub_eval.part': [{ pn: 'A', exists_status: 'found' }, { pn: 'B', exists_status: 'absent' }] }),
);
expect(out.map((e) => e.row['pn'])).toEqual(['B']);
});
it('两层:里层的 where 认得出外层那一行', () => {
const out = expandSource(
{ each: 'mine:sub_eval.part', then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } } },
ctxWith({
'mine:sub_eval.part': [{ pn: 'A', chip_type_id: 1 }, { pn: 'B', chip_type_id: 2 }],
'parts.type_param': [
{ type_id: 1, param_name: '输入电压范围' },
{ type_id: 1, param_name: '输出电流' },
{ type_id: 2, param_name: '压差' },
],
}),
);
expect(out).toEqual([
{ outer: { pn: 'A', chip_type_id: 1 }, row: { type_id: 1, param_name: '输入电压范围' } },
{ outer: { pn: 'A', chip_type_id: 1 }, row: { type_id: 1, param_name: '输出电流' } },
{ outer: { pn: 'B', chip_type_id: 2 }, row: { type_id: 2, param_name: '压差' } },
]);
});
it('外层的 chip_type_id 还是空的时候,里层一行都不命中 —— 「保存即铺格」靠的就是这一条', () => {
const out = expandSource(
{ each: 'mine:sub_eval.part', then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } } },
ctxWith({
'mine:sub_eval.part': [{ pn: 'A', chip_type_id: '' }],
'parts.type_param': [{ type_id: 1, param_name: '输入电压范围' }],
}),
);
expect(out).toEqual([]);
});
});