36 KiB
Wave 2 Fiber v3 Migration Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Migrate micrach from Fiber v2 to the current stable Fiber v3 and compatible HTML template engine while preserving every route, status, template, form, upload path, and browser flow.
Architecture: Keep the existing main.go application assembly, Fiber controllers, global repositories/configuration, pgx persistence, server-rendered templates, and filesystem uploads. Add only two small test seams in main.go, characterize migration-sensitive behavior without external services, then manually adapt the Fiber APIs in one buildable migration slice.
Tech Stack: Go 1.26, current stable Fiber v3 (design-time snapshot v3.4.0), current stable Fiber HTML v3 (design-time snapshot v3.0.6), pgx v5, PostgreSQL 18 for manual smoke only, Docker Compose, govulncheck v1.6.0.
Global Constraints
- Recheck stable Fiber v3, Fiber HTML, and Go requirements through Context7, official release notes, and
go list -mat implementation start. - Keep Go 1.26 unless the rechecked stable Fiber release requires a later supported Go line.
- Preserve all existing routes, methods, status codes, templates, view data, form field names, static URLs, upload paths, response flow, and minimal-JS UI.
- Preserve current package boundaries and server-rendered architecture.
- Do not add repository mocks, dependency injection, an application factory, generalized compatibility wrappers, or another service layer.
- Do not add database migrations, schema changes, routes, features, CSS, JavaScript, or UI changes.
- Do not fix pre-existing bugs unless Fiber v3 causes a demonstrated regression.
go test ./...must require no PostgreSQL, Docker, network service, browser, or external process.- Do not add integration or end-to-end test infrastructure.
- Do not take another dependency major upgrade in Wave 2.
- Use
github.com/gofiber/template/html/v3unless a demonstrated compile/render incompatibility requires the documentedhtml/v2fallback. - Never commit
.env, uploads, binaries, credentials, temporary smoke files, or generated artifacts.
File Map
Create:
main_test.go: DB-free static-route and rate-limiter characterization.templates/templates_test.go: real template load/render compatibility.docs/superpowers/modernization-followups.md: confirmed remaining major/deprecated candidates, or an explicit no-findings record.
Modify:
main.go: two small test seams, Fiber v3 imports, limiter callback, static middleware.controllers/threads_controller.go: Fiber v3 context signatures, route parameter parsing, request-context propagation, explicit 302 redirects.controllers/captcha_controller.go: Fiber v3 signature and raw request-context API.controllers/threads_controller_test.go: multipart characterization and Fiber v3 test types.gateway/controllers.go: Fiber v3 signature/import.gateway/controllers_test.go: Fiber v3 import.go.mod,go.sum: Fiber v3 and HTML v3 module graph.README.md,AGENTS.md: Fiber v3 baseline and testing guidance.
Verify only unless a concrete failure requires an in-scope fix:
.github/workflows/push.yml: Go line and verification commands remain correct.Dockerfile: Go builder line remains correct and image still builds.templates/**/*.html,static/**,migrations/**,repositories/**,db/**,utils/**: behavior remains unchanged.
Task 1: Add DB-Free Fiber Behavior Characterization
Files:
- Create:
main_test.go - Create:
templates/templates_test.go - Modify:
main.go - Modify:
controllers/threads_controller_test.go
Interfaces:
-
Consumes: current Fiber v2
*fiber.Ctx,app.Static, limiter middleware, real templates,controllers.CreateThreadvalidation flow. -
Produces:
skipRateLimit(isLocal bool, path string) boolandregisterStaticRoutes(app *fiber.App); DB-free tests that pass unchanged in behavior after their Fiber v3 type/import adaptation. -
Step 1: Record the clean Wave 1 baseline
Run:
git status --short --branch
go test ./...
go vet ./...
go build ./...
Expected: worktree is clean apart from the already committed spec/plan history; all Go commands exit 0.
- Step 2: Write failing tests for the two application seams
Create main_test.go with the v2 imports used before dependency migration:
package main
import (
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
)
func TestRegisterStaticRoutes(t *testing.T) {
t.Chdir(t.TempDir())
files := map[string]string{
"static/styles/app.css": "body { color: black; }",
"uploads/42/o/7.png": "png-body",
}
for name, content := range files {
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
t.Fatalf("create fixture directory: %v", err)
}
if err := os.WriteFile(name, []byte(content), 0o644); err != nil {
t.Fatalf("write fixture: %v", err)
}
}
app := fiber.New()
registerStaticRoutes(app)
app.Get("/static/fallback", func(c *fiber.Ctx) error {
return c.SendString("fallback")
})
tests := []struct {
name string
path string
wantCode int
wantBody string
}{
{name: "checked-in static file", path: "/static/styles/app.css", wantCode: fiber.StatusOK, wantBody: files["static/styles/app.css"]},
{name: "generated upload", path: "/uploads/42/o/7.png", wantCode: fiber.StatusOK, wantBody: files["uploads/42/o/7.png"]},
{name: "missing file continues routing", path: "/static/fallback", wantCode: fiber.StatusOK, wantBody: "fallback"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := app.Test(httptest.NewRequest(http.MethodGet, tt.path, nil))
if err != nil {
t.Fatalf("request %s: %v", tt.path, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read response: %v", err)
}
if resp.StatusCode != tt.wantCode || string(body) != tt.wantBody {
t.Fatalf("response = (%d, %q), want (%d, %q)", resp.StatusCode, body, tt.wantCode, tt.wantBody)
}
})
}
}
func TestSkipRateLimit(t *testing.T) {
tests := []struct {
name string
isLocal bool
path string
want bool
}{
{name: "ordinary remote request", path: "/", want: false},
{name: "local request", isLocal: true, path: "/", want: true},
{name: "static", path: "/static/styles/index.css", want: true},
{name: "upload", path: "/uploads/1/o/1.png", want: true},
{name: "captcha", path: "/captcha/id", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := skipRateLimit(tt.isLocal, tt.path); got != tt.want {
t.Fatalf("skipRateLimit(%v, %q) = %v, want %v", tt.isLocal, tt.path, got, tt.want)
}
})
}
}
func TestLimiterRejectsOrdinaryRemoteRequests(t *testing.T) {
app := fiber.New()
app.Use(limiter.New(limiter.Config{
Next: func(c *fiber.Ctx) bool {
return skipRateLimit(c.IsFromLocal(), c.Path())
},
Max: 2,
}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
for requestNumber, want := range []int{fiber.StatusOK, fiber.StatusOK, fiber.StatusTooManyRequests} {
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil))
if err != nil {
t.Fatalf("request %d: %v", requestNumber+1, err)
}
resp.Body.Close()
if resp.StatusCode != want {
t.Fatalf("request %d status = %d, want %d", requestNumber+1, resp.StatusCode, want)
}
}
}
Run:
go test . -run 'Test(RegisterStaticRoutes|SkipRateLimit|LimiterRejectsOrdinaryRemoteRequests)' -count=1
Expected: build fails with undefined: registerStaticRoutes and
undefined: skipRateLimit.
- Step 3: Add the smallest application seams and use them in production assembly
Add above main() in main.go:
func skipRateLimit(isLocal bool, path string) bool {
isRequestForStatic := strings.Contains(path, "/static") ||
strings.Contains(path, "/uploads") ||
strings.Contains(path, "/captcha")
return isRequestForStatic || isLocal
}
func registerStaticRoutes(app *fiber.App) {
app.Static("/uploads", "./uploads")
app.Static("/static", "./static")
}
Replace the inline limiter predicate with:
Next: func(c *fiber.Ctx) bool {
return skipRateLimit(c.IsFromLocal(), c.Path())
},
Replace the two direct app.Static calls with:
registerStaticRoutes(app)
Run:
go fmt ./...
go test . -run 'Test(RegisterStaticRoutes|SkipRateLimit|LimiterRejectsOrdinaryRemoteRequests)' -count=1
Expected: all three tests pass. Existing production limiter and static behavior is unchanged.
- Step 4: Write a DB-free invalid multipart controller test
Add imports to controllers/threads_controller_test.go:
"bytes"
"mime/multipart"
"net/http"
"micrach/config"
Append:
func TestCreateThreadRejectsInvalidMultipartBeforeDatabase(t *testing.T) {
previousConfig := config.App
t.Cleanup(func() {
config.App = previousConfig
})
config.App.IsCaptchaActive = false
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("title", ""); err != nil {
t.Fatal(err)
}
if err := writer.WriteField("text", ""); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
app := fiber.New(fiber.Config{Views: testViews{}})
app.Post("/", CreateThread)
req := httptest.NewRequest(http.MethodPost, "/", &body)
req.Header.Set(fiber.HeaderContentType, writer.FormDataContentType())
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != fiber.StatusBadRequest {
t.Fatalf("status = %d, want %d", resp.StatusCode, fiber.StatusBadRequest)
}
}
Run:
go test ./controllers -run TestCreateThreadRejectsInvalidMultipartBeforeDatabase -count=1
Expected: PASS without an initialized database pool. The form includes both keys, so it does not exercise the pre-existing missing-field panic.
- Step 5: Add real template load/render characterization
Create templates/templates_test.go:
package templates
import (
"bytes"
"strings"
"testing"
"github.com/gofiber/template/html/v2"
"micrach/repositories"
)
func TestHTMLTemplatesLoadAndRender(t *testing.T) {
engine := html.New("../templates", ".html")
engine.AddFunc("Iterate", Iterate)
engine.AddFunc("NotNil", NotNil)
if err := engine.Load(); err != nil {
t.Fatalf("load templates: %v", err)
}
const message = "template compatibility"
var rendered bytes.Buffer
if err := engine.Render(&rendered, "pages/400", repositories.BadRequestHtmlData{Message: message}); err != nil {
t.Fatalf("render template: %v", err)
}
if !strings.Contains(rendered.String(), message) {
t.Fatalf("rendered template does not contain %q", message)
}
}
Run:
go test ./templates -run TestHTMLTemplatesLoadAndRender -count=1
go test ./...
go vet ./...
Expected: all commands pass with Fiber v2 and no external service.
- Step 6: Commit the characterization slice
git add main.go main_test.go controllers/threads_controller_test.go templates/templates_test.go
git diff --cached --check
git commit -m "test: characterize Fiber behavior"
Expected: focused commit containing only DB-free tests and the two small test seams.
Task 2: Migrate Fiber and HTML Template APIs
Files:
- Modify:
go.mod - Modify:
go.sum - Modify:
main.go - Modify:
main_test.go - Modify:
controllers/threads_controller.go - Modify:
controllers/captcha_controller.go - Modify:
controllers/threads_controller_test.go - Modify:
gateway/controllers.go - Modify:
gateway/controllers_test.go - Modify:
templates/templates_test.go
Interfaces:
-
Consumes: Task 1 characterization and existing repository methods accepting
context.Context. -
Produces: Fiber v3 handlers using
fiber.Ctx; explicitstrconv.Atoiparameter errors; explicit 302 redirects; static v3 middleware; HTML v3 engine; no Fiber v2 direct dependency. -
Step 1: Recheck and record current stable migration targets
Use Context7 and official release pages from the design spec, then run:
go list -m -json github.com/gofiber/fiber/v3@latest
go list -m -json github.com/gofiber/template/html/v3@latest
Expected at plan-writing time:
github.com/gofiber/fiber/v3 v3.4.0, GoVersion 1.25.0
github.com/gofiber/template/html/v3 v3.0.6, GoVersion 1.25.0
Requirements before continuing:
-
both resolved versions are stable releases;
-
both support Go 1.26;
-
release notes contain no migration blocker affecting micrach;
-
if either version differs from the snapshot, record the exact selected version in the implementation report and use it in every later command.
-
Step 2: Add the selected v3 modules
With the design-time versions shown here, run:
go get github.com/gofiber/fiber/v3@v3.4.0
go get github.com/gofiber/template/html/v3@v3.0.6
If Step 1 found newer stable versions, replace only the two version suffixes with those exact recorded stable versions.
Expected: commands exit 0. Fiber v2 remains temporarily because source imports have not yet changed.
- Step 3: Move application assembly and its tests to Fiber v3
In main.go, replace imports:
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/compress"
"github.com/gofiber/fiber/v3/middleware/limiter"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/gofiber/fiber/v3/middleware/static"
"github.com/gofiber/template/html/v3"
Replace registerStaticRoutes with:
func registerStaticRoutes(app *fiber.App) {
app.Get("/uploads/*", static.New("./uploads"))
app.Get("/static/*", static.New("./static"))
}
Change the limiter callback only:
Next: func(c fiber.Ctx) bool {
return skipRateLimit(c.IsFromLocal(), c.Path())
},
Keep middleware order, Max: 50, route order, engine functions, app config,
and listen behavior unchanged.
In main_test.go, replace Fiber and limiter imports with /v3, then change all
test handlers and callbacks from func(c *fiber.Ctx) to func(c fiber.Ctx).
Do not pass an explicit fiber.TestConfig to app.Test.
In templates/templates_test.go, replace the HTML import with:
"github.com/gofiber/template/html/v3"
Run:
go test . ./templates -count=1
Expected at this intermediate point: main/template packages compile and their tests pass; other packages may still fail until their v2 handler imports are migrated in the next steps.
- Step 4: Migrate thread handlers without changing business flow
In controllers/threads_controller.go, replace the Fiber import with:
"github.com/gofiber/fiber/v3"
Change signatures exactly:
func GetThreads(c fiber.Ctx) error
func GetThread(c fiber.Ctx) error
func CreateThread(c fiber.Ctx) error
func UpdateThread(c fiber.Ctx) error
Delete every:
ctx := c.UserContext()
Pass c directly as context.Context at every former ctx call site. This
includes repository calls, db.Pool.Acquire, conn.Begin, deferred
tx.Rollback, and tx.Commit. Representative exact forms:
threads, err := repositories.Posts.Get(c, limit, offset)
conn, err := db.Pool.Acquire(c)
tx, err := conn.Begin(c)
rollbackErr := tx.Rollback(c)
if err := tx.Commit(c); err != nil {
Replace both removed ParamsInt calls with error-preserving parsing:
threadID, err := strconv.Atoi(c.Params("threadID"))
if err != nil {
return c.Status(fiber.StatusNotFound).Render("pages/404", nil)
}
Replace the create-thread redirect with:
path := "/" + strconv.Itoa(threadID)
return c.Redirect().Status(fiber.StatusFound).To(path)
Replace the reply redirect with:
path := "/" + strconv.Itoa(threadID) + "#" + strconv.Itoa(postID)
return c.Redirect().Status(fiber.StatusFound).To(path)
Do not use Fiber v3's default redirect, because it is 303. Do not use
fiber.Params[int], because its fallback value would change invalid-ID flow.
- Step 5: Migrate CAPTCHA and gateway handlers
In controllers/captcha_controller.go, change the import and signature to:
"github.com/gofiber/fiber/v3"
func GetCaptcha(c fiber.Ctx) error {
Replace the old raw fasthttp access:
c.RequestCtx().SetContentType("image/png")
Do not use c.Context().SetContentType; Fiber v3 Context() now returns a
standard context.Context.
In gateway/controllers.go, change the import and signature to:
"github.com/gofiber/fiber/v3"
func Ping(c fiber.Ctx) error {
Keep header selection, JSON, and statuses unchanged.
- Step 6: Migrate controller and gateway tests
In controllers/threads_controller_test.go and
gateway/controllers_test.go, change imports from Fiber v2 to Fiber v3.
Change the test view signature to the v3-compatible spelling:
func (testViews) Render(out io.Writer, name string, _ any, _ ...string) error {
Keep every app.Test(req) call without an explicit config. No test may
initialize db.Pool or start an external service.
- Step 7: Normalize the module graph and prove old APIs are gone
Run:
go mod tidy
go fmt ./...
rg -n 'gofiber/fiber/v2|gofiber/template/html/v2|\*fiber\.Ctx|UserContext\(|ParamsInt\(|app\.Static\(|c\.Redirect\([^)]' --glob '*.go' go.mod
Expected: final rg returns no matches. go.mod has direct requirements for
Fiber /v3 and HTML /v3; old Fiber v2 and HTML v2 direct requirements are
absent.
Run:
go test ./...
go vet ./...
go build ./...
Expected: all commands pass without PostgreSQL or Docker. The real template render test proves HTML v3 compatibility, so fallback is not used.
If the HTML v3 compile/render test fails specifically inside the engine, record
the exact failure, restore github.com/gofiber/template/html/v2@v2.1.3, repeat
go mod tidy, and rerun this entire step. Do not fallback for an unrelated
application failure.
- Step 8: Review and commit the Fiber v3 migration
Run:
git diff -- go.mod go.sum main.go main_test.go controllers gateway templates/templates_test.go
git diff --check
Review requirements:
- no route or middleware order changed;
- both redirects specify
fiber.StatusFound; - invalid IDs fail before DB access;
- no form, upload, repository, template, or CSS behavior was redesigned.
Commit:
git add go.mod go.sum main.go main_test.go controllers gateway templates/templates_test.go
git commit -m "build: migrate to Fiber v3"
Task 3: Audit Dependencies and Document the New Baseline
Files:
- Create:
docs/superpowers/modernization-followups.md - Modify:
go.modonly if a newly available compatible patch/minor release is selected - Modify:
go.sumonly with an approved module change - Modify:
README.md - Modify:
AGENTS.md
Interfaces:
-
Consumes: passing Fiber v3 graph from Task 2.
-
Produces: reviewed direct dependencies, accurate Fiber v3 docs, and an explicit confirmed follow-up record.
-
Step 1: Audit direct and transitive modules
Run:
go list -u -m -json all > /tmp/micrach-wave2-modules.json
jq -r 'select(.Main != true and .Indirect != true) | [.Path, .Version, (.Update.Version // "current"), (.GoVersion // "unknown")] | @tsv' /tmp/micrach-wave2-modules.json
go mod graph | sort > /tmp/micrach-wave2-module-graph.txt
go mod why -m github.com/gofiber/fiber/v3
go mod why -m github.com/gofiber/template/html/v3
Expected from the design-time audit: captcha v1.1.0, imaging v1.6.2,
pgx/v5 v5.10.0, and godotenv v1.5.1 report current.
If a direct dependency has a newer patch/minor release, inspect its official
release notes, select its exact version, run go get module@version, then
repeat go mod tidy, go test ./..., and go vet ./.... Do not run a blanket
go get -u, and do not accept another major.
- Step 2: Audit deprecated and remaining major APIs
Run:
go vet ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
rg -n 'Deprecated:|gofiber/fiber/v2|gofiber/template/html/v2|io/ioutil|context[.]TO[D]O' --glob '*.go' --glob 'go.mod' .
Expected: vet and vulnerability scan exit 0; source scan finds no old Fiber, deprecated I/O, or broad placeholder contexts. Comments unrelated to an imported deprecated API do not become Wave 2 work.
- Step 3: Write the confirmed follow-up record
If the audit matches the expected no-findings state, create
docs/superpowers/modernization-followups.md:
# Modernization Follow-ups
Updated: 2026-07-16
Wave 2 completed the confirmed Fiber v3 and template-engine major upgrades.
The final direct-module, deprecated-API, `go vet`, and `govulncheck` audits found
no remaining major-version or deprecated-API candidate requiring a scheduled
follow-up.
Recheck this file during the next deliberate dependency refresh. Do not add
speculative architecture cleanup or ordinary patch/minor updates here.
If Step 1 or Step 2 proves a real deferred candidate, replace the no-findings paragraph with a concrete entry containing its package/API, current version or usage, candidate version/replacement, official evidence URL, and precise reason for deferral. Do not write placeholder markers, guesses, or unrelated refactoring ideas.
- Step 4: Update README and AGENTS
In README.md, change the stack item exactly:
- Fiber v3
In AGENTS.md, change the project overview to:
micrach is a small, server-rendered, single-board imageboard. It uses Go,
Fiber v3, Fiber HTML templates, PostgreSQL through `pgx`, and filesystem-backed
image uploads. It intentionally uses little browser-side JavaScript.
Add this testing rule under Testing Expectations in AGENTS.md:
Routine `go test ./...` must remain DB-free. Fiber handler and middleware tests
must use in-memory apps and temporary directories; PostgreSQL-backed behavior
belongs to the explicit manual smoke pass unless a later design approves an
integration-test harness.
Keep Go 1.26, PostgreSQL 18, Compose, Docker, CI, and all other documentation unchanged unless the rechecked Fiber minimum forced a Go-line change.
- Step 5: Verify and commit dependency documentation
Run:
go mod tidy
go test ./...
go vet ./...
rg -n 'Fiber v2|gofiber/fiber/v2|gofiber/template/html/v2' README.md AGENTS.md go.mod
rg -n 'gofiber/fiber/v2|gofiber/template/html/v2' --glob '*.go' .
git diff --check
Expected: tests/vet pass; rg returns no stale baseline; module files remain
stable after tidy.
Commit:
git add go.mod go.sum README.md AGENTS.md docs/superpowers/modernization-followups.md
git commit -m "docs: record Fiber v3 baseline"
If go.mod and go.sum did not change in this task, omit them from git add.
Task 4: Run the Automated Wave 2 Gate
Files:
- Verify only; modify only the file responsible for a concrete in-scope failure.
Interfaces:
-
Consumes: completed migration and documentation tasks.
-
Produces: fresh automated evidence with no external service used by Go tests.
-
Step 1: Verify repository and module hygiene
Run:
git status --short --branch
git diff --check
git ls-files .env uploads micrach '*.out' '*.test'
go mod tidy
git diff --exit-code -- go.mod go.sum
go list -m all | rg 'github.com/gofiber/(fiber/v3|template/html/v3)'
Expected:
- worktree is clean;
- no forbidden artifact is tracked;
- tidy changes nothing;
- Fiber v3 and HTML v3 appear in the graph.
Run:
if go list -m all | rg -q 'github.com/gofiber/fiber/v2|github.com/gofiber/template/html/v2'; then
echo 'obsolete direct/graph dependency remains'
exit 1
fi
Expected: exit 0 for the primary HTML v3 path. Do not reject
github.com/gofiber/template/v2 when it is a legitimate transitive dependency
of HTML v3; it is a different module from template/html/v2.
- Step 2: Run the full Go and vulnerability gate
Run:
go fmt ./...
test -z "$(gofmt -l .)"
go test ./...
go vet ./...
go build ./...
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/micrach-linux-amd64 .
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
Expected: every command exits 0; no test starts or requires PostgreSQL, Docker, network access, a browser, or another external process; no reachable vulnerability remains unhandled.
- Step 3: Build and inspect the container
Run:
docker build --tag micrach:wave2 .
docker image inspect micrach:wave2 --format '{{.Config.User}} {{json .Config.Entrypoint}}'
docker run --rm --entrypoint sh micrach:wave2 -c 'touch /app/uploads/write-test && test -f /app/uploads/write-test'
Expected inspect output:
micrach ["./micrach"]
Expected: image build and writable-upload check exit 0. No image is published.
- Step 4: Review the final code diff against migration constraints
Run:
git diff origin/master...HEAD --stat
git diff origin/master...HEAD -- main.go controllers gateway go.mod README.md AGENTS.md
rg -n 'gofiber/fiber/v2|\*fiber\.Ctx|UserContext\(|ParamsInt\(|app\.Static\(' --glob '*.go' .
git status --short --branch
Expected:
- final
rgreturns no matches; - no repository, schema, migration, template, CSS, or JavaScript redesign is in the diff;
- branch contains focused, buildable commits and a clean worktree.
Task 5: Run the Manual Application Smoke Matrix
Files:
- Verify only; use disposable runtime state and
/tmpartifacts.
Interfaces:
-
Consumes: verified Wave 2 binary, local Compose PostgreSQL, existing routes and forms.
-
Produces: recorded operational evidence for catalog, forms, redirects, CAPTCHA, uploads, limiter,
sage, bump limit, archival, static files, and responsive pages. This task creates no automated integration test. -
Step 1: Start PostgreSQL and create an isolated smoke database
Run:
docker compose up -d db
docker compose ps
SMOKE_DB="micrach_wave2_$(date +%s)"
docker compose exec -T db createdb -U micrach "$SMOKE_DB"
printf '%s\n' "$SMOKE_DB" > /tmp/micrach-wave2-smoke-db
Expected: Compose db is healthy and the uniquely named database is created.
Do not run docker compose down -v; existing developer data must not be
deleted.
- Step 2: Generate disposable upload fixtures
Run on macOS:
cp static/images/errors/404.png /tmp/micrach-wave2.png
sips -s format jpeg static/images/errors/404.png --out /tmp/micrach-wave2.jpg
cp /tmp/micrach-wave2.png /tmp/micrach-wave2-oversize.png
dd if=/dev/zero bs=1048576 count=4 >> /tmp/micrach-wave2-oversize.png 2>/dev/null
file /tmp/micrach-wave2.png /tmp/micrach-wave2.jpg
Expected: file identifies one PNG and one JPEG; oversize fixture exceeds 3
MiB. These remain outside the repository.
- Step 3: Start the application with smoke-specific limits
Run:
SMOKE_DB="$(cat /tmp/micrach-wave2-smoke-db)"
ENV=development \
PORT=3100 \
POSTGRES_URL="postgres://micrach:micrach@localhost:5432/${SMOKE_DB}?pool_max_conns=5" \
IS_DB_SEEDED=false \
IS_CAPTCHA_ACTIVE=false \
IS_RATE_LIMITER_ENABLED=false \
GATEWAY_URL= \
THREADS_MAX_COUNT=2 \
THREAD_BUMP_LIMIT=3 \
go run . >/tmp/micrach-wave2-app.log 2>&1 &
APP_PID=$!
printf '%s\n' "$APP_PID" > /tmp/micrach-wave2-app.pid
Poll without a long blocking sleep:
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if curl -fsS http://localhost:3100/ >/dev/null; then break; fi
sleep 1
done
curl -fsS http://localhost:3100/ >/dev/null
Expected: migrations apply to the isolated DB and catalog returns 200.
- Step 4: Verify catalog, errors, static files, and CAPTCHA media
Run:
curl -sS -o /tmp/micrach-wave2-catalog.html -w '%{http_code}\n' http://localhost:3100/
curl -sS -o /tmp/micrach-wave2-page0.html -w '%{http_code}\n' 'http://localhost:3100/?page=0'
curl -sS -o /tmp/micrach-wave2-invalid.html -w '%{http_code}\n' http://localhost:3100/invalid
curl -sS -o /tmp/micrach-wave2-css -w '%{http_code}\n' http://localhost:3100/static/styles/index.css
Expected status sequence:
200
404
404
200
Open the catalog in a browser at narrow and wide widths. Expected: form, catalog, pagination area, header, and footer have no visual regression.
CAPTCHA generation uses a live ID embedded in the form only when CAPTCHA is
enabled. Restart the application later in Step 8 with CAPTCHA enabled and
verify the returned image reports Content-Type: image/png.
- Step 5: Verify thread/reply redirects, uploads,
sage, bump limit, and archival
Create thread A with PNG and JPEG:
curl -sS -D /tmp/micrach-wave2-thread-a.headers -o /tmp/micrach-wave2-thread-a.body \
-F 'title=thread A' -F 'text=first thread' \
-F 'files=@/tmp/micrach-wave2.png;type=image/png' \
-F 'files=@/tmp/micrach-wave2.jpg;type=image/jpeg' \
http://localhost:3100/
rg -n '^HTTP/|^Location:' /tmp/micrach-wave2-thread-a.headers
Expected: 302 and Location: /<threadAID>. Save the numeric ID:
THREAD_A="$(awk '/^Location:/ {gsub("/|\\r", "", $2); print $2}' /tmp/micrach-wave2-thread-a.headers)"
test -n "$THREAD_A"
Create thread B the same way without files, then add one normal reply to A:
curl -sS -D /tmp/micrach-wave2-thread-b.headers -o /dev/null \
-F 'title=thread B' -F 'text=second thread' http://localhost:3100/
THREAD_B="$(awk '/^Location:/ {gsub("/|\\r", "", $2); print $2}' /tmp/micrach-wave2-thread-b.headers)"
curl -sS -D /tmp/micrach-wave2-reply-a.headers -o /dev/null \
-F 'text=normal reply' "http://localhost:3100/${THREAD_A}"
rg -n '^HTTP/|^Location:' /tmp/micrach-wave2-reply-a.headers
Expected: reply returns 302; Location matches
/<threadAID>#<newPostID>. Catalog shows A bumped above B.
Add a sage reply to B:
curl -sS -D /tmp/micrach-wave2-reply-b.headers -o /dev/null \
-F 'text=sage reply' -F 'sage=on' "http://localhost:3100/${THREAD_B}"
Expected: 302 with fragment; B does not move above A.
Create thread C:
curl -sS -D /tmp/micrach-wave2-thread-c.headers -o /dev/null \
-F 'title=thread C' -F 'text=third thread' http://localhost:3100/
rg -n '^HTTP/|^Location:' /tmp/micrach-wave2-thread-c.headers
Expected: 302; because THREADS_MAX_COUNT=2, the oldest active thread is
archived. At this point B is the oldest active thread because A was bumped and
B's sage reply did not bump it. Attempting to reply to B returns 400 with the
existing THREAD IS ARCHIVED page.
Add another normal reply to A. With THREAD_BUMP_LIMIT=3, A now reaches the
limit and must not move above C. Verify C remains first in the catalog.
Create thread D. Expected: A is now the oldest active thread and becomes
archived; replying to A returns 400 with the existing THREAD IS ARCHIVED
page. This separately proves bump-limit and archival behavior.
Inspect thread A in the browser. Expected: both original images and thumbnails
load from unchanged /uploads/<threadID>/... paths.
- Step 6: Verify validation boundaries without external test infrastructure
Run:
curl -sS -o /tmp/micrach-wave2-oversize.body -w '%{http_code}\n' \
-F 'title=oversize' -F 'text=oversize upload' \
-F 'files=@/tmp/micrach-wave2-oversize.png;type=image/png' \
http://localhost:3100/
curl -sS -o /tmp/micrach-wave2-four-files.body -w '%{http_code}\n' \
-F 'title=four files' -F 'text=four uploads' \
-F 'files=@/tmp/micrach-wave2.png;type=image/png' \
-F 'files=@/tmp/micrach-wave2.png;type=image/png' \
-F 'files=@/tmp/micrach-wave2.png;type=image/png' \
-F 'files=@/tmp/micrach-wave2.png;type=image/png' \
http://localhost:3100/
Expected: oversize request returns 400 with the existing file-size message;
four valid small files remain accepted and return 302. If Fiber rejects the
request before application validation due solely to changed v3 multipart body
enforcement, capture status/log evidence and add only the smallest explicit
fiber.Config.BodyLimit needed to preserve the verified Wave 1 contract, then
rerun Tasks 2-5 gates.
- Step 7: Verify limiter behavior from a non-loopback client
Stop the current app, then restart it with rate limiting enabled and other smoke settings unchanged:
kill "$(cat /tmp/micrach-wave2-app.pid)"
wait "$(cat /tmp/micrach-wave2-app.pid)" 2>/dev/null || true
SMOKE_DB="$(cat /tmp/micrach-wave2-smoke-db)"
ENV=development PORT=3100 \
POSTGRES_URL="postgres://micrach:micrach@localhost:5432/${SMOKE_DB}?pool_max_conns=5" \
IS_DB_SEEDED=false IS_CAPTCHA_ACTIVE=false IS_RATE_LIMITER_ENABLED=true \
GATEWAY_URL= \
THREADS_MAX_COUNT=2 THREAD_BUMP_LIMIT=3 \
go run . >/tmp/micrach-wave2-app.log 2>&1 &
printf '%s\n' "$!" > /tmp/micrach-wave2-app.pid
Local requests are intentionally bypassed, so the DB-free unit test is the
authoritative 429 check for remote requests. Manually confirm repeated local
catalog requests and /static, /uploads, and /captcha paths remain
unlimited. Do not weaken IsFromLocal or trust proxy headers merely to force a
manual 429.
- Step 8: Verify pagination with the same disposable database
Restart with CAPTCHA and limiter disabled and THREADS_MAX_COUNT=20:
kill "$(cat /tmp/micrach-wave2-app.pid)"
wait "$(cat /tmp/micrach-wave2-app.pid)" 2>/dev/null || true
SMOKE_DB="$(cat /tmp/micrach-wave2-smoke-db)"
ENV=development PORT=3100 \
POSTGRES_URL="postgres://micrach:micrach@localhost:5432/${SMOKE_DB}?pool_max_conns=5" \
IS_DB_SEEDED=false IS_CAPTCHA_ACTIVE=false IS_RATE_LIMITER_ENABLED=false \
GATEWAY_URL= THREADS_MAX_COUNT=20 THREAD_BUMP_LIMIT=3 \
go run . >/tmp/micrach-wave2-app.log 2>&1 &
printf '%s\n' "$!" > /tmp/micrach-wave2-app.pid
Create eleven small text-only threads:
for number in 1 2 3 4 5 6 7 8 9 10 11; do
curl -fsS -o /dev/null \
-F "title=pagination ${number}" -F "text=page fixture ${number}" \
http://localhost:3100/
done
curl -sS -o /tmp/micrach-wave2-page2.html -w '%{http_code}\n' 'http://localhost:3100/?page=2'
curl -sS -o /tmp/micrach-wave2-page3.html -w '%{http_code}\n' 'http://localhost:3100/?page=3'
Expected: page 2 returns 200 and contains catalog content; page 3 returns 404; pagination links navigate between pages 1 and 2.
- Step 9: Verify CAPTCHA and responsive browser behavior
Restart exactly:
kill "$(cat /tmp/micrach-wave2-app.pid)"
wait "$(cat /tmp/micrach-wave2-app.pid)" 2>/dev/null || true
SMOKE_DB="$(cat /tmp/micrach-wave2-smoke-db)"
ENV=development PORT=3100 \
POSTGRES_URL="postgres://micrach:micrach@localhost:5432/${SMOKE_DB}?pool_max_conns=5" \
IS_DB_SEEDED=false IS_CAPTCHA_ACTIVE=true IS_RATE_LIMITER_ENABLED=false \
GATEWAY_URL= THREADS_MAX_COUNT=20 THREAD_BUMP_LIMIT=3 \
go run . >/tmp/micrach-wave2-app.log 2>&1 &
printf '%s\n' "$!" > /tmp/micrach-wave2-app.pid
Open the catalog in a browser, then:
- confirm CAPTCHA image loads with
Content-Type: image/png; - submit a wrong answer and confirm 400 with
INVALID CAPTCHA; - solve a refreshed CAPTCHA and confirm valid thread creation returns 302;
- inspect catalog and thread pages at narrow and wide viewport widths;
- confirm no template, CSS, static, upload, or browser-flow regression.
This remains a manual smoke pass. Do not add browser automation or CAPTCHA test hooks.
- Step 10: Clean disposable state and record results
Run:
if test -f /tmp/micrach-wave2-app.pid; then
kill "$(cat /tmp/micrach-wave2-app.pid)" 2>/dev/null || true
fi
SMOKE_DB="$(cat /tmp/micrach-wave2-smoke-db)"
docker compose exec -T db dropdb -U micrach "$SMOKE_DB"
rm -f /tmp/micrach-wave2-* /tmp/micrach-linux-amd64
git status --short --branch
Expected: isolated database and /tmp fixtures are removed; existing Compose
database/volume and .env remain untouched; Git worktree is clean.
Record in the pull request:
- selected Fiber, HTML, and Go versions;
- HTML v3 success or exact fallback reason;
- all automated commands and results;
- manual status/redirect/static/upload/CAPTCHA/limiter/
sage/bump/archive and responsive results; - confirmed follow-ups or explicit no-findings result;
- statement that no DB-backed/integration/E2E tests were added.