# M6.7 — Single-binary embed verification ## Context M6.7 is the final task in M6 (Go-native HTML frontend). Per the [progress tracker](2026-05-03-2349-go-backend-rewrite-progress.md): "Wire `embed.FS` into handlers; verify single-binary deployment includes all assets." The wiring is **already in place** from M6.1 onward: - [go/internal/web/assets.go](../../go/internal/web/assets.go) declares `//go:embed templates` → `templateFS` and `//go:embed static` → `staticFS`. - [go/internal/web/render.go:66](../../go/internal/web/render.go#L66) parses every page template via `template.New(...).ParseFS(templateFS, ...)`. - [go/internal/web/server.go:48-68](../../go/internal/web/server.go#L48-L68) serves `/static/*` via `http.FileServerFS(fs.Sub(staticFS, "static"))`. - [go/build/Dockerfile](../../go/build/Dockerfile) copies only the compiled binary into the `alpine:3` runtime image — no `templates/` or `static/` directory ever lands beside it. What is missing is **proof** the embed is complete and stays complete: 1. Nothing fails the build/test if a contributor adds a new file under `internal/web/templates/` or `internal/web/static/` that isn't matched by an `//go:embed` glob (or, more realistically, adds a sibling directory like `static/img/` and the glob still picks it up — but a typo'd directive would silently drop it). 2. No automated test exercises the `/static/*` route against the embedded FS — current tests in [html_handler_test.go](../../go/internal/web/html_handler_test.go) render templates (which proves `templateFS` is good) but never hit a static URL through the mux. 3. The "single binary, no working-dir assets" property is undocumented — if it ever broke, no one would notice until the Docker image started 500'ing in prod. The intended outcome: a small test file plus a documented manual verification step, after which M6.7 can be ticked and M6 closed. ## Plan ### 1. Add `go/internal/web/assets_test.go` One new file, two tests, no production code changes. **Test A — embed completeness regression guard.** Walks the on-disk `templates/` and `static/` directories and asserts every regular file is also present in the corresponding embedded FS. Catches: - A new template added without updating the `//go:embed` directive (current globs are `templates` and `static` — recursive by default for directories, so this is a low-probability regression, but the test doubles as living documentation of the contract). - A typo in the directive (e.g. someone renames `static` → `assets` in one place but not the other). Implementation sketch: ```go func TestEmbedCompleteness(t *testing.T) { cases := []struct { name string diskFS fs.FS // os.DirFS("templates") / os.DirFS("static") embed fs.FS // exported helper or via internal test in package web root string }{...} for _, tc := range cases { _ = fs.WalkDir(tc.diskFS, ".", func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } embPath := tc.root + "/" + path if _, err := fs.Stat(tc.embed, embPath); err != nil { t.Errorf("file %q on disk but missing in embed.FS: %v", embPath, err) } return nil }) } } ``` Because `templateFS` and `staticFS` are unexported package vars, this test lives in `package web` (not `web_test`) — sibling to [assets.go](../../go/internal/web/assets.go). All the existing handler tests are in `package web_test`; that's fine, this one is internal. **Test B — `/static/*` end-to-end via the mux.** Builds an `http.ServeMux` with the same wiring as [server.go:68](../../go/internal/web/server.go#L68), fires httptest requests, asserts: - `GET /static/css/app.css` → 200, `Content-Type: text/css; charset=utf-8`, body contains a known string from app.css (e.g. a CSS selector). - `GET /static/js/member-detail.js` → 200, `Content-Type` starts with `text/javascript` or `application/javascript`, body non-empty. - `GET /static/js/payment-qr.js` → 200, body non-empty. - `GET /static/css/missing.css` → 404 (sanity: the file server actually rejects unknown paths instead of returning some default). Rather than duplicate the mux assembly, factor a tiny helper (or test the existing mux). The cleanest move: extract `staticHandler()` from [server.go:48-50,68](../../go/internal/web/server.go#L48-L68) into a small exported-from-package function or just `staticFS` / `fs.Sub` helper, and have the test call it. Smallest delta: keep production code unchanged and replicate the two-line wiring inside the test file (acceptable — it's two lines and the test exists precisely to lock that contract). ### 2. Manual / one-shot verification (no code; documented in plan only) Run once locally and tick M6.7. Command transcript: ```bash make go-build # → ./bin/fuj cp bin/fuj /tmp/fuj-standalone cd /tmp # working dir has no templates/ or static/ ./fuj-standalone server & SERVER_PID=$! sleep 1 curl -sf http://localhost:8080/adults | grep -q "Adults Dashboard" curl -sf http://localhost:8080/juniors | grep -q "Juniors" curl -sf http://localhost:8080/payments | grep -q "Payments Ledger" curl -sf -o /tmp/app.css http://localhost:8080/static/css/app.css \ && test -s /tmp/app.css curl -sf -o /tmp/qr.js http://localhost:8080/static/js/payment-qr.js \ && test -s /tmp/qr.js kill $SERVER_PID ``` `fuj server` will fail to talk to Sheets without credentials, so the `/adults` etc. pages will render with the `Error` field set — that's fine; the assertion is that the **template + static asset pipeline** is self-contained, not that data loads. Each curl above only checks for markup present in every render path (header text and stylesheet body). ### 3. Tracker + changelog - Tick `M6.7` in [docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md:120](2026-05-03-2349-go-backend-rewrite-progress.md#L120), append the merge SHA on the line. - Mark "Last updated" date and bump milestone status: M6 complete, next is M7. - Append a `CHANGELOG.md` entry per CLAUDE.md convention (`date "+%Y-%m-%d %H:%M %Z"`). ## Files touched | File | Change | | --- | --- | | [go/internal/web/assets_test.go](../../go/internal/web/assets_test.go) | **new** — two tests (embed completeness + `/static/*` mux) | | [docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md](2026-05-03-2349-go-backend-rewrite-progress.md) | tick M6.7, bump "last updated" | | [CHANGELOG.md](../../CHANGELOG.md) | new top entry | No production source files change. (If extracting the static handler reads cleaner, a 4-line refactor in [server.go](../../go/internal/web/server.go) is acceptable but optional.) ## Branch + MR Per project convention this is a feature, so: ```bash git checkout -b feat/go-m6-7-embed-verify # … commits … git push -u origin feat/go-m6-7-embed-verify tea pr create --title "feat(go): M6.7 — single-binary embed verification" \ --description "" --base main \ --head feat/go-m6-7-embed-verify ``` ## Verification After implementation: 1. `make go-test` → green (new `TestEmbedCompleteness` and `TestStaticAssetsServed` pass). 2. `make go-lint` → clean. 3. Run the manual transcript in §2 above — all curls succeed, no "template not found" or 404 on static assets. 4. `make go-build && docker build -f go/build/Dockerfile -t fuj-go:m6-7 go/` → succeeds; `docker run --rm -p 8080:8080 fuj-go:m6-7` serves `/adults` with stylesheet attached (visual smoke test in browser). ## Out of scope - Re-architecting how templates are parsed or served. - Compressing / fingerprinting static assets (a separate concern). - Live integration test with real Sheets data — covered later in M7.