mirror of
https://github.com/yanislav-igonin/micrach
synced 2026-07-27 13:24:18 +03:00
1171 lines
36 KiB
Markdown
1171 lines
36 KiB
Markdown
# 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 -m` at 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/v3` unless a demonstrated compile/render incompatibility requires the documented `html/v2` fallback.
|
|
- 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.CreateThread` validation flow.
|
|
- Produces: `skipRateLimit(isLocal bool, path string) bool` and `registerStaticRoutes(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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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:
|
|
|
|
```go
|
|
Next: func(c *fiber.Ctx) bool {
|
|
return skipRateLimit(c.IsFromLocal(), c.Path())
|
|
},
|
|
```
|
|
|
|
Replace the two direct `app.Static` calls with:
|
|
|
|
```go
|
|
registerStaticRoutes(app)
|
|
```
|
|
|
|
Run:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```go
|
|
"bytes"
|
|
"mime/multipart"
|
|
"net/http"
|
|
|
|
"micrach/config"
|
|
```
|
|
|
|
Append:
|
|
|
|
```go
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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:
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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`; explicit `strconv.Atoi` parameter 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```go
|
|
"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:
|
|
|
|
```go
|
|
func registerStaticRoutes(app *fiber.App) {
|
|
app.Get("/uploads/*", static.New("./uploads"))
|
|
app.Get("/static/*", static.New("./static"))
|
|
}
|
|
```
|
|
|
|
Change the limiter callback only:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
"github.com/gofiber/template/html/v3"
|
|
```
|
|
|
|
Run:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```go
|
|
"github.com/gofiber/fiber/v3"
|
|
```
|
|
|
|
Change signatures exactly:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
path := "/" + strconv.Itoa(threadID)
|
|
return c.Redirect().Status(fiber.StatusFound).To(path)
|
|
```
|
|
|
|
Replace the reply redirect with:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
"github.com/gofiber/fiber/v3"
|
|
|
|
func GetCaptcha(c fiber.Ctx) error {
|
|
```
|
|
|
|
Replace the old raw fasthttp access:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
"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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.mod` only if a newly available compatible patch/minor release is selected
|
|
- Modify: `go.sum` only 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```markdown
|
|
# 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:
|
|
|
|
```markdown
|
|
- Fiber v3
|
|
```
|
|
|
|
In `AGENTS.md`, change the project overview to:
|
|
|
|
```markdown
|
|
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`:
|
|
|
|
```markdown
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
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 `rg` returns 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 `/tmp` artifacts.
|
|
|
|
**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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. confirm CAPTCHA image loads with `Content-Type: image/png`;
|
|
2. submit a wrong answer and confirm 400 with `INVALID CAPTCHA`;
|
|
3. solve a refreshed CAPTCHA and confirm valid thread creation returns 302;
|
|
4. inspect catalog and thread pages at narrow and wide viewport widths;
|
|
5. 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:
|
|
|
|
```bash
|
|
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.
|