---
phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin
plan: 03
type: execute
wave: 3
depends_on: ["11.2-01", "11.2-02"]
files_modified:
- scripts/check-phase11.2.sh
- internal/docsite/load_test.go
- internal/docsite/theme_test.go
- cmd/summer/docs_test.go
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md
- ../sm-summercms-app/plugins/golem15/summercms/static_test.go
- ../sm-summercms-app/plugins/golem15/summercms/routes_test.go
- ../sm-summercms-app/plugins/golem15/summercms/links_test.go
- ../sm-summercms-app/terminal_check_test.go
- ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts
- ../sm-summercms-app/vue-summercms-app/tests/scrollSpy.test.ts
autonomous: true
requirements: []
assumption_delta_decision: no-change
specless_probe_fallback: "skipped: phase has no requirement IDs to probe (visible skip); ROADMAP SC1-SC5 and the D-IDs are the acceptance contract"
user_setup: []
estimate:
tokens: 120000
raw_tokens: 120000
tasks: 3
confidence: low
must_haves:
truths:
- "Per SC5 and the CLAUDE.md rule that unit tests are the last plan, the site plugin package `git.golem15.com/golem15/sm-summercms-plugin` reaches at least 90.0% statement coverage from tests that need no build output (fstest fixtures), and `internal/docsite` stays at or above 85.0%."
- "Every serving rule from D-07 and D-47 has a test that fails when the rule breaks: content types from the own table, immutable cache only for `_nuxt/` (not `_nuxt/builds/`) and `_fonts/`, no-cache with a strong ETag elsewhere including `/docs/assets/`, 304 on a matching If-None-Match, HEAD and Range handled, dot-segment and traversal paths 404, extension-less docs paths 301 to `.html`, `/docs` 301 to `/docs/`, tree 404 pages with status 404, and no robots-blocking, CSP or frame-deny header on any response."
- "No redirect the plugin emits has a Location outside `/docs/`, and no Location starts with `//` (T-11.2-05)."
- "The plugin assembles alone through `surf.Assemble` with GET-only routes (POST 405, `/backend` falls to the site 404), fails closed with `public/site/index.html missing` on an empty tree, registers itself through init under `golem15.summercms`, declares no admin controllers, and its three patterns coexist with cabana's admin patterns on one ServeMux (ready for Phase 11.3)."
- "Per D-41 and D-46, every branch of `checkSiteURL`, `siteLabel`, the `site_url`/`site_label` parsing rules and the flag override precedence is tested, the header escapes the label, the link appears on the 404 page, and the unset header keeps its exact bytes."
- "Per D-40, the terminal-check helpers `loadTerminal`, `terminalScript` and `terminalEnv` have ungated tests, and the TypeScript utilities cover their edge cases."
- "`scripts/check-phase11.2.sh --all` runs the framework, plugin, app, site, built-tree, smoke, deploy and terminal stages, requires every named test to PASS (a SKIP, FAIL or zero-match fails), and prints `phase11.2 all passed`."
- "11.2-VALIDATION.md has every per-task row filled with its command and a green status, `status: validated`, `nyquist_compliant: true` and `wave_0_complete: true`, and keeps the manual-only rows (visual UAT, rome bring-up, external links and the verbatim clone at cutover)."
- "No production code changes in this plan except fixes for defects the new tests expose; each fix lands with its failing-then-passing test in the same commit and is listed in the SUMMARY. summercms.go commits here are tests and the gate only, after v0.1.0 (RESEARCH Pitfall 13)."
- statement: "External link reachability and the verbatim terminal clone are re-run at cutover after the repository is made public (D-38)."
verification: backstop
artifacts:
- path: "../sm-summercms-app/plugins/golem15/summercms/static_test.go"
provides: "table-driven tests of every static serving rule"
contains: "fstest.MapFS"
- path: "../sm-summercms-app/plugins/golem15/summercms/routes_test.go"
provides: "assembly, fail-closed, coexistence and identity tests"
contains: "surf.Assemble"
- path: "scripts/check-phase11.2.sh"
provides: "phase gate across the four repositories"
contains: "phase11.2 all passed"
- path: "../sm-summercms-app/terminal_check_test.go"
provides: "ungated tests of the D-40 helpers"
contains: "TestTerminalScript"
- path: "internal/docsite/load_test.go"
provides: "site_url and site_label branch tests"
contains: "TestCheckSiteURL"
- path: ".planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md"
provides: "validated per-task verification map"
contains: "nyquist_compliant: true"
key_links:
- from: "scripts/check-phase11.2.sh"
to: "../sm-summercms-app/plugins/golem15/summercms"
via: "go test -json with a detector that requires each named test to PASS"
pattern: "go -C .*summercms test"
- from: "../sm-summercms-app/plugins/golem15/summercms/routes_test.go"
to: "modules/surf router"
via: "surf.Assemble(backpack.New(nil), []party.Plugin{...})"
pattern: "surf\\.Assemble\\("
- from: "scripts/check-phase11.2.sh"
to: "../sm-summercms-app/scripts/smoke.sh"
via: "--smoke stage boots the binary on postgres:15"
pattern: "smoke\\.sh"
---
Bring full unit test coverage to the phase's new code and close the phase with a gate (SC5, CLAUDE.md "unit tests are always the last plan"). The site plugin's static handler and routes get table-driven tests over `fstest.MapFS` fixtures (D-07, D-47, T-11.2-04, T-11.2-05), the framework's `site_url`/`site_label` work gets every branch tested (D-41, D-46, T-11.2-09), the D-40 terminal-check helpers and the TypeScript utilities get their edge cases, and `scripts/check-phase11.2.sh` runs all four repositories' checks fail-closed. VALIDATION.md is filled and validated.
Purpose: plans 11.2-01 and 11.2-02 shipped smoke tests only; this plan pins every rule so a regression fails `go test`.
Output: test files in sm-summercms-plugin, sm-summercms-app, vue-summercms-app and summercms.go, the phase gate script in summercms.go, and a validated 11.2-VALIDATION.md.
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@CLAUDE.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-01-SUMMARY.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-02-SUMMARY.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-02-PLAN.md
@scripts/check-phase11.1.sh
@modules/boardwalk/boardwalk_test.go
@modules/surf/example_test.go
**Paths.** Commands run from the summercms.go root. `APP` = `../sm-summercms-app`, `PLUG` = `APP/plugins/golem15/summercms`, `SITE` = `APP/vue-summercms-app`. Each task names the repositories it commits to.
**Interfaces under test** are the ones plan 11.2-02 fixed in its `` block (`Plugin`, `newHandlers`, `tree`, `newTree`, `errMissingIndex`, `siteImmutable`, `contentTypes`, `contentType`, `redirectTo`, `cacheImmutable`, `cacheNoCache`; `checkSiteURL`, `siteLabel`, `Options.SiteURL`, `Options.SiteLabel`; `terminalGroup`, `loadTerminal`, `terminalScript`, `terminalEnv`, `terminalCloneURL`) and plan 11.2-01's TypeScript utilities (`copyPayload`, `activeSection`, `SPY_THRESHOLD`). Read the 11.2-02 SUMMARY for any name that changed during execution and test the shipped name.
**Conventions.** Stdlib `testing` only (no testify in these packages), table-driven where natural, `t.TempDir()` for files, `t.Fatalf("x = %v, want %v")`. Commit per repository, one logical change per commit, never a co-author tag. In summercms.go stage only the listed files; the VALIDATION.md update is a separate planning-docs commit. If a test exposes a production defect, fix it in the same commit as its test and list it in the SUMMARY; make no other production change.
Task 1: Tracer: the phase gate runs the site plugin's full rule table, and the plugin package reaches 90% coverage
../sm-summercms-app/plugins/golem15/summercms/static_test.go, ../sm-summercms-app/plugins/golem15/summercms/routes_test.go, ../sm-summercms-app/plugins/golem15/summercms/links_test.go, scripts/check-phase11.2.sh
- ../sm-summercms-app/plugins/golem15/summercms/plugin.go, static.go, smoke_test.go and links_test.go (as shipped by plan 11.2-02)
- modules/boardwalk/boardwalk_test.go lines 1-44 (fstest fixture and `get` helper)
- modules/surf/example_test.go lines 114-140 (surf.Assemble with backpack.New(nil))
- modules/party/registry.go lines 29-60 (Register, Activate)
- modules/cabana/http.go and modules/cabana/prefix.go (the admin route patterns to mirror in the coexistence test)
- scripts/check-phase11.1.sh lines 1-60 and 171-240 (mode layout, detect_json over go test -json)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md "Catch-all conflicts", "Cache-Control by path", "MIME table", Pitfalls 8-11
- Fixture site tree: `index.html`, `404.html`, `200.html`, `_payload.json`, `_nuxt/entry.abc.js`, `_nuxt/entry.abc.css`, `_nuxt/builds/latest.json`, `_nuxt/builds/meta/x.json`, `_fonts/r.woff2`, `_i18n/h/en/messages.json`, `robots.txt`, `sitemap.xml`, `og-image.png`, `favicon.ico`, `sun-logo.webp`, `.hidden`. Fixture docs tree: `index.html`, `404.html`, `.summer-docs`, `assets/site.css`, `assets/site.js`, `backend/admin-spa.html`, `backend/admin-spa.md`, `guide/index.html`, `llms.txt`, `search-index.json`.
- Site: `/` 200 `text/html; charset=utf-8` `no-cache`; `/_nuxt/entry.abc.js` `text/javascript; charset=utf-8` immutable; `/_nuxt/entry.abc.css` `text/css; charset=utf-8` immutable; `/_nuxt/builds/latest.json` and `/_nuxt/builds/meta/x.json` `application/json` no-cache; `/_fonts/r.woff2` `font/woff2` immutable; `/_i18n/h/en/messages.json`, `/_payload.json`, `/robots.txt`, `/sitemap.xml`, `/og-image.png`, `/favicon.ico`, `/sun-logo.webp` no-cache with their table types; `/.hidden`, `/_nuxt/../.hidden`, `/missing` 404 with the site 404 body, `text/html; charset=utf-8`, `no-cache`.
- Docs: `/docs/` and `/docs/index.html` 200; `/docs/backend/admin-spa` and `/docs/backend/admin-spa/` 301 to `/docs/backend/admin-spa.html`; `/docs/backend/admin-spa.md` `text/markdown; charset=utf-8`; `/docs/assets/site.css` no-cache (never immutable); `/docs/guide` and `/docs/guide/` serve `guide/index.html`; `/docs/.summer-docs` and `/docs/assets/../.summer-docs` 404 with the docs 404 body; `/docs/missing` 404; a request whose path is `/docs//evil.example/x` never yields a Location starting with `//`.
- ETag is `"` + 16 lowercase hex + `"`; equal content gives equal ETags, different content different ones; If-None-Match with the ETag gives 304 and no body; HEAD gives 200, a Content-Length and no body; `Range: bytes=0-3` gives 206.
- Every response (200, 301, 304, 404) carries no X-Robots-Tag, Content-Security-Policy or X-Frame-Options header; served files carry `X-Content-Type-Options: nosniff`.
- A tree without `404.html` answers a miss with Go's plain 404; `newHandlers` on a fixture without `site/index.html` or without `docs/index.html` returns an error naming `public/site/index.html missing` or `public/docs/index.html missing`.
- `contentType` returns the table value for every listed extension, lower-cases the extension (`X.HTML`), falls back to `mime` for an unlisted known type (`.pdf`), and to `application/octet-stream` for an unknown one; `siteImmutable` is true for `_nuxt/a.js` and `_fonts/x.woff2`, false for `_nuxt/builds/latest.json`, `index.html` and `_i18n/x.json`.
- Routes: `surf.Assemble(backpack.New(nil), []party.Plugin{&Plugin{fsys: fixture}})` gives `GET /` 200, `HEAD /` 200, `GET /docs` 301 to `/docs/`, `GET /docs/` 200, `POST /` 405, `GET /backend` 404 with the site 404 body; an empty fixture makes Assemble fail with `public/site/index.html missing`; a fresh ServeMux holding the plugin's three patterns plus `GET /backend`, `GET /backend/{path...}`, `GET /backend/assets/{vendor}/{plugin}/{file...}`, `POST /backend/api/v1/auth/login` and `GET /backend/api/v1/{vendor}/{plugin}/{controller}` registers without a panic and routes `/backend/x` to the admin handler and `/` to the site; `ID()` is `golem15.summercms`, `Requires()` is nil, `Register` and `Boot` return nil, the plugin does not satisfy `pact.HasAdminControllers`, and `party.Activate(backpack.New(nil), []string{"golem15.summercms"})` finds the init-registered plugin; `&Plugin{}` (embedded tree) either assembles (tree built) or fails with the missing-index error (tree not built), never panics.
- `pageLinks` extracts every `href` and `src` value, including duplicates removed and fragment-only links kept.
1. `static_test.go` in PLUG (package `summercms`): one fixture builder returning the `fstest.MapFS` from the behavior block (distinct bodies per file so ETags differ), a `do(h, method, target, header)` helper, and table-driven tests `TestStaticSite`, `TestStaticDocs`, `TestStaticConditionalAndRange`, `TestStaticNoBlockingHeaders`, `TestStaticRedirectLocations`, `TestStaticMissing404Page`, `TestNewHandlersMissingIndex`, `TestContentType` and `TestSiteImmutable` covering every bullet. For paths that `httptest.NewRequest` would normalise, set `r.URL.Path` directly so the handler's own cleaning is what is tested. Assert exact header values, not substrings, for Content-Type and Cache-Control.
2. `routes_test.go` in PLUG: `TestRoutesAssemble`, `TestRoutesFailClosed`, `TestRoutesCoexistWithAdminPatterns` (mirror cabana's patterns as literal strings, register with a `recover` guard), `TestPluginIdentity` and `TestPluginEmbeddedTree` per the behavior block.
3. `links_test.go` in PLUG: add the ungated `TestPageLinks` for the `pageLinks` helper; leave the build-gated tests as shipped.
4. Coverage: `go -C PLUG test -count=1 -coverprofile= ./...` then `go tool cover -func` total at least 90.0%. Add cases until it is, or record in the SUMMARY any line that is unreachable without a build and why.
5. `scripts/check-phase11.2.sh` in summercms.go (bash, `set -euo pipefail`, structure and `go test -json` detector modelled on `scripts/check-phase11.1.sh` `detect_json`: fail on a build failure, any FAIL, any SKIP of a named test, zero matched tests or "no tests to run"). This task implements `--plugin`: `go -C "$PLUG" vet ./...`, the named tests from steps 1-3 run with `-json` through the detector, and the coverage threshold; it prints `phase11.2 plugin passed`. Paths resolve from the script location (`APP="$ROOT/../sm-summercms-app"`). Unknown modes print usage and exit 2. Later tasks add the other modes.
Commit in PLUG as `test: cover every static serving rule and the plugin routes`, and in summercms.go as `test(11.2): add the phase gate with the plugin stage`.
go -C ../sm-summercms-app/plugins/golem15/summercms vet ./... && go -C ../sm-summercms-app/plugins/golem15/summercms test ./... -run '^(TestStatic|TestNewHandlersMissingIndex|TestContentType|TestSiteImmutable|TestRoutes|TestPlugin|TestPageLinks)' -count=1 -v
non-zero exit, a "--- FAIL" line, or "no tests to run"
scripts/check-phase11.2.sh --plugin
non-zero exit, a coverage line below 90.0%, or no "phase11.2 plugin passed" line
- `go -C ../sm-summercms-app/plugins/golem15/summercms test -cover ./... -count=1` prints a `coverage:` value of at least 90.0%.
- `go -C ../sm-summercms-app/plugins/golem15/summercms test ./... -run '^(TestStatic.*|TestRoutes.*|TestPlugin.*)$' -count=1 -v` prints at least 10 `--- PASS` lines (subtests included).
- Changing `siteImmutable` to also return true for `_nuxt/builds/` paths in a scratch copy makes `TestStaticSite` fail (checked once by hand during execution and recorded in the SUMMARY), proving the cache test fails when broken.
- `bash -n scripts/check-phase11.2.sh` exits 0 and `scripts/check-phase11.2.sh --bogus` exits 2.
Every static serving rule and route behaviour of the site plugin is pinned by a test that fails when the rule breaks, the plugin package is at 90% or more, and the phase gate runs that stage fail-closed.
Task 2: Every new framework and app branch is pinned: site_url and site_label rules, override precedence, header rendering, CLI flags, terminal-check helpers and the TypeScript edge cases
internal/docsite/load_test.go, internal/docsite/theme_test.go, cmd/summer/docs_test.go, ../sm-summercms-app/terminal_check_test.go, ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts, ../sm-summercms-app/vue-summercms-app/tests/scrollSpy.test.ts, scripts/check-phase11.2.sh
- internal/docsite/load.go, internal/docsite/docsite.go, internal/docsite/emit.go and internal/docsite/theme/templates/header.html (as shipped by plan 11.2-02: checkSiteURL, siteLabel, the override block in load)
- internal/docsite/load_test.go (TestParseSite table), internal/docsite/theme_test.go (themeTree, buildTheme, TestSiteLink), cmd/summer/docs_test.go (TestDocsBuildSiteFlags, TestToolCommandNames helpWants)
- ../sm-summercms-app/terminal_check_test.go (helpers as shipped)
- ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts, tests/scrollSpy.test.ts, app/utils/terminal.ts, app/utils/scrollSpy.ts (as shipped by plan 11.2-01)
- checkSiteURL accepts `https://acme.example`, `https://acme.example/`, `http://acme.example:8080/x`, `/` and `/home`; rejects the empty string, `javascript:alert(1)`, `JavaScript:alert(1)`, `data:text/html,x`, `//acme.example`, `acme.example`, `docs/x`, `https://`, `https://user:pw@acme.example`, `ftp://acme.example`, `/ x`, a value with a newline and a value with a leading tab.
- siteLabel returns a trimmed explicit label; else `acme.example` for `https://acme.example/docs`, `acme.example:8080` for `http://acme.example:8080/`; else `Home` for `/` and `/home`.
- ParseSite: `site_url` alone is accepted; `site_label` without `site_url`, a blank label and a two-line label are rejected with their messages.
- Load precedence: a yaml `site_url` plus an `Options.SiteURL` uses the option; a yaml `site_label` plus `Options.SiteLabel` uses the option; `Options.SiteLabel` with no URL anywhere fails with the `--site-label needs` message; an invalid `Options.SiteURL` fails with the `--site-url:` message; a yaml URL with no label derives the label.
- Rendering: a label `&` appears as `<b>&` in the header; the link is on `index.html`, a section page and `404.html`; with nothing set, the header contains the wordmark's closing tag immediately followed by a newline and `