Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

fix(nodes): case 节点 case["name"] 与自身校验文案矛盾——按文档省略 name 直接裸 KeyError 崩在节点构造期

Open Beginner friendly
#112 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
75/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
backend

Research direction

Start at SwitchLegacy.setup_branches in plaita/node/decide.py around line 362, where label is read unconditionally from the case's name key. Make that read optional, as the issue suggests. Add a unit test beside test_case_targets_count_as_edges in tests/unit/test_mfix_i_rules_hook.py: a case without name should construct, its first branch's name should be the case id, and the missing-id and missing-value ValueErrors should still fire. Done when Flow.model_validate accepts that IR without a KeyError.

Written by the indexing model from the issue text.

Description

优先级 P2 · 依赖:无

基线 HEAD = 65f48f2

Summary

case 节点(SwitchLegacy)的校验与取值对 name 字段自相矛盾:校验循环明确宣称 name
可选,紧接着的分支构造却无条件用 case["name"] 取值,name 缺失时抛裸 KeyError。

  1. plaita/node/decide.py:350-361 —— 校验只覆盖 id / value,且文案自述 name 可选:
        for i, case in enumerate(cases):
            # 缺 id 曾直接裸 KeyError: 'id',无任何上下文(R5 差分评审 P1-5)
            if not isinstance(case, dict) or "id" not in case:
                raise ValueError(
                    f"switch-case node {values.get('id', '?')!r}: cases[{i}] 缺少必填的 "
                    f"'id' 字段(得到 {case!r})。每个 case 需要 id/value,可选 name。"
                )
            if "value" not in case:
                raise ValueError(
                    f"switch-case node {values.get('id', '?')!r}: cases[{i}] ({case.get('id')!r}) "
                    "缺少必填的 'value' 字段。"
                )
  1. plaita/node/decide.py:362-372 —— 紧随其后无条件取 case["name"]:
        branches = [
            Branch(
                name=case["id"],
                condition=Condition(field=values.get("target"), operator=CONDITION_OP_EQ, value=case["value"]),
                priority=0,
                label=case["name"],
                next=case["id"],
                is_default=False,
            )
            for case in cases
        ]
  1. plaita/node/decide.py:149 —— 落点 label 本身可选,即缺失 name 在数据模型上是合法的:
    label: Optional[str] = None
  1. 上游 IR 静态门不拦 name。plaita/dsl/ir_validate.py:182-188 只按 id/next 建边:
    for c in node.get("cases") or []:
        if not isinstance(c, dict):
            continue
        t = c.get("id") or c.get("next")
        if isinstance(t, str):
            yield t
  1. 崩点位于节点构造期,plaita/core/flow.py:169 处 dict 节点进入 registry 解析:
                resolved.append(reg.parse_node(n))
  1. 无 name 的 case 形状被当作正向用例存在于本仓测试中,
    tests/unit/test_mfix_i_rules_hook.py:164-173:
    def test_case_targets_count_as_edges(self):
        validate_flow_ir(
            _flow([
                {"type": "start", "id": "s", "next": "c"},
                {"type": "case", "id": "c", "target": "$INPUT.k",
                 "cases": [{"id": "a", "value": 1}], "default": "e"},
                {"type": "mock", "id": "a", "next": "e"},
                {"type": "end", "id": "e"},
            ])
        )

复现(实跑,Python 3.12,plaita editable 安装):

$ /tmp/plaita-audit-venv/bin/python /tmp/pnb-repro/t2_case.py
raised: KeyError KeyError('name')

$ /tmp/plaita-audit-venv/bin/python /tmp/pnb-repro/t2b_case_e2e.py
validate_flow_ir: PASSED (no error)
Flow.model_validate raised: KeyError KeyError('name')

即:validate_flow_ir 对这份 IR 判定通过,随后 Flow.model_validate(ir) 直接抛 KeyError('name')。

影响

手写 JSON IR、MCP/LLM 生成的 IR、以及 console「JSON 侧」直接编辑节点参数的作者会踩到:
plaita-console/frontend/src/components/flow/schemaForm/coreFields.ts 把 cases 作为 case
节点的透传字段,用户在 JSON 中写 {"id": "...", "value": ...}(完全符合节点自身报错文案
「每个 case 需要 id/value,可选 name」)时,发布前静态门 validate_flow_ir 放行,
解析期 Flow.model_validate 抛裸 KeyError('name')——没有节点 id、没有 cases[i] 下标、
没有修复提示,作者只能看到 pydantic mode="before" validator 里逃逸出来的裸异常。

代价:错误信息不可定位(对比同函数对缺 id/缺 value 都给了带节点 id 与下标的
ValueError),排查成本被推给作者;且失败点从「静态门」后移到「解析/加载期」,
任何按「过门即安全」组织的发布或加载流程都会在门后崩溃。

不修的后果:同一个节点对同一字段给出互相矛盾的两套语义(文档与报错说可选、代码要求必填),
缺 name 的 IR 永远无法加载,且只能得到无上下文的 KeyError。

建议

修复方向(plaita/node/decide.py 的 SwitchLegacy.setup_branches):把 label=case["name"]
改为可选读取,与 Branch.label 的可选语义及自身校验文案对齐:

  • 最小修法(一行):label=case["name"] → label=case.get("name");此时 case 缺 name
    时 label 为 None,与 decide.py:149 的声明一致。若希望保留展示名,可退化为
    label=case.get("name") or case["id"],但需同步确认该处语义是否影响下游展示/路由。

验收条件:

  • 新增(或改造 test_case_targets_count_as_edges 同形状的)单测,
    断言 SwitchLegacy(id="c", target="$INPUT.k", cases=[{"id": "a", "value": 1}], default="e")
    可正常构造,且构造出的 branches[0].name == "a"。
  • 端到端断言:对上述第 6 条所示的 IR,validate_flow_ir(ir) 与 Flow.model_validate(ir)
    均不再抛异常;/tmp/pnb-repro/t2b_case_e2e.py 应输出 Flow.model_validate OK
    而不是 raised: KeyError KeyError('name')。
  • 回归:缺 id、缺 value 两条路径仍抛带节点 id 与下标的 ValueError(现有文案不得回退为裸异常)。

边界

  • 只修 SwitchLegacy.setup_branches 的 name 取值与「可选 name」语义矛盾,不顺手改动
    id/value 两条已有校验分支的文案,也不改 plaita/dsl/ir_validate.py 的建边逻辑。
  • 不改变「case 的跳转目标恒为 id」这一既有约定,next 归一化仍留在 plaita/dsl/builder.py
    的 case() 中(该函数不补 name,是暴露路径之一,但本次不要求它代填)。
  • 与其他路径的掩盖关系需说明,避免误判影响面:plaita/dsl/sexpr.py:584 的 _c_match
    总会生成 name,docs-site/docs/guide/yaml-and-dsl.md:176-179 的官方示例每条 case 都写了
    name,因此主流前端路径会掩盖该缺陷;本单只针对手写 JSON / LLM 生成 / console JSON 侧
    这条暴露路径。
  • 查重:gh issue list -R jeffkit/plaita --state all --limit 100 共 33 条(含 closed),
    grep case/switch/SwitchLegacy 无同根条目(#16 是 map 子流程与 if 作用域的内核限制,
    #31 虽同为 plaita/node/ 下 HTTP 节点但缺陷面无关),本单非重复。
Dominant language
Python
Stars
0
Forks
1
Avg merge
3h 53m
Merged PRs (30d)
3

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from jeffkit/plaita

All issues in jeffkit/plaita

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.