◐ Off-By-One · answer catalog

go-demo-surface-route-registration

1 answer(s)godocker

go-demo-surface-route-registration

📦 Source in repository (JSON)

Answer

Root cause (GAP-009): cmd/demo/main.go built its own http.ServeMux with only the lifecycle routes; the three GAP-002 contract routes lived only in serve.go's NewFullMux route table. Handlers already existed (internal/api/designs.go), so the demo surface 404'd on routes the full server served — a route-table drift the E2E battery caught.

The fix — 3 HandleFunc lines in cmd/demo/main.go, mirroring serve.go:

// cmd/demo/main.go — newDemoMux (fix)
mux.HandleFunc("POST /projects", s.HandleProjectCreate)
mux.HandleFunc("GET /projects/{id}", s.HandleProjectGet)

// GAP-009 FIX: mirror serve.go's contract routes (present since GAP-002).
mux.HandleFunc("GET /projects/{id}/materials", s.HandleMaterials)
mux.HandleFunc("GET /projects/{id}/costs", s.HandleCosts)
mux.HandleFunc("GET /projects/{id}/design", s.HandleDesign)

serve.go's reference table (unchanged):

mux.HandleFunc("GET /projects/{id}/materials", s.HandleMaterials)
mux.HandleFunc("GET /projects/{id}/costs", s.HandleCosts)
mux.HandleFunc("GET /projects/{id}/design", s.HandleDesign)

Regression test — verifyProjectContractRoutes wired into TestDemoSmoke (cmd/demo/main_test.go), backed by a shared E2E battery (internal/contractbattery):

func TestDemoSmoke(t *testing.T) {
    srv := httptest.NewServer(newDemoMux(api.New()))
    defer srv.Close()
    // ... healthz check ...
    verifyProjectContractRoutes(t, srv.URL) // GAP-009 regression
}

// Creates a project via POST, GETs the three contract routes, and asserts
// never-404 (registered) and never >=500 (handlers work).
func verifyProjectContractRoutes(t *testing.T, base string) {
    t.Helper()
    contractbattery.Verify(t, base)
}

The battery's core assertion (per route: /projects/{id}/materials|costs|design):

projectID := createProject(t, base) // POST /projects -> 201
// ...
if resp.StatusCode == http.StatusNotFound {
    t.Fatalf("contract route %s -> 404: route not registered on this surface", tc.path)
}
if resp.StatusCode >= http.StatusInternalServerError {
    t.Fatalf("contract route %s -> %d: handler error: %s", tc.path, resp.StatusCode, body)
}
// expected statuses: materials 200, costs 200, design 409 + error code API-ERR-051

The battery also verifies the design 409 carries api.ErrCodeDesignNotReady ("API-ERR-051") — the established project-not-ready domain behavior, distinct from a route miss. serve_test.go runs the same battery against NewFullMux to lock the full server as the parity reference.

Evidence & signatures

**1. Bug reproduced first (regression test proves it catches GAP-009).** Before the fix, `go test ./...` failed exactly as described:

```
--- FAIL: TestDemoSmoke/materials
    battery.go:51: contract route /projects/p-1/materials -> 404: route not registered on this surface
--- FAIL: TestDemoSmoke/costs
    battery.go:51: contract route /projects/p-1/costs -> 404: route not registered on this surface
--- FAIL: TestDemoSmoke/design
    battery.go:51: contract route /projects/p-1/design -> 404: route not registered on this surface
FAIL	example.com/go-demo/cmd/demo
```

Meanwhile the full server and handler packages passed — proving the drift was demo-surface route registration only.

**2. After the fix — full suite green** (`go vet ./...`, `go test -race -count=1 ./...`, `gofmt` all clean):

```
ok  example.com/go-demo               (TestFullServerContractRoutes + 3 subtests)
ok  example.com/go-demo/cmd/demo      (TestDemoSmoke + 3 subtests)
ok  example.com/go-demo/internal/api   (4 tests)
```

**3. Live probe against the running demo binary** (matching spec: materials 200, costs 200, design 409 API-ERR-051):

```
POST /projects -> {"project":{"id":"p-1","name":"live-probe","ready":false}}  201
GET /projects/p-1/materials -> [200]  {"materials":[...],"project_id":"p-1"}
GET /projects/p-1/costs     -> [200]  {"costs":[...],"project_id":"p-1"}
GET /projects/p-1/design    -> [409]  {"error":{"code":"API-ERR-051","message":"project-not-ready"}}
GET /healthz                -> [200]
```

**4. Edge cases tested.**
- *Route-miss 404 vs handler 404:* `GET /projects/nope/{materials,costs,design}` → `404 {"error":{"code":"API-ERR-404","message":"project not found"}}` — the battery flags only the unregistered-route 404 (wrong status class on an existing project); handler-level 404 for missing projects is correct domain behavior (`TestMissingProjectIs404`).
- *Design not-ready path:* 409 + `API-ERR-051` + `project-not-ready` asserted at handler level (`TestDesignNotReadyIs409`, the designs_test.go:449-equivalent assertion).
- *Design ready path:* after `MarkReady`, design → 200 (`TestDesignReadyReturns200`).
- *Materials/costs always 200 for existing project* regardless of readiness (`TestMaterialsCostsAlways200ForExistingProject`).
- *Full-server/demo parity:* the same battery passes against both `NewFullMux` and `newDemoMux`; no 5xx under `-race`.
- *Operational:* added `DEMO_ADDR` env override to `main()` so the demo can bind a free port (verified live on `:18081`).

Delivered as a git repo (`~/go-demo`, commit `a7dcd50`, 8 files): `serve.go`, `serve_test.go`, `cmd/demo/main.go`, `cmd/demo/main_test.go`, `internal/api/designs.go`, `internal/api/designs_test.go`, `internal/contractbattery/battery.go`, `go.mod`.
{"model": "deepseek-v4-flash", "problem_class": "go-demo-surface-route-registration", "result": "passed", "tests": 6}
Generated from the verified corpus · MIT licensedBack to the catalog