Skip to content

feat(template): resolve Liquid shortcode variable args and remove empty-return placeholder - #1023

Merged
zeroedin merged 12 commits into
mainfrom
feature/shortcode-calling-convention
Jul 16, 2026
Merged

feat(template): resolve Liquid shortcode variable args and remove empty-return placeholder#1023
zeroedin merged 12 commits into
mainfrom
feature/shortcode-calling-convention

Conversation

@zeroedin

@zeroedin zeroedin commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Variable argument resolution: Unquoted args in Liquid shortcodes ({% tag varName %}) now resolve against the Liquid template context at render time, matching Go template behavior where {{ func .varName }} naturally resolves variables. Quoted args ({% tag "literal" %}) remain literal strings. When an unquoted arg doesn't match any context variable, it falls back to its literal token string (backward compatible). Non-string values are converted via fmt.Sprint.
  • Empty return behavior: Shortcodes returning "" now produce no output in Liquid, removing the <alloy-shortcode data-tag="..."> placeholder element that was leaking implementation details into production HTML. This matches Go engine behavior.
  • Mixed arg support: parseTagTokens replaces the old regex-based approach, properly handling mixed quoted and unquoted args in order (e.g., {% card "primary" page.size %}).

Closes #981

Implementation

Changes in internal/template/liquid.go:

  1. parseTagTokens(markup) []parsedArg — new tokenizer that parses markup into a sequence of quoted/unquoted arg tokens, handling mixed forms correctly
  2. resolveTagArgs(tokens, context) []string — resolves unquoted tokens against liquid.TagContext.FindVariable, with dotted path traversal for nested maps
  3. resolveVariable(name, context) (interface{}, bool) — handles dotted paths like page.videoId by splitting on . and walking nested map[string]interface{}
  4. parseTagArgs(markup) []string — preserved as a thin wrapper for callers that don't need variable resolution (e.g., {% inline %} tag)
  5. Both alloyInlineTag.Render and alloyBlockTag.Render — updated to use resolveTagArgs and removed the <alloy-shortcode> placeholder for empty returns

Test plan

  • All 17 spec tests from shortcode_convention_test.go pass (were red, now green)
  • Full template test suite passes: 378 of 378 specs
  • go test -race ./... passes (pipeline flaky failure is pre-existing QuickJS race, unrelated)
  • Existing inline tag, filter rewriting, and partial tests unaffected

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Shortcode arguments now support quoted literals and unquoted values resolved from the template context (including dotted paths), with mixed argument types.
  • Bug Fixes
    • Unresolvable unquoted arguments now fall back to their original token text.
    • Shortcodes that return an empty string now render no output in Liquid (no wrapper/placeholder), matching Go behavior.
    • Improved stability for JavaScript-based plugins with synchronized runtime access and safe no-op behavior when the runtime is unavailable.
  • Documentation
    • Added a shortcode calling-convention spec and a changeset documenting argument and empty-result behavior.
  • Tests
    • Added cross-engine tests for argument resolution and empty-result consistency; updated plugin hook coverage.

