Files
fuj-management/docs/plans/2026-05-08-1457-go-m6-7-single-binary-embed-verify.md
Jan Novak d981392593
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
feat(go): M6.7 — single-binary embed verification
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>
2026-05-08 15:24:47 +02:00

7.7 KiB

M6.7 — Single-binary embed verification

Context

M6.7 is the final task in M6 (Go-native HTML frontend). Per the progress tracker: "Wire embed.FS into handlers; verify single-binary deployment includes all assets."

The wiring is already in place from M6.1 onward:

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 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 staticassets in one place but not the other).

Implementation sketch:

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. 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, 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 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:

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

Files touched

File Change
go/internal/web/assets_test.go new — two tests (embed completeness + /static/* mux)
docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md tick M6.7, bump "last updated"
CHANGELOG.md new top entry

No production source files change. (If extracting the static handler reads cleaner, a 4-line refactor in server.go is acceptable but optional.)

Branch + MR

Per project convention this is a feature, so:

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.