Skip to content

fix(scripts): stop wrap composition looping on a token in core content - #4396

Open
Yash-Chindam wants to merge 1 commit into
github:mainfrom
Yash-Chindam:fix/4385-wrap-infinite-loop
Open

fix(scripts): stop wrap composition looping on a token in core content#4396
Yash-Chindam wants to merge 1 commit into
github:mainfrom
Yash-Chindam:fix/4385-wrap-infinite-loop

Conversation

@Yash-Chindam

Copy link
Copy Markdown
Contributor

Fixes #4385

Problem

scripts/bash/common.sh rewrote layer_content in place and then re-tested the string it had just modified:

while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do
    local before="${layer_content%%\{CORE_TEMPLATE\}*}"
    local after="${layer_content#*\{CORE_TEMPLATE\}}"
    layer_content="${before}${content}${after}"
done

When the resolved core content holds a literal {CORE_TEMPLATE}, every pass reinserts it and the condition is true forever.

Why this shape of fix

The PowerShell and Python ports were already correct, so the intended semantics did not need inventing — only matching:

# scripts/powershell/common.ps1:789
$content = $layerContent.Replace('{CORE_TEMPLATE}', $content)
# scripts/python/common.py:466
content = layer_content.replace(placeholder, content)

String.Replace and str.replace scan the original subject once and never revisit inserted text. The new loop consumes the wrapper left to right into an accumulator, so work is bounded by the placeholders in the original wrapper and inserted content is never re-examined. This aligns bash with the other two ports rather than introducing a third behaviour.

tests/parity_helpers.py already treats .replace() as the oracle for expected wrap output (install_composition_stack), so the three ports now agree with the harness by construction.

Verification

I extracted the wrap block verbatim from upstream/main and from this branch and ran both under Git Bash:

case wrapper core content before after
A — normal BEFORE\n{CORE_TEMPLATE}\nAFTER BASE BEFORE\nBASE\nAFTER identical
B — this bug BEFORE\n{CORE_TEMPLATE}\nAFTER BASE {CORE_TEMPLATE} hangs (rc=124) BEFORE\nBASE {CORE_TEMPLATE}\nAFTER
C — two tokens A{CORE_TEMPLATE}B{CORE_TEMPLATE}C X AXBXC identical

A and C confirm no behaviour change on the paths that already worked, including multiple placeholders in the wrapper — the fix is not "replace the first occurrence". B matches str.replace byte for byte.

Test

test_all_variants_treat_core_token_in_core_content_as_literal runs all three variants through the existing parity harness with core content carrying a literal token.

The regression mode here is a hang, not a wrong value, so a plain assertion would never fail — it would just never finish. run() therefore grows an optional timeout parameter (default None, so every existing call is unchanged) and the new test passes timeout=30. Without it, a reintroduced bug would stall the suite instead of failing it.

Notes on local test runs

The bash parity tests skip on my machine: Windows resolves bash to the WSL launcher, which tests/conftest.py::_has_working_bash correctly rejects. I verified the bash behaviour by driving Git Bash directly, as in the table above; CI will exercise the new test through the normal harness.

pytest tests/ shows 20 pre-existing failures locally, all WinError 1314: A required privilege is not held by the client from symlink creation. I confirmed the identical set fails on unmodified upstream/main, so they are environmental and unrelated.


Disclosure: this change was developed with AI assistance (Claude). The AI helped locate the defect, compare the three ports, draft the fix and test, and write this description. All reasoning and results above were verified by running the code; I reviewed the change before submitting.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The bounded replacement correctly matches existing cross-runtime semantics and has targeted regression coverage.

Pull request overview

Fixes Bash wrap composition hangs when inserted core content contains {CORE_TEMPLATE}, aligning behavior with Python and PowerShell.

Changes:

  • Performs single-pass Bash wrapper substitution.
  • Adds timeout-enabled regression coverage across script variants.
File summaries
File Description
scripts/bash/common.sh Prevents rescanning inserted content.
tests/parity_helpers.py Adds optional subprocess timeout.
tests/test_resolve_template_python_parity.py Tests literal token preservation and termination.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

The bash wrap strategy rewrote layer_content in place and then re-tested
the string it had just modified. When the resolved core content held a
literal {CORE_TEMPLATE}, every pass reintroduced the token and the loop
never terminated.

Consume the wrapper left to right instead, appending each segment and the
core content to an accumulator. Work is bounded by the placeholders in the
original wrapper and inserted content is never re-examined, matching the
single-pass semantics the PowerShell (.Replace) and Python (.replace)
ports already have -- so this aligns bash with the other two rather than
introducing new behaviour.

The regression mode is a hang rather than a wrong value, so the new parity
test passes a timeout; run() grows an optional timeout parameter for that.
Without it a reintroduced bug would stall the suite instead of failing it.

Fixes github#4385
@Yash-Chindam
Yash-Chindam force-pushed the fix/4385-wrap-infinite-loop branch from 314cc5e to 86fd717 Compare September 1, 2026 23:08
@mnriem
mnriem requested a balanced review from Copilot September 2, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The bounded replacement preserves established semantics and is covered by cross-runtime regression testing.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: wrap template composition can loop forever when core content contains {CORE_TEMPLATE}

3 participants