Archived
fix(activity-registry): 配方静态审补 card/say 的表达式扫描(评审 Important)
card.title / card.ask / card.standard / card.basis[].v 与 say.text 全是 AutoExpr, 合法地可以含 lookup —— 原实现只核了 gateLedgers,这几处从没被 walkExpr 走过, 卡面上的一处笔误因此能一声不响地装进来,只在这一步第一次真跑起来时才现形。 补齐四处 walkExpr 调用 + 新增 say 分支;顺带核实 walkExpr 对 when/then/else 这类 嵌套结构确实递归到底(title 的测试用例特意嵌了一层 when/then 验证)。 同批把名不副实的「each / lookup 的地址同样要在声明里」拆开:该用例改名只测 each, 另加一条真的在 project.fields 里放 lookup 节点的用例(评审 Minor)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -435,13 +435,70 @@ describe('配方静态审:写错地址 = 每一单都错,装的时候就拒', ()
|
|||||||
expect(findings.some((f) => f.severity === 'error' && f.message.includes('不存在的列'))).toBe(true);
|
expect(findings.some((f) => f.severity === 'error' && f.message.includes('不存在的列'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('each / lookup 的地址同样要在声明里', () => {
|
it('each 的地址同样要在声明里', () => {
|
||||||
const findings = autoRecipeFindings(
|
const findings = autoRecipeFindings(
|
||||||
withRecipe({ do: [{ kind: 'project', into: 'demo.record', from: { each: 'mine:nope.nope' }, fields: {} }] }),
|
withRecipe({ do: [{ kind: 'project', into: 'demo.record', from: { each: 'mine:nope.nope' }, fields: {} }] }),
|
||||||
);
|
);
|
||||||
expect(findings.some((f) => f.message.includes('nope.nope'))).toBe(true);
|
expect(findings.some((f) => f.message.includes('nope.nope'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 2026-08-29 评审(Minor):上面那条「each / lookup 的地址同样要在声明里」的名字曾经名不副实 ——
|
||||||
|
* 用例体只测了 `from.each`,从没有一条真的在 `project.fields` 里放过 `{ lookup, by, take }` 节点。
|
||||||
|
* `walkExpr` 的 lookup 递归分支因此只被间接触到,card/say 那个洞就是这么躲过测试套件的(见下方)。
|
||||||
|
*/
|
||||||
|
it('project.fields 里的 lookup 地址同样要在声明里', () => {
|
||||||
|
const findings = autoRecipeFindings(
|
||||||
|
withRecipe({
|
||||||
|
do: [
|
||||||
|
{
|
||||||
|
kind: 'project',
|
||||||
|
into: 'demo.record',
|
||||||
|
from: { once: true },
|
||||||
|
fields: { note: { lookup: 'nope.nope', by: {}, take: 'x' } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(findings.some((f) => f.severity === 'error' && f.message.includes('nope.nope'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 2026-08-29 评审(Important):`card.title` / `card.ask` / `card.standard` / `card.basis[].v`
|
||||||
|
* 全是 AutoExpr,合法地可以含 lookup —— 从前只核了 `gateLedgers`,这四处没被 `walkExpr` 走过,
|
||||||
|
* 卡面上的一处笔误因此能一声不响地装进来。`title` 这里刻意嵌一层 `when/then` ——
|
||||||
|
* 顺手核实 `walkExpr` 对嵌套结构是不是真的递归到底(不是只查表层)。
|
||||||
|
*/
|
||||||
|
it('card 的 title / ask / standard / basis 里藏的 lookup 地址也要核(含嵌套在 when/then 里的)', () => {
|
||||||
|
const findings = autoRecipeFindings(
|
||||||
|
withRecipe({
|
||||||
|
do: [
|
||||||
|
{
|
||||||
|
kind: 'card',
|
||||||
|
as: 'execution',
|
||||||
|
title: { when: 'a = 1', then: { lookup: 'title.nope', by: {}, take: 'x' }, else: 'x' },
|
||||||
|
ask: { lookup: 'ask.nope', by: {}, take: 'x' },
|
||||||
|
standard: { lookup: 'standard.nope', by: {}, take: 'x' },
|
||||||
|
basis: [{ k: 'b', v: { lookup: 'basis.nope', by: {}, take: 'x' } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const messages = findings.map((f) => f.message).join('\n');
|
||||||
|
expect(messages).toContain('title.nope');
|
||||||
|
expect(messages).toContain('ask.nope');
|
||||||
|
expect(messages).toContain('standard.nope');
|
||||||
|
expect(messages).toContain('basis.nope');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 2026-08-29 评审(Important):say.text 同样是 AutoExpr —— 从前压根没有 say 分支 */
|
||||||
|
it('say.text 里藏的 lookup 地址也要核', () => {
|
||||||
|
const findings = autoRecipeFindings(
|
||||||
|
withRecipe({ do: [{ kind: 'say', text: { lookup: 'say.nope', by: {}, take: 'x' } }] }),
|
||||||
|
);
|
||||||
|
expect(findings.some((f) => f.severity === 'error' && f.message.includes('say.nope'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('in: 与 result: 不是台账地址,不该被当成台账来审', () => {
|
it('in: 与 result: 不是台账地址,不该被当成台账来审', () => {
|
||||||
const findings = autoRecipeFindings(
|
const findings = autoRecipeFindings(
|
||||||
withRecipe({ do: [{ kind: 'project', into: 'demo.record', from: { each: 'in:pns' }, fields: {} }] }),
|
withRecipe({ do: [{ kind: 'project', into: 'demo.record', from: { each: 'in:pns' }, fields: {} }] }),
|
||||||
|
|||||||
@@ -462,6 +462,19 @@ export function autoRecipeFindings(manifest: ActivityManifest): StaticAuditFindi
|
|||||||
if (action.as === 'decide' && (action.options ?? []).length === 0) {
|
if (action.as === 'decide' && (action.options ?? []).length === 0) {
|
||||||
out.push({ code: 'schema-invalid', severity: 'error', message: '决策卡一个选项都没有', at: where });
|
out.push({ code: 'schema-invalid', severity: 'error', message: '决策卡一个选项都没有', at: where });
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* **`title`/`ask`/`standard`/`basis[].v` 都是 AutoExpr,合法地可以含 lookup**
|
||||||
|
* (2026-08-29 评审补,原实现只核了 `gateLedgers`,这四处从没被 `walkExpr` 走过 ——
|
||||||
|
* 卡面上的一处笔误因此能一声不响地装进来,只在这一步第一次真跑起来时才现形)。
|
||||||
|
*/
|
||||||
|
walkExpr(action.title, where);
|
||||||
|
if (action.ask !== undefined) walkExpr(action.ask, where);
|
||||||
|
if (action.standard !== undefined) walkExpr(action.standard, where);
|
||||||
|
for (const b of action.basis ?? []) walkExpr(b.v, where);
|
||||||
|
}
|
||||||
|
/* `say.text` 同样是 AutoExpr,同一个理由要核(2026-08-29 评审补) */
|
||||||
|
if (action.kind === 'say') {
|
||||||
|
walkExpr(action.text, where);
|
||||||
}
|
}
|
||||||
if (action.kind === 'submit' && (step.acceptanceChecklist ?? []).length === 0) {
|
if (action.kind === 'submit' && (step.acceptanceChecklist ?? []).length === 0) {
|
||||||
out.push({
|
out.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user