Conversation
The evaluator matched word operators by their surrounding spaces
(" or ", " and ", " in ", " not in ", a leading "not "), so an operator
next to a newline or tab was never split on. A condition wrapped after
its operator in YAML keeps that line break -- a `|` block keeps every
one, and `>` keeps the break before a more-indented continuation line --
so `{{ inputs.skip_review or\n inputs.scope == 'docs' }}` was resolved
as a single dot path, came back None, and an `if` step silently took its
`else` branch. Workflow validation accepted it.
Collapse each run of whitespace outside quoted strings to one space
before parsing, as Jinja2 treats any whitespace between tokens alike.
Quoted operands keep their text exactly.
The condition gate had pinned the old behaviour: it withheld the "wrap
the expression" correction for `inputs.x == 1\nand inputs.name == 'abc'`
because the wrapped form read False. Wrapping now repairs it, so the
case moves from MULTI_POSITION_UNFIXABLE to a test that the correction
is offered and evaluates True.
Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The focused implementation preserves quoted content and has comprehensive regression coverage.
Review effort: Balanced
Findings: None
What changed in this PR
Normalizes unquoted whitespace so multiline workflow expressions parse consistently.
Changes:
- Collapse whitespace outside quoted strings.
- Add operator, YAML, quote-preservation, and remediation regressions.
- Static review only; tests were not independently rerun.
| File | Description |
|---|---|
src/specify_cli/workflows/expressions.py |
Adds quote-aware whitespace normalization. |
tests/test_workflows.py |
Covers multiline operators and quoted whitespace. |
tests/unit/test_condition_expression_block.py |
Verifies multiline-condition remediation. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
If a workflow condition wraps onto a second line after
or,and,in,not inornot, it evaluates to false, with no error.The evaluator finds word operators by their surrounding spaces (
" or "," and "," in "," not in ",expr.startswith("not ")). An operator next to a newline or tab is never split on. The whole expression is then looked up as one dot path and resolves toNone.YAML keeps that line break in the usual way of wrapping a long condition. A
|block keeps every break, and>keeps the break before a more-indented continuation line:With
skip_review: falseandscope: docsthe condition is true, butmainruns theelsebranch.specify workflow runvalidates the file without a warning:The same thing happens to
while/do-whileconditions and to any other{{ }}expression wrapped this way.Fix:
_evaluate_simple_expressionnow collapses each run of whitespace outside a quoted string to a single space before parsing, as Jinja2 treats any whitespace between tokens alike.inputs.title == 'two spaces'still compares against two spaces.The condition gate had pinned the old behaviour. For the bare condition
inputs.x == 1\nand inputs.name == 'abc', it withheld the "Wrap the expression" correction, because the wrapped form read False (see the comment inMULTI_POSITION_UNFIXABLE). Wrapping now repairs it, so that case moves to a new test. The test checks that the correction is offered and that the wrapped form evaluates True.This is a sibling of #4417 (a parenthesised group read as a dot path and resolving to
None). The failure mode is the same; the trigger is different.Testing
uv run specify --helpuv sync && uv run pytestDetails:
tests/test_workflows.py::TestExpressions:test_operators_separated_by_any_whitespace: 9 cases coveringor/andat the end of a line, a|-style break beforeor, a tab,not,in,not insplit both ways, and CRLF inside a group.test_condition_wrapped_across_lines_in_yaml: the folded scalar above, throughyaml.safe_loadandevaluate_condition.test_whitespace_inside_quoted_operand_is_kept: a guard for quoted text.tests/unit/test_condition_expression_block.py:test_a_condition_wrapped_across_lines_gets_a_paste_ready_correction(If/While/DoWhile).main, green here: withexpressions.pyreverted, the new cases fail (10 intest_workflows.pyand 3 in the gate test). The quoted-text guard passes on both, as intended.*_python_paritytemplate tests (test_resolve_template_python_parity.py,test_check_prerequisites_python_parity.py, and others). They pass when rerun on their own, both on this branch and onmain, and they don't go through the workflow evaluator.tests/test_workflows.pyandtests/unit/test_condition_expression_block.pypass: 1074 tests.uvx ruff@0.15.0 check src tests(the CI pin): all checks passed.AI Disclosure
AI disclosure: Opened on behalf of @huiq777 with Claude Code (model: Claude Opus 5.5, default settings, autonomous). Claude Code found the bug while probing the expression evaluator for siblings of #4416, #4417 and #4572. It wrote the fix, the tests and this description, and ran the reproduction and the test suite shown above. The commit carries an
Assisted-by:trailer.🤖 Generated with Claude Code