mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
* fix: interpolate multi-expression templates instead of returning None (#3208) `evaluate_expression` returned None for templates containing two or more `{{ }}` blocks with no surrounding literal text, e.g. `"{{ context.run_id }} {{ inputs.issue }}"`. The single-expression fast path used `_EXPR_PATTERN.fullmatch()`, but `fullmatch` defeats the pattern's non-greedy `(.+?)` body: for two adjacent expressions it still matches, capturing everything between the first `{{` and the last `}}` (`"context.run_id }} {{ inputs.issue"`) as the body. That garbage failed dot-path resolution and returned None directly, bypassing the `sub()` interpolation path that would have resolved each expression. Downstream this surfaced as the literal string "None" reaching commands. Guard the fast path on `stripped.count("{{") == 1` so only genuine single-expression templates take the typed return; multi-expression templates fall through to `sub()` and interpolate correctly. Add regression tests for two expressions separated by a space and for adjacent expressions with no separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(expressions): use match-span guard so single expressions with literal {{ keep their type The previous `stripped.count("{{") == 1` guard misclassified a genuine single expression whose string argument contains a literal `{{` (e.g. `{{ inputs.text | contains('{{') }}`) as multi-expression, routing it through `sub()` interpolation and coercing the typed (bool/int/list) return value to a string -- breaking the type-preservation the docstring promises (Copilot review on #3228). Anchor a single match at the start and require it to consume the whole stripped string instead. The non-greedy body stops at the first `}}`, so a two-block template fails the span check (falls through to interpolation, fixing #3208) while a lone expression -- including one with a `{{` inside a string literal -- matches to the end and keeps its typed value. Add a regression test for the literal-brace single-expression case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(expressions): detect single expression with quote-aware scan The match-span guard using the non-greedy _EXPR_PATTERN stopped at the first `}}`, so a lone expression whose string argument contains a literal `}}` (e.g. `{{ inputs.text | contains('}}') }}`) was misclassified as multi-expression and mis-parsed by the interpolation path, raising ValueError and turning CI red (Copilot review on #3228). Replace the span check with `_is_single_expression`, which scans the `{{ ... }}` body for a block-closing `}}` outside string literals (mirrors the quote handling already in `_split_top_level_commas`). A genuine two-block template closes early and falls through to interpolation (fixing #3208); a lone expression with a literal `{{` or `}}` inside a string argument keeps its typed return value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -226,6 +226,40 @@ class TestExpressions:
|
||||
result = evaluate_expression("Feature: {{ inputs.name }} done", ctx)
|
||||
assert result == "Feature: login done"
|
||||
|
||||
def test_multi_expression_no_surrounding_text(self):
|
||||
"""Two expressions with no surrounding literal text must interpolate each,
|
||||
not collapse to None via the fullmatch fast path (#3208)."""
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"issue": "23"}, run_id="47c5eb4b")
|
||||
result = evaluate_expression(
|
||||
"{{ context.run_id }} {{ inputs.issue }}", ctx
|
||||
)
|
||||
assert result == "47c5eb4b 23"
|
||||
|
||||
def test_multi_expression_adjacent_no_separator(self):
|
||||
"""Back-to-back expressions with no separator still interpolate (#3208)."""
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"a": "foo", "b": "bar"})
|
||||
result = evaluate_expression("{{ inputs.a }}{{ inputs.b }}", ctx)
|
||||
assert result == "foobar"
|
||||
|
||||
def test_single_expression_with_literal_braces_preserves_type(self):
|
||||
"""A lone expression whose string argument contains a literal ``{{`` or ``}}``
|
||||
must still take the typed fast path and return a bool, not a string
|
||||
(the fix for #3208 must not coerce it to ``\"True\"``)."""
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"text": "uses {{ jinja }} syntax"})
|
||||
assert evaluate_expression("{{ inputs.text | contains('{{') }}", ctx) is True
|
||||
|
||||
ctx = StepContext(inputs={"text": "uses }} syntax"})
|
||||
assert evaluate_expression("{{ inputs.text | contains('}}') }}", ctx) is True
|
||||
|
||||
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