fix(workflows): reject a switch expression that is never evaluated - #4295
fix(workflows): reject a switch expression that is never evaluated#4295ntdatt812 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The validator incorrectly rejects valid non-boolean string literals used as switch values.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds switch-expression validation to catch unevaluated or malformed expression blocks.
Changes:
- Reuses existing condition-expression predicates for switch validation.
- Adds regression and boundary tests.
File summaries
| File | Description |
|---|---|
src/specify_cli/workflows/steps/switch/__init__.py |
Validates switch expressions. |
tests/test_workflows.py |
Covers invalid and accepted expression forms. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # Only these two checks apply. A switch matches on strings, so a composite key | ||
| # such as `{{ inputs.a }}-{{ inputs.b }}` is legitimate here even though the | ||
| # same shape would be a fault in a boolean condition. | ||
| elif condition_is_never_evaluated(config["expression"]): |
There was a problem hiding this comment.
Right: reusing the condition predicate was wrong for a switch. A condition is coerced by bool(), so any braceless text is always true there. A switch matches its value against case keys, and a case key is a literal, so expression: review is a valid (if constant) switch and whitespace strips to the "" key.
Fixed in 2554a12. switch_expression_is_never_evaluated flags braceless text only when it opens by walking into a root _build_namespace supplies (inputs.mode, item[0], context.run_id, ...), and keeps the verbatim-unclosable {{ case.
test_a_literal_expression_stays_acceptedrunsreview, whitespace,approve meand a bareinputsthroughexecute()first, asserts the case each one actually dispatches, then assertsvalidate()accepts it.inputsX.modeis accepted too: a name that merely starts like a root is not a reference into it.test_every_namespace_root_written_without_a_block_is_rejectedcovers every root, plus a filter, a comparison and surrounding whitespace.
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
`SwitchStep.validate` checked only that `expression` is present. It goes through
the same `evaluate_expression` as a condition, so one written without braces
comes back as its own source text:
expression: inputs.mode -> expression_value: "inputs.mode"
matched_case: "__default__"
status: COMPLETED
It matches no case key, falls through to `default` on every run — or dispatches
nothing at all when there is no default — and still reports COMPLETED. That is
the "silent empty result + COMPLETED" wiring bug this file's own `cases:` guard
was written to prevent, on the field one line above it.
`if`, `while` and `do-while` already run these two predicates on their
`condition`. This reuses them rather than writing a third scan.
Only those two apply. A switch matches on strings, so a composite key such as
`{{ inputs.a }}-{{ inputs.b }}` is legitimate here even though the same shape
would be a fault in a boolean condition — there is a test pinning that, and a
literal `true` and the empty string stay accepted as ordinary case keys for the
same reason.
…switch
The validator reused condition_is_never_evaluated, which flags every
braceless string because a condition is coerced by bool(). A switch is
not: it matches its resolved value against case keys, and a case key is
a literal. `expression: review` dispatches the `review:` case and
whitespace strips to the "" key, so both were being rejected while the
step runs them correctly.
switch_expression_is_never_evaluated flags braceless text only when it
opens by walking into a root _build_namespace supplies (inputs.mode,
item[0], context.run_id), and keeps the verbatim-unclosable {{ case.
Tests pin both directions, with the literal cases checked against what
execute() actually dispatches.
37ea908 to
2554a12
Compare
|
@mnriem Copilot's point is addressed in 2554a12 (details in the inline reply), and the branch is rebased onto current Evidence:
AI disclosure, per CONTRIBUTING: this change, its tests and this comment were written with Claude Code as a coding agent; the runs above were executed locally. |
The defect
SwitchStep.validatechecks only thatexpressionis present (steps/switch/__init__.py:105). The field goes through the sameevaluate_expressionas a condition, so one written without braces comes back as its own source text.Measured on
main(27f50f7),inputs.mode = "review", casesreview/build, plus adefault:It matches no case key, falls through to
defaulton every run — or, with nodefault:, dispatches nothing at all — and still reports COMPLETED. That is exactly the "silent empty result + COMPLETED" wiring bug this file's owncases:guard was written to prevent, quoting its comment, on the field one line above it.if,whileanddo-whileall runcondition_is_never_evaluatedandcondition_has_malformed_expression_blockon theircondition.switchran neither on itsexpression.The fix
Reuse those two predicates rather than write a third scan, with switch-appropriate wording.
Only those two apply, deliberately. A switch matches on strings, so a composite key is legitimate here even though the same shape is a fault in a boolean condition:
inputs.mode{{ inputs.x{{ inputs.missing | default('oops }}{{ inputs.mode }}{{ inputs.a }}-{{ inputs.b }}true,""The last two rows are the condition-specific exemptions
condition_is_never_evaluatedalready carries. They are harmless on a condition and actively correct here, which is what makes these two predicates safe to reuse on a non-boolean field. There is a test pinning that boundary so a later narrowing cannot quietly reject composite keys.This is independent of #4292, deliberately: that PR adds a predicate for a condition holding more than one block, which is precisely the shape a switch is allowed to have. It is not applied here.
Verification
Windows, Python 3.11.
Identical 22 failures on both sides,
diffclean — the pre-existing symlink and bash-parity classes on unelevated Windows. 1728 → 1731 is exactly the three cases added.Mutation-checked, after confirming the edit applied: disabling the never-evaluated branch fails 2 of the 3 new cases, and the composite-key case stays green under it — which is the point, since it does not depend on the branch and still guards the other side.
Tests
tests/test_workflows.py::TestSwitchStep— three cases: the braceless form (asserting the current runtime behaviour first, then the validator), both unclosable shapes, and the composite-key/literal boundary that must stay accepted.AI disclosure
Per CONTRIBUTING: this pull request (code, tests and description) was developed with Claude Code as a coding agent.