fix(dx): update CLAUDE.md size gate, sync Makefile limit, add coverage tests
All checks were successful
Security Scan / security (pull_request) Successful in 7s
Test / test (pull_request) Successful in 38s

- CLAUDE.md: correct WASM raw size gate from 3 MB to 3.5 MB (matches size_test.go)
- CLAUDE.md: document error handling (log.E) and file I/O (coreio.Local) conventions
- Makefile: sync WASM_RAW_LIMIT to 3670016 (3.5 MB) to match size_test.go
- Tests: add coverage for NewContextWithService, Attr through wrapper nodes,
  Unless(true), and Text.Render with i18n service — core package 95.8% → 99.4%

No fmt.Errorf or os.ReadFile/os.WriteFile violations found.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-17 08:21:15 +00:00
parent 44b3f77806
commit e532c219b9
3 changed files with 65 additions and 5 deletions

View file

@ -27,7 +27,7 @@ See `docs/architecture.md` for full detail. Summary:
- **Responsive**: Multi-variant breakpoint wrapper (`data-variant` attributes), renders all variants in insertion order
- **Pipeline**: Render → StripTags → go-i18n/reversal Tokenise → GrammarImprint (server-side only)
- **Codegen**: Web Component classes with closed Shadow DOM, generated at build time by `cmd/codegen/`
- **WASM**: `cmd/wasm/` exports `renderToString()` only — size gate: < 3 MB raw, < 1 MB gzip
- **WASM**: `cmd/wasm/` exports `renderToString()` only — size gate: < 3.5 MB raw, < 1 MB gzip
## Server/Client Split
@ -53,6 +53,8 @@ Files guarded with `//go:build !js` are excluded from WASM:
- Licence: EUPL-1.2 — add `// SPDX-Licence-Identifier: EUPL-1.2` to new files
- Safe-by-default: HTML escaping via `html.EscapeString()` on Text nodes and attribute values, void element handling, entitlement deny-by-default
- Deterministic output: sorted attributes on El nodes, reproducible block ID paths
- Errors: use `log.E("scope", "message", err)` from `go-log`, never `fmt.Errorf`
- File I/O: use `coreio.Local` from `go-io`, never `os.ReadFile`/`os.WriteFile`
- Commits: conventional commits + `Co-Authored-By: Virgil <virgil@lethean.io>`
## Test Conventions

View file

@ -1,8 +1,8 @@
.PHONY: wasm test clean
WASM_OUT := dist/go-html.wasm
# Raw size limit: 3MB (Go WASM has ~2MB runtime floor)
WASM_RAW_LIMIT := 3145728
# Raw size limit: 3.5MB (Go 1.26 WASM runtime growth)
WASM_RAW_LIMIT := 3670016
# Gzip transfer size limit: 1MB (what users actually download)
WASM_GZ_LIMIT := 1048576
@ -21,9 +21,9 @@ $(WASM_OUT): $(shell find . -name '*.go' -not -path './dist/*')
echo "FAIL: gzip transfer size exceeds 1MB limit ($${GZ} bytes)"; \
exit 1; \
elif [ "$$RAW" -gt $(WASM_RAW_LIMIT) ]; then \
echo "WARNING: raw binary exceeds 3MB ($${RAW} bytes) — check imports"; \
echo "WARNING: raw binary exceeds 3.5MB ($${RAW} bytes) — check imports"; \
else \
echo "OK: gzip $${GZ} bytes (limit 1MB), raw $${RAW} bytes (limit 3MB)"; \
echo "OK: gzip $${GZ} bytes (limit 1MB), raw $${RAW} bytes (limit 3.5MB)"; \
fi
clean:

View file

@ -3,6 +3,8 @@ package html
import (
"strings"
"testing"
i18n "forge.lthn.ai/core/go-i18n"
)
func TestRawNode_Render(t *testing.T) {
@ -191,6 +193,62 @@ func TestAttr_NonElement(t *testing.T) {
}
}
func TestUnlessNode_True(t *testing.T) {
ctx := NewContext()
node := Unless(func(*Context) bool { return true }, Raw("hidden"))
got := node.Render(ctx)
if got != "" {
t.Errorf("Unless(true) = %q, want %q", got, "")
}
}
func TestAttr_ThroughIfNode(t *testing.T) {
ctx := NewContext()
inner := El("div", Raw("content"))
node := If(func(*Context) bool { return true }, inner)
Attr(node, "class", "wrapped")
got := node.Render(ctx)
want := `<div class="wrapped">content</div>`
if got != want {
t.Errorf("Attr through If = %q, want %q", got, want)
}
}
func TestAttr_ThroughUnlessNode(t *testing.T) {
ctx := NewContext()
inner := El("div", Raw("content"))
node := Unless(func(*Context) bool { return false }, inner)
Attr(node, "id", "test")
got := node.Render(ctx)
want := `<div id="test">content</div>`
if got != want {
t.Errorf("Attr through Unless = %q, want %q", got, want)
}
}
func TestAttr_ThroughEntitledNode(t *testing.T) {
ctx := NewContext()
ctx.Entitlements = func(string) bool { return true }
inner := El("div", Raw("content"))
node := Entitled("feature", inner)
Attr(node, "data-feat", "on")
got := node.Render(ctx)
want := `<div data-feat="on">content</div>`
if got != want {
t.Errorf("Attr through Entitled = %q, want %q", got, want)
}
}
func TestTextNode_WithService(t *testing.T) {
svc, _ := i18n.New()
ctx := NewContextWithService(svc)
node := Text("hello")
got := node.Render(ctx)
if got != "hello" {
t.Errorf("Text with service context = %q, want %q", got, "hello")
}
}
func TestSwitchNode(t *testing.T) {
ctx := NewContext()
cases := map[string]Node{