test: spec tests for inline tag hardening (#931, #932)#933
Conversation
Issue #932 (fail closed): Add test verifying inline tag returns an error when _contentRoot is missing from the render context, rather than silently skipping the path-traversal sandbox. Update existing tests 1, 2, and 5 to include _contentRoot to match production context shape. Issue #931 (mixed-case extension): Add two regression tests verifying case-insensitive extension matching — .SVG is accepted (text allowlist) and .PNG is rejected (binary blocklist). PLAN.md and IMPLEMENTATION.md updated with fail-closed requirement and case-insensitive extension note. Test status: - returns error when _contentRoot is not set (#932): RED - matches file extensions case-insensitively (#931): GREEN - rejects binary file type with mixed-case extension (#931): GREEN Refs #931, #932 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds/updates specification-level tests and planning docs around {% inline %} tag hardening in the Liquid template engine—specifically (a) requiring _contentRoot for path sandboxing (fail-closed) and (b) documenting/test-covering case-insensitive extension handling.
Changes:
- Update PLAN/IMPLEMENTATION docs to explicitly require fail-closed behavior when
_contentRootis missing and to note case-insensitive extension normalization. - Adjust existing inline-tag spec tests to pass
_contentRoot(to match production render context shape). - Add new spec tests for missing
_contentRoot(expected to be red until implementation changes) and mixed-case extension behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| plans/PLAN.md | Documents fail-closed _contentRoot requirement and notes case-insensitive extension handling. |
| plans/IMPLEMENTATION.md | Updates inline-tag implementation notes with fail-closed and case-insensitive extension guidance. |
| internal/template/liquid_test.go | Updates inline-tag tests to include _contentRoot and adds new regression/spec tests for _contentRoot and mixed-case extensions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
zeroedin
left a comment
There was a problem hiding this comment.
Code Review Results
Scope: internal/template/liquid_test.go, plans/PLAN.md, plans/IMPLEMENTATION.md
Intent: Harden the {% inline %} tag with a fail-closed sandbox test (#932) and regression tests for case-insensitive extension matching (#931). Update specs to match.
PR type: Architect spec PR — reviewing test quality, coverage, spec alignment, and PLAN.md/IMPLEMENTATION.md consistency.
Verification
All claims in the PR body confirmed by running the test suite:
| Test | Claimed | Actual |
|---|---|---|
returns error when _contentRoot is not set (#932) |
🔴 RED | 🔴 RED — Render() returns nil error; sandbox silently skipped |
matches file extensions case-insensitively (#931) |
✅ GREEN | ✅ GREEN — strings.ToLower normalizes .SVG |
rejects binary file type with mixed-case extension (#931) |
✅ GREEN | ✅ GREEN — strings.ToLower normalizes .PNG |
Updated: inlines an SVG file |
✅ GREEN | ✅ GREEN |
Updated: inserts content raw |
✅ GREEN | ✅ GREEN |
Updated: returns error when file not found |
✅ GREEN | ✅ GREEN |
Analysis
Test quality. All three new tests are self-contained, deterministic, use DeferCleanup for temp directory cleanup, and have specific assertions with descriptive failure messages. The red test (#932) asserts both that an error occurs AND that the error message mentions _contentRoot — this constrains the developer's implementation to produce a useful error, not just any error.
Existing test updates. The three updated tests add _contentRoot alongside _contentDir to match production render context shape. The tests that were NOT updated (returns error for binary file types, returns error for absolute paths) correctly omit _contentRoot — both error before reaching the sandbox check (inline.go:72-73 and inline.go:67-68 respectively), so adding _contentRoot would test nothing new and obscure the test's actual focus.
Spec consistency. PLAN.md and IMPLEMENTATION.md changes are internally consistent:
- Both describe the fail-closed requirement the same way: error when
_contentRootis missing/empty rather than skipping the sandbox - Both document case-insensitive extension matching via
strings.ToLower - IMPLEMENTATION.md includes concrete implementation guidance (invert the
if contentRoot != ""guard toif contentRoot == ""→ error) and specifies the exact error message string
No Skip() used. Red test fails with a real assertion failure as required.
Coverage
- Testing gaps: none for the scope of this PR
- Residual risks: none
Verdict: Ready to merge
Reasoning: Clean spec PR. The red test correctly captures the fail-closed security requirement at
inline.go:85where theif contentRoot != ""guard silently skips the sandbox. The green regression tests pin existingstrings.ToLowerbehavior so future removals would be caught. Spec docs are consistent with the tests. Implementation guidance is concrete and minimal (one guard inversion). No issues found.
Fix allowlist/blocklist terminology in all three files — the inline tag code enforces a binary-extension blocklist, not an allowlist. The PLAN.md "allowed file types" list is informational only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
zeroedin
left a comment
There was a problem hiding this comment.
Code Review Results (Re-review)
Scope: internal/template/liquid_test.go, plans/PLAN.md, plans/IMPLEMENTATION.md
Intent: Harden the {% inline %} tag with a fail-closed sandbox test (#932) and regression tests for case-insensitive extension matching (#931). Update specs to match.
PR type: Architect spec PR — reviewing test quality, coverage, spec alignment, and PLAN.md/IMPLEMENTATION.md consistency.
Prior Feedback Status
All three Copilot comments (allowlist → blocklist terminology in PLAN.md, IMPLEMENTATION.md, and liquid_test.go) were addressed in 395c1cd. Each fix is correct and minimal.
Re-verification
| Test | Expected | Actual |
|---|---|---|
returns error when _contentRoot is not set (#932) |
🔴 RED | 🔴 RED — Render() returns nil; sandbox silently skipped |
matches file extensions case-insensitively (#931) |
✅ GREEN | ✅ GREEN |
rejects binary file type with mixed-case extension (#931) |
✅ GREEN | ✅ GREEN |
inlines an SVG file (updated) |
✅ GREEN | ✅ GREEN |
inserts content raw (updated) |
✅ GREEN | ✅ GREEN |
returns error when file not found (updated) |
✅ GREEN | ✅ GREEN |
| 4 pre-existing inline tests | ✅ GREEN | ✅ GREEN |
Analysis
No new issues found. The fix commit is clean — three one-line terminology corrections, no behavioral changes. The blocklist terminology now accurately describes the code at inline.go:13-20 and inline.go:71-73. PLAN.md, IMPLEMENTATION.md, and test comments are consistent with each other and with the implementation.
Coverage
- Testing gaps: none
- Residual risks: none
Verdict: Ready to merge
Reasoning: Prior feedback fully addressed. All test status claims verified. No new issues introduced by the fix commit.
Summary
_contentRootis missing (inline tag: fail closed when _contentRoot is empty #932)_contentRootin render context to match production shapeTest summary
returns error when _contentRoot is not set(#932)Render()succeeds without error — sandbox silently skippedmatches file extensions case-insensitively(#931)strings.ToLoweralready normalizes.SVG→.svgrejects binary file type with mixed-case extension(#931)strings.ToLowernormalizes.PNG→.pngbefore blocklist checkExisting test updates
Tests 1 ("inlines an SVG file"), 2 ("inserts content raw"), and 5 ("returns error when file not found") now set
_contentRootalongside_contentDir. These changes are green — the current code ignores_contentRootwhen set (it just runs the sandbox check, which passes for same-directory files). After the developer makes_contentRootrequired, these tests will continue to pass because they already provide it.Implementation guidance
In
internal/template/inline.go, theresolvemethod at thecontentRootcheck (currentlyif contentRoot != ""):The change is minimal: invert the guard from
if contentRoot != ""(skip) toif contentRoot == ""(error), then unconditionally run the sandbox check.Refs #931, #932
🤖 Generated with Claude Code