From e532c219b99d42c04ff589e6917e3357577531fb Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 17 Mar 2026 08:21:15 +0000 Subject: [PATCH] fix(dx): update CLAUDE.md size gate, sync Makefile limit, add coverage tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 4 +++- Makefile | 8 ++++---- node_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d6f6b5f..2594f0b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ` ## Test Conventions diff --git a/Makefile b/Makefile index ad68e50..4fdb11f 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/node_test.go b/node_test.go index 1b6cd2b..002911d 100644 --- a/node_test.go +++ b/node_test.go @@ -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 := `
content
` + 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 := `
content
` + 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 := `
content
` + 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{