mirror of
https://github.com/github/spec-kit.git
synced 2026-07-07 22:56:57 +08:00
fix(workflows): quote-aware interpolation so a literal }} in a filter arg doesn't break multi-expression templates (#3307)
* fix(workflows): quote-aware interpolation so a literal }} in a filter arg doesn't break multi-expression templates
#3208/#3228 hardened the single-expression fast path (_is_single_expression)
so a literal {{ or }} inside a string argument like `| default('}}')` stays on
the typed path. the multi-expression interpolation path was left on the old
_EXPR_PATTERN regex, whose non-greedy `(.+?)}}` body stops at the first }}
regardless of quoting. so a multi-expression template with a literal }} in any
block captured a truncated body, hit the filter parser malformed, and raised
ValueError.
e.g. `{{ inputs.name }}: {{ inputs.missing | default('}}') }}` raised instead
of interpolating.
replace _EXPR_PATTERN.sub with _interpolate_expressions, which scans each block
for a }} outside string literals - the same quote handling _is_single_expression
already uses. plain-value passthrough (a literal }} in a resolved value, not an
expression) is unchanged.
add regression tests for a literal }} in the second block and in the first
block, plus a literal {{ guard.
* fix(workflows): surface malformed templates in interpolation instead of emitting verbatim
address copilot review on #3307: when the quote-aware scan finds no block-closing
`}}` (e.g. an unbalanced quote in a filter arg swallowed the delimiter), fall back
to the first raw `}}` in the tail and evaluate it, so the parser raises ValueError
just as the old _EXPR_PATTERN.sub path did. only when there is no `}}` at all is
the tail left verbatim (a genuinely unterminated `{{`, which the regex also could
not match). keeps a typo failing loudly rather than being silently hidden.
add a regression test for an unbalanced quote in a multi-expression template.
This commit is contained in:
@@ -260,6 +260,60 @@ class TestExpressions:
|
||||
ctx = StepContext(inputs={"text": "uses }} syntax"})
|
||||
assert evaluate_expression("{{ inputs.text | contains('}}') }}", ctx) is True
|
||||
|
||||
def test_multi_expression_with_literal_close_brace_in_argument(self):
|
||||
"""A multi-expression template with a literal ``}}`` inside a string
|
||||
argument must interpolate, not raise. #3208/#3228 hardened the single-
|
||||
expression fast path for literal braces but left the interpolation path
|
||||
on ``_EXPR_PATTERN``, whose non-greedy body stops at the first ``}}`` --
|
||||
so the block was captured truncated and the filter parser raised
|
||||
ValueError."""
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"name": "Bob", "missing": None})
|
||||
# ``}}`` in the default fallback of the second block.
|
||||
result = evaluate_expression(
|
||||
"{{ inputs.name }}: {{ inputs.missing | default('}}') }}", ctx
|
||||
)
|
||||
assert result == "Bob: }}"
|
||||
# ``}}`` in the first block, expression following it.
|
||||
result = evaluate_expression(
|
||||
"{{ inputs.missing | default('}}') }} / {{ inputs.name }}", ctx
|
||||
)
|
||||
assert result == "}} / Bob"
|
||||
|
||||
def test_multi_expression_with_literal_open_brace_in_argument(self):
|
||||
"""A literal ``{{`` inside a string argument in a multi-expression
|
||||
template must not confuse block detection either."""
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"name": "Bob", "missing": None})
|
||||
result = evaluate_expression(
|
||||
"{{ inputs.name }} {{ inputs.missing | default('{{') }}", ctx
|
||||
)
|
||||
assert result == "Bob {{"
|
||||
|
||||
def test_multi_expression_unbalanced_quote_still_raises(self):
|
||||
"""A malformed block (an unbalanced quote in a filter arg) must still
|
||||
surface a ValueError, not be silently emitted verbatim.
|
||||
|
||||
The quote-aware scan never finds a block-closing ``}}`` when a quote is
|
||||
left open, but a raw ``}}`` is still present in the tail. It must fall
|
||||
back to that raw delimiter and evaluate — same as the old regex path —
|
||||
so a typo fails loudly instead of being hidden (Copilot review on
|
||||
#3307)."""
|
||||
import pytest
|
||||
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"name": "Bob", "missing": None})
|
||||
with pytest.raises(ValueError):
|
||||
evaluate_expression(
|
||||
"{{ inputs.name }} {{ inputs.missing | default('oops }}", ctx
|
||||
)
|
||||
|
||||
def test_comparison_equals(self):
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
Reference in New Issue
Block a user