All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
Add TestEmbedCompleteness and TestStaticAssetsServed in go/internal/web/assets_test.go. The completeness guard walks the on-disk templates/ and static/ directories and asserts every file is present in the corresponding embed.FS, catching forgotten files on future additions. The static mux test hits /static/css/app.css and all JS files through the same http.FileServerFS wiring used in server.go, confirming assets are served from the embedded FS with correct Content-Type and a 404 for unknown paths. Standalone binary smoke test passed manually: binary copied to /tmp (no adjacent templates/ or static/), assets served correctly. Closes M6. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
185 lines
7.7 KiB
Markdown
185 lines
7.7 KiB
Markdown
# 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 "<short body referencing M6.7>" --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.
|