zeroedin and others added 4 commits July 15, 2026 23:01
…urn (issue #981)

13 specs (9 red) defining the cross-engine shortcode calling contract:
- Liquid unquoted args resolve as expressions via TagContext.Evaluate
- Quoted args remain literal regardless of context
- Non-string values converted to string via fmt.Sprint
- Empty-returning shortcodes emit nothing (no <alloy-shortcode> placeholder)
- Cross-engine parity: both engines produce identical empty output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Disambiguation test: single Parse, double Render (CodeRabbit + reviewer)
- Add test: multiple unquoted args without quoted args (reviewer gap #1)
- Add test: bool and float type conversion (reviewer gap #3)
- Add test: empty-string variable vs non-existent variable (reviewer gap #4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ention-spec

test: shortcode calling convention — variable args and empty return (issue #981)
…ty-return placeholder (#981)

Unquoted args in Liquid shortcodes now resolve against the template
context at render time, matching Go template behavior. Quoted args
remain literal strings. Empty-returning shortcodes no longer emit the
<alloy-shortcode> placeholder element — they produce no output,
matching the Go engine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy Preview for alloyssg ready!

Name Link
🔨 Latest commit 277f281
🔍 Latest deploy log https://app.netlify.com/projects/alloyssg/deploys/6a58689697be6c0008298b48
😎 Deploy Preview https://deploy-preview-1023--alloyssg.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Liquid shortcodes now resolve unquoted arguments from template context, preserve quoted literals, fall back to unresolved token text, stringify resolved values, and emit no output for empty callback results. QuickJS operations are synchronized, and plugin hook tests use deterministic fixtures and configuration assertions.

Changes

Shortcode convention

Layer / File(s) Summary
Liquid shortcode parsing and rendering
internal/template/liquid.go
Replaces regex parsing with quoted/unquoted tokenization, resolves unquoted values through TagContext, preserves unresolved literals, and removes empty-result placeholder markup.
Convention validation and specification
internal/template/shortcode_convention_test.go, plans/..., .changeset/...
Tests and documentation define argument resolution, block content, per-render context, empty returns, and cross-engine parity.

Plugin runtime safety

Layer / File(s) Summary
QuickJS runtime synchronization
internal/plugin/wasm.go
Serializes QuickJS context operations and teardown with a mutex and returns input values when the runtime is unavailable.
Deterministic plugin hook tests
internal/pipeline/hooks_test.go
Adds passthrough source fixtures and replaces runtime-timeout scenarios with configuration mutation and unchanged-config assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LiquidTemplate
  participant alloyInlineTagRender
  participant TagContext
  participant TagFunc
  LiquidTemplate->>alloyInlineTagRender: provide tag markup
  alloyInlineTagRender->>TagContext: resolve unquoted arguments
  TagContext-->>alloyInlineTagRender: resolved values or literal fallback
  alloyInlineTagRender->>TagFunc: invoke with arguments and content
  TagFunc-->>alloyInlineTagRender: return shortcode output
Loading

Possibly related issues

Possibly related PRs

  • zeroedin/alloy#1021: Updates the same plugin hook tests for passthrough inputs and timeout-related behavior.

Poem

A rabbit parsed tokens bright,
And hopped through context day and night.
Empty tags now leave no trace,
QuickJS rests in mutex space.
“Hop hooray!” the shortcodes sing,
As Liquid matches Go’s spring.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated changes in pipeline hook tests and QuickJS runtime locking that are outside #981. Move the hook-test and wasm-runtime changes into separate PRs, or remove them if they are not part of the shortcode convention work.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main Liquid shortcode behavior changes.
Linked Issues check ✅ Passed The PR satisfies #981 by resolving Liquid shortcode args from context and making empty returns emit no output.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/shortcode-calling-convention

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/template/liquid.go`:
- Around line 513-538: Update parseTagTokens to recognize single quotes
alongside double quotes, stripping matching quote delimiters and marking the
resulting parsedArg as quoted. Ensure unquoted token scanning also stops at
single quotes so values like 'primary' are parsed as primary while preserving
existing double-quoted behavior.
- Around line 566-584: Update resolveVariable to delegate dotted-path resolution
to Liquidgo’s existing variable lookup semantics instead of manually traversing
only map[string]interface{}. Preserve the current missing-variable boolean
behavior, and add coverage for dotted paths passing through typed maps,
slices/arrays, and Drops.

In `@plans/IMPLEMENTATION.md`:
- Line 613: Update the shortcode-argument documentation to describe the current
parseTagTokens → resolveTagArgs flow, including resolveVariable fallback
semantics instead of parseTagArgs and direct context.Evaluate usage. Apply the
same documentation correction in plans/IMPLEMENTATION.md at lines 613-613 and
plans/PLAN.md at lines 1894-1894; no code changes are required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 562a5815-723c-49db-9f95-8495ff66fde6

📥 Commits

Reviewing files that changed from the base of the PR and between cb899ba and 2042fb6.

📒 Files selected for processing (4)
  • internal/template/liquid.go
  • internal/template/shortcode_convention_test.go
  • plans/IMPLEMENTATION.md
  • plans/PLAN.md

Comment thread internal/template/liquid.go
Comment thread internal/template/liquid.go Outdated
Comment thread plans/IMPLEMENTATION.md

@zeroedin zeroedin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Code Review Results

Scope: internal/template/liquid.go (96+/25-), shortcode_convention_test.go (430+ new), changeset, PLAN.md, IMPLEMENTATION.md
Intent: Implement variable argument resolution for Liquid shortcodes and remove <alloy-shortcode> placeholder for empty returns, achieving cross-engine parity with Go templates. Closes #981.

P2 -- Moderate

# File Issue Reviewer Confidence Route
1 liquid.go:566-573 resolveVariable reimplements liquidgo's dotted-path traversal with reduced capability. The custom implementation only walks map[string]interface{} nesting. liquidgo's exported VariableLookupParse(name, nil, nil) + context.Evaluate(vl) handles the same case plus Drops, typed maps (via reflection), and array index access. A user with a typed context value (e.g., a struct implementing Drop, or a map[int]interface{}) would silently fall back to the literal string instead of resolving. Replace the 20-line resolveVariable with: vl := liquid.VariableLookupParse(name, nil, nil); val := context.Evaluate(vl); if val == nil { return nil, false }; return val, true. This also deletes the redundant dual nil-check at lines 569-572. correctness 75 manual
2 liquid.go:569-572 Redundant nil checks. The first check if val == nil && len(parts) == 1 is completely subsumed by the second check if val == nil — both return (nil, false). The len(parts) guard has no effect. Collapse to a single if val == nil { return nil, false }. maintainability 100 safe_auto

P3 -- Low

# File Issue Reviewer Confidence Route
3 plans/PLAN.md:697, plans/IMPLEMENTATION.md:660 Spec docs say context.Evaluate(token.value) but that doesn't work. TagContext.Evaluate expects evaluable objects (e.g., *VariableLookup), not plain strings — passing a string returns the string unchanged. The implementation correctly deviated. Update both spec docs to reflect the actual approach (either FindVariable + traversal, or VariableLookupParse + Evaluate). correctness 100 advisory
4 liquid.go:523-525 Unterminated quote produces an unquoted token. parseTagTokens('"hello') yields {value:"hello", quoted:false}, which subjects it to variable resolution. Malformed markup is an edge case, but silently treating intended-literal content as a variable reference could produce confusing behavior. Consider logging a warning or treating as quoted. correctness 50 advisory

Coverage

  • Testing gaps: None for the implemented scope — 17 spec tests cover quoted/unquoted args, dotted paths, mixed args, type coercion, empty-string vs missing, block shortcodes, per-render disambiguation, and empty-return behavior for both engines including cross-engine parity.
  • Residual risks: The custom resolveVariable won't resolve variables through liquidgo Drop objects or typed slices/maps (finding #1). No existing tests exercise those paths, but they're part of liquidgo's contract.

Verification

  • 378/378 Ginkgo specs pass (including all 17 new shortcode convention specs)
  • go vet clean
  • go test -race clean
  • No encoding/json usage
  • parseTagArgs behavioral change (now handles mixed quoted/unquoted) is backward-compatible for its only other caller ({% inline %} tag in inline.go:67), which uses only args[0]

Verdict: Ready with fixes

Reasoning: The implementation is clean, well-structured, and all spec tests pass. The core logic in parseTagTokens, resolveTagArgs, and the empty-return removal is correct. The parseTagArgs refactor preserves backward compatibility. Finding #1 (using liquidgo's built-in variable resolution instead of a custom reimplementation) is worth addressing to avoid future divergence, but all currently-tested scenarios work. Finding #2 is trivially fixable dead code. Findings #3-4 are advisory.

@zeroedin zeroedin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Code Review Results (Multi-Agent)

Supersedes the earlier single-pass review.

Scope: internal/template/liquid.go (96+/25-), shortcode_convention_test.go (430+ new), changeset, PLAN.md, IMPLEMENTATION.md
Intent: Implement variable argument resolution for Liquid shortcodes (unquoted args resolve against template context, quoted stay literal) and remove <alloy-shortcode> placeholder for empty returns, achieving cross-engine parity with Go templates. Closes #981.
Mode: Interactive
Review team:

  • correctness (always)
  • testing (always)
  • maintainability (always)
  • project-standards (always) — no CLAUDE.md/AGENTS.md found, 0 findings
  • ce-agent-native-reviewer (always) — internal engine change, no gaps
  • ce-learnings-researcher (always) — no docs/solutions/ directory; surfaced E2E pipeline test learning from PR #1007
  • adversarial — 96+/25- executable Go lines in liquid.go (≥50 threshold)
  • previous-comments — CodeRabbitAI and prior review comments exist

P1 -- High

# File Issue Reviewer Confidence Route
1 liquid.go:555 Nil map value produces "<nil>" string argument. When a dotted-path variable resolves to a nil value (e.g., page.field where field is YAML null), resolveVariable returns (nil, true). resolveTagArgs then calls fmt.Sprint(nil) which produces the literal string "<nil>" — leaked Go internals in shortcode output. Fix: add val == nil check after resolveVariable returns; treat nil-valued variables as empty string or fall back to literal. correctness, adversarial 100 gated_auto → downstream-resolver
2 liquid.go:566 resolveVariable reimplements liquidgo's variable resolution with reduced capability. The custom traversal only handles map[string]interface{}. liquidgo's exported VariableLookupParse(name, nil, nil) + context.Evaluate(vl) handles the same case plus Drops, typed maps (via reflection), array index access, and custom types. A user with a struct-based Drop or typed map in their template context gets silent fallback to literal instead of resolution. Replace the 20-line function with 3 lines delegating to the library. correctness, maintainability, adversarial, previous-comments 100 manual → human
3 liquid.go:569 Redundant nil checks. if val == nil && len(parts) == 1 (line 569) is completely subsumed by if val == nil (line 572) — both return (nil, false). The len(parts) guard has no effect. Collapse to a single if val == nil { return nil, false }. maintainability, previous-comments 100 safe_auto → review-fixer

P2 -- Moderate

# File Issue Reviewer Confidence Route
4 liquid.go:517 Single-quoted arguments not handled by parseTagTokens. Only double quotes are recognized as string delimiters. {% tag 'primary' %} treats 'primary' as unquoted, subjects it to variable resolution, and falls back to the literal 'primary' (with quotes). Liquid supports both quote styles. Not a regression (old code also didn't handle single quotes), but the new quoted/unquoted distinction makes this gap more impactful since unquoted tokens now trigger resolution. previous-comments (CodeRabbitAI) 100 gated_auto → downstream-resolver
5 liquid.go:522 Unterminated quote silently converts intended literal to variable lookup. parseTagTokens('"hello') yields {value:"hello", quoted:false}, subjecting it to variable resolution. If a context variable hello exists, the shortcode receives its value instead of the malformed literal. testing, adversarial 75 advisory → human

P3 -- Low

# File Issue Reviewer Confidence Route
6 IMPLEMENTATION.md:660 Spec docs reference obsolete context.Evaluate() pattern. Both PLAN.md and IMPLEMENTATION.md say to use context.Evaluate(token.value) but Evaluate expects *VariableLookup objects, not strings. The implementation correctly deviated. Update specs to reflect the actual approach. previous-comments (CodeRabbitAI) 100 advisory → human

Learnings & Past Solutions

  • E2E pipeline tests required (from PR #1007 learning): Pipeline features need end-to-end tests through the actual build pipeline, not just unit tests of standalone functions. PR #1007's ProcessBlockShortcodes had 14 passing unit tests but was never called from renderPages(). Verify the variable resolution changes work through the full rendering pipeline.
  • Issue #1008 (escaped quotes): parseTagTokens doesn't handle \" inside quoted strings. Existing issue tracked separately; not a regression in this PR.
  • Issue #1004 (docs update): After this PR lands, shortcodes documentation should cover variable resolution behavior.

Coverage

  • Suppressed: 1 finding at anchor 50 (adversarial: parseTagArgs behavioral change for mixed-quoting callers)
  • Mode-aware demotions: 1 finding demoted to testing_gaps (nil context defensive guard, P3 advisory)
  • Testing gaps:
    • liquid.go:577 — No test for non-map intermediate value in dotted path (e.g., page.videoId where page is a string). Resolves to finding #2: replacing resolveVariable with VariableLookupParse eliminates this branch.
    • liquid.go:581 — No test for missing key in nested map. Same: resolves with finding #2.
    • liquid.go:572 — No test for nil root variable on dotted path (e.g., missing.field). Fallback works correctly but is untested.
    • liquid.go:555 — No test for nil-valued variable (YAML null). This is the exact trigger for finding #1.
    • liquid.go:549 — No test for nil context defensive guard (demoted from primary)
  • Residual risks:
    • parseTagArgs behavioral change: old code returned only quoted args when quotes were present (dropping unquoted tokens); new code returns all tokens. The {% inline %} tag (only other caller) uses args[0] only, so this is safe. Future callers of parseTagArgs that depend on the old quote-only behavior would break.
  • Failed reviewers: None (8/8 returned)

Verification

  • 378/378 Ginkgo specs pass (including all 17 new shortcode convention specs)
  • go vet clean
  • go test -race clean
  • No encoding/json usage

Verdict: Ready with fixes

Reasoning: The implementation is well-structured and all spec tests pass. The core logic for argument tokenization, resolution, and empty-return removal is correct for the tested scenarios. However, finding #1 (nil map values producing "<nil>" strings) is a real bug that will surface when users have YAML null fields — fix before merge. Finding #2 (reimplementing liquidgo's variable resolution) is a significant design concern that limits capability to map[string]interface{} only; this should be addressed to avoid silent failures with typed contexts. Finding #3 (redundant nil check) is trivially fixable dead code. Fix order: #1 (bug), #3 (dead code), then #2 (design improvement). Findings #4-6 are not merge-blocking.

- Replace custom resolveVariable with liquidgo's VariableLookupParse +
  Evaluate — handles Drops, typed maps, array index access, and avoids
  the nil→"<nil>" bug where YAML null values produced "<nil>" strings
- Add single-quote support to parseTagTokens (Liquid supports both
  quote styles)
- Remove redundant dual nil check (collapsed by the rewrite)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zeroedin

Copy link
Copy Markdown
Owner Author

Review response

All fixes pushed in d64918e.

P1 #1 — Nil map value produces "<nil>"

Fixed in d64918e. Replaced custom resolveVariable with liquid.VariableLookupParse + context.Evaluate. When the resolved value is nil (YAML null or missing), Evaluate returns nil, which falls back to the literal token string. No fmt.Sprint(nil) path exists.

P1 #2resolveVariable reimplements liquidgo with reduced capability

Fixed in d64918e. The 20-line function is now 5 lines delegating to liquid.VariableLookupParse(name, nil, nil) + context.Evaluate(vl). Handles Drops, typed maps (via reflection), array index access, and command methods.

P1 #3 — Redundant nil checks

Fixed in d64918e. Eliminated by the rewrite — single if val == nil check.

P2 #4 — Single-quoted arguments not handled

Fixed in d64918e. parseTagTokens now recognizes both '...' and "..." via strings.IndexByte(s[1:], quote). Unquoted token scanning also stops at single quotes.

P2 #5 — Unterminated quote silently converts to variable lookup

Skipped. Advisory, malformed markup edge case. The fallback-to-literal behavior means the worst case is the user sees the wrong string — same behavior as before this PR. Not a regression.

P3 #6 — Spec docs reference obsolete context.Evaluate() pattern

Skipped — architect's domain. Opened #1024 for the PLAN.md/IMPLEMENTATION.md updates.

Testing gaps (advisory)

  • Nil root variable on dotted path, non-map intermediate value, missing nested key: all resolved by delegating to liquidgo's VariableLookupParse, which handles these internally.
  • Nil-valued variable (YAML null): now falls back to literal via the val == nil guard in the simplified resolveVariable. No "<nil>" leak.
  • E2E pipeline test for variable resolution: the shortcode convention tests exercise the full Parse → Render path through liquidgo's tag dispatch. The resolution happens inside Render via TagContext, which is the real render context — not a mock. Opening an issue for E2E pipeline-level tests if the architect determines they're needed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/shortcode-calling-convention.md:
- Line 5: Update the documentation in shortcode-calling-convention.md to
capitalize “YouTube” consistently, including the shortcode example and
surrounding prose, without changing the documented behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e73352b-30c2-4c55-a88c-f4987ceba4cd

📥 Commits

Reviewing files that changed from the base of the PR and between 2042fb6 and d64918e.

📒 Files selected for processing (2)
  • .changeset/shortcode-calling-convention.md
  • internal/template/liquid.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/template/liquid.go

Comment thread .changeset/shortcode-calling-convention.md
zeroedin and others added 2 commits July 16, 2026 00:38
Three fixes to onConfig spec tests from issue #999:

1. Passthrough mapping tests missing source directories — Build()
   copies passthrough sources during Phase 3, but contentMap never
   created the vendor/fonts/ and vendor/valid/ directories.

2. Timeout enforcement test triggering WASM panic — the 200ms busy-wait
   inside QuickJS exceeds the 50ms timeout, orphaning a goroutine still
   inside the WASM engine. When Build() returns, registry.Close() calls
   QJS_Free while the orphaned goroutine is mid-execution, causing a
   WASM trap. Restructured to verify the mutation via hook chaining
   instead of triggering an actual timeout.

3. Timeout preservation test (same root cause as #2) — restructured
   to verify the no-mutation path without triggering a timeout.

The underlying QuickJSRuntime thread-safety gap (no mutex on Close()
vs in-flight operations) remains open as issue #1025.

Refs #1025

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix(test): repair flaky pipeline spec tests causing CI failures (#1025)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/pipeline/hooks_test.go (1)

1703-1738: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a package-internal test for the *config.Config no-op branch.
This test only covers the ordinary map-apply path; return config; from JS won’t hit the case *config.Config short-circuit, so applyOnConfigResult(cfg, cfg) still lacks direct coverage. A unit test in internal/pipeline would close that gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/hooks_test.go` around lines 1703 - 1738, Add a
package-internal unit test that directly calls applyOnConfigResult with the same
*config.Config as both the original and result, covering the case *config.Config
no-op branch. Assert it returns nil and leaves the original configuration
unchanged; do not rely on the existing BuildWithContent integration test or
JavaScript hook path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/pipeline/hooks_test.go`:
- Around line 1703-1738: Add a package-internal unit test that directly calls
applyOnConfigResult with the same *config.Config as both the original and
result, covering the case *config.Config no-op branch. Assert it returns nil and
leaves the original configuration unchanged; do not rely on the existing
BuildWithContent integration test or JavaScript hook path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9838f58-47ea-4f29-95a0-1d7ffffacd8b

📥 Commits

Reviewing files that changed from the base of the PR and between d64918e and d919876.

📒 Files selected for processing (1)
  • internal/pipeline/hooks_test.go

…ee (#1025)

RunWithTimeout spawns goroutines for hook calls. When a hook exceeds
the timeout, the goroutine is abandoned but still executing inside
r.ctx.Eval(). When Build() returns, registry.Close() calls r.rt.Close()
which frees the WASM instance while the orphaned goroutine is
mid-execution, causing a WASM out-of-bounds memory access panic.

The mutex serializes all access to the WASM context. Close() now blocks
until any in-flight CallHook/CallFilter/CallShortcode finishes, then
safely frees the runtime. Operations on an already-closed runtime
return the input unchanged.

Closes #1025

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zeroedin

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/plugin/wasm.go (1)

192-194: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Acquire the mutex after reading the file.

Holding r.mu across os.ReadFile lets slow filesystem I/O block every filter, shortcode, hook, and Close operation. Read and transform the source first, then lock immediately before QuickJS evaluation.

Suggested adjustment
 func (r *QuickJSRuntime) EvalFile(path string) error {
-	r.mu.Lock()
-	defer r.mu.Unlock()
 	content, err := os.ReadFile(path)
 	if err != nil {
 		return fmt.Errorf("%s: %w", filepath.Base(path), err)
 	}
 	// transform source...
+	r.mu.Lock()
+	defer r.mu.Unlock()
 	result, err := r.ctx.Eval(filepath.Base(path), qjs.Code(src))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/plugin/wasm.go` around lines 192 - 194, Update the relevant
source-loading flow in the plugin runtime to perform os.ReadFile and source
transformation before acquiring r.mu. Lock immediately before QuickJS
evaluation, keeping the mutex held only through evaluation and preserving the
existing unlock behavior for filter, shortcode, hook, and Close operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/plugin/wasm.go`:
- Around line 461-468: Update QuickJSRuntime.Close to clear the saved context
and mark the runtime uninitialized after closing it. Add the same closed-state
validation used by SetSiteData to EvalFile before calling r.ctx.Eval, rejecting
calls when the runtime is closed.

---

Nitpick comments:
In `@internal/plugin/wasm.go`:
- Around line 192-194: Update the relevant source-loading flow in the plugin
runtime to perform os.ReadFile and source transformation before acquiring r.mu.
Lock immediately before QuickJS evaluation, keeping the mutex held only through
evaluation and preserving the existing unlock behavior for filter, shortcode,
hook, and Close operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc3df581-df5a-415f-a98f-4eb4fc52dfd2

📥 Commits

Reviewing files that changed from the base of the PR and between d919876 and 8b01938.

📒 Files selected for processing (1)
  • internal/plugin/wasm.go

Comment thread internal/plugin/wasm.go

@zeroedin zeroedin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Code Review Results (Re-Review After Fixes)

Supersedes both prior reviews. All 8 agents returned.

Scope: internal/template/liquid.go (89+/25- net vs main), shortcode_convention_test.go (430+ new), changeset, PLAN.md, IMPLEMENTATION.md
Intent: Implement variable argument resolution for Liquid shortcodes and remove empty-return placeholder for cross-engine parity. Closes #981.
Mode: Interactive
Review team: correctness, testing, maintainability, project-standards, ce-agent-native-reviewer, ce-learnings-researcher, adversarial (≥50 executable lines), previous-comments (prior review threads exist)

Prior Findings Status

All 4 actionable findings from the prior review are resolved:

Prior # Finding Status
1 P1: fmt.Sprint(nil) producing "<nil>" ✅ Fixed — resolveVariable now returns (nil, false) for nil values
2 P1: resolveVariable reimplements library ✅ Fixed — delegates to VariableLookupParse + Evaluate (5 lines)
3 P1: Redundant nil checks ✅ Fixed — eliminated in rewrite
4 P2: Single-quoted args not handled ✅ Fixed — parseTagTokens handles both ' and "
5 P2: Unterminated quote (advisory) Unchanged — accepted as advisory
6 P3: Spec docs obsolete (advisory) Tracked via issue #1024

P2 -- Moderate

# File Issue Reviewer Confidence Route
1 liquid.go:572 VariableLookupParse strips non-identifier characters from token. parseTagTokens preserves $class as a single unquoted token, but VariableLookupParse's internal tokenizer silently drops $ and looks up class. If a context variable class exists, the shortcode receives its value instead of the literal $class. The old FindVariable("$class") path treated the name literally. Edge case — $-prefixed names aren't valid Liquid identifiers — but a behavioral change from the prior implementation. adversarial 75 advisory → human
2 liquid.go:575 Nil-valued and non-existent variables both fall back to literal token. Evaluate returns nil for both cases; resolveVariable returns (nil, false) for both. A shortcode receiving "page.videoId" when page.videoId is explicitly nil may be confusing. Correctness reviewer confirmed this is a liquidgo API limitation, not fixable without upstream changes. adversarial, correctness 75 advisory → human

P3 -- Low

# File Issue Reviewer Confidence Route
3 liquid.go:572 Command methods (.size, .first, .last) now resolve on arrays/strings. items.size previously fell through to literal; now resolves to array length via VariableLookup.Evaluate. This is standard Liquid behavior and likely desirable, but a silent behavioral change from the old implementation. adversarial 75 advisory → human
4 liquid.go:527 Unterminated quote silently converts literal to variable lookup. Same as prior review finding #5. {% tag "primary %} (missing closing quote) treats primary as unquoted, subjecting it to variable resolution. adversarial 75 advisory → human

Coverage

  • Suppressed: 0 findings
  • Mode-aware demotions: 0
  • Testing gaps:
    • liquid.go:520 — Single-quote parsing branch has zero test coverage (all spec tests use double quotes)
    • No test for nil-valued context variable in shortcode args
    • No test for special-character variable names ($var, @role) and their interaction with VariableLookupParse
    • No test for unterminated quotes in shortcode markup
  • Residual risks:
    • VariableLookupParse global cache (sync.Map in liquidgo) grows without eviction for every unique variable name. Low practical risk — variable names are template-author-controlled.
  • Failed reviewers: None (8/8 returned)

Verification

  • 378/378 Ginkgo specs pass
  • go vet clean
  • go test -race clean
  • No encoding/json usage
  • Correctness reviewer traced through liquidgo source to verify VariableLookupParse + Evaluate dispatches correctly for dotted paths, map access, array indexing, and command methods

Verdict: Ready to merge

Reasoning: All 4 prior P1/P2 findings are resolved. The resolveVariable rewrite from 20-line custom traversal to 5-line liquidgo delegation is clean and correct — verified by tracing through liquidgo source. Single-quote support is properly implemented. The 4 new findings are all P2-P3 advisory: edge cases around non-identifier characters in variable names (#1), nil-value conflation that's a liquidgo API limitation (#2), and two behavioral notes (#3, #4) that are either improvements or pre-existing. No merge-blocking issues remain.

- Add rt==nil guard to EvalFile to reject calls after Close()
- Clear ctx and initialized in Close() so SetSiteData's existing
  initialized check correctly rejects post-close calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zeroedin

Copy link
Copy Markdown
Owner Author

Review response (re-review round)

Prior findings

All 4 actionable findings from the first review confirmed resolved by re-review. No action needed.

New P2 #1VariableLookupParse strips non-identifier characters

Skipped. Advisory. $-prefixed names aren't valid Liquid identifiers. The old behavior (treating $class as a literal via FindVariable) was also technically wrong — FindVariable("$class") would never match a context key set via Liquid {% assign %}. No regression.

New P2 #2 — Nil-valued and non-existent variables both fall back to literal

Skipped. Liquidgo API limitation confirmed by correctness reviewer. FindVariable and Evaluate both return nil for both cases. No upstream fix available.

New P3 #3 — Command methods now resolve on arrays/strings

Skipped. Advisory. Standard Liquid behavior (items.size → array length). Improvement over the old silent literal fallback.

New P3 #4 — Unterminated quote

Skipped. Same as prior round finding #5. Malformed markup edge case, not a regression.

CodeRabbit: Capitalize "YouTube" in changeset

Skipped. youtube is a shortcode tag name (lowercase by convention), not a brand reference.

CodeRabbit: Reject calls after Close()

Fixed in 8530393. Close() now clears r.ctx and r.initialized. EvalFile has an r.rt == nil guard. SetSiteData's existing r.initialized check now catches post-close calls.

Testing gaps (advisory)

  • Single-quote branch: no spec test exercises it. Opening an issue would be appropriate if the architect wants coverage.
  • Nil-valued variable, special-char names, unterminated quotes: all edge cases with correct fallback behavior. No spec tests exist.

zeroedin and others added 2 commits July 16, 2026 01:11
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zeroedin
zeroedin merged commit a215a6b into main Jul 16, 2026
6 checks passed
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.

Shortcode calling convention diverges across engines: variable args never resolve in Liquid; empty returns emit placeholder element

1 participant