go-demo-route-wiring
Root cause (IM-GAP-010): cmd/demo/main.go had three defects vs. the full server:
1. The two BOQ routes (GET /boq, GET /boq/history) that serve.go:104-106 registers were never registered, so requests fell through to the SPA shell (HTML).
2. api.SetBOQStore was never called, so the package-level BOQSt stayed nil and any BOQ handler would 503 with BOQ-ERR-001.
3. No demo data was seeded, so even a wired store would 404.
Fix — three parts:
1. internal/store/memory/boq_store.go (new): mutex-guarded map[projectID][]BOQ kept newest-first (index 0 = latest).
var ErrBOQNotFound = errors.New("boq: not found")
type BOQStore struct {
mu sync.Mutex
byProject map[string][]BOQ // newest-first per project
}
func (s *BOQStore) SaveBOQ(b BOQ) error { // dedupes same revision, prepends
s.mu.Lock(); defer s.mu.Unlock()
revs := s.byProject[b.ProjectID]
filtered := revs[:0]
for _, e := range revs { if e.Revision != b.Revision { filtered = append(filtered, e) } }
s.byProject[b.ProjectID] = append([]BOQ{b}, filtered...)
return nil
}
func (s *BOQStore) GetBOQ(projectID string, revision int) (BOQ, error) {
// revision==0 -> revs[0] (O(1) newest); else linear revision match
// unknown -> ErrBOQNotFound
}
func (s *BOQStore) ListBOQs(projectID string) ([]BOQ, error) { /* defensive copy */ }
func (s *BOQStore) SeedDemo(projectID string) { // idempotent: one seeded revision
s.SaveBOQ(BOQ{ProjectID: projectID, Revision: 1, Lines: []BOQLine{...3 demo lines...}})
}
2. cmd/demo/server.go (the wiring gap fix, factored out of main for testability):
func newHandler() http.Handler {
boqStore := memory.NewBOQStore()
boqStore.SeedDemo(demoProjectID) // fix part 2: GET /boq returns 200
api.SetBOQStore(boqStore) // fix part 1: no more nil store (503)
mux := http.NewServeMux()
// fix part 3: mirror serve.go:104-106, registered BEFORE the SPA fallback
mux.HandleFunc("GET /boq", api.HandleBOQ)
mux.HandleFunc("GET /boq/history", api.HandleBOQHistory)
mux.HandleFunc("GET /healthz", ...)
mux.HandleFunc("/", spaFallback) // catch-all, last
return mux
}
3. Handlers (internal/api/boq.go, pre-existing per the report) were kept and now receive a live store. Errors are RFC 7807 application/problem+json with machine-readable codes: BOQ-ERR-001 (503, nil store), BOQ-ERR-002 (400, missing projectId), BOQ-ERR-003 (400, bad revision), BOQ-ERR-004 (404, not found), BOQ-ERR-005 (500, store error). cmd/demo/verify_test.go implements verifyBOQRoutes as the regression guard.
Reconstructed the codebase (`go 1.26`), applied the fix, and verified:
- **`go build ./... && go vet ./... && gofmt -l .`** — clean.
- **`go test -race -count=1 ./...`** — **10/10 tests pass** (6 `verifyBOQRoutes` subtests + nil-store 503 API test + wired-store API test + 7 store unit tests incl. concurrent-save race check).
- **Live server on a free port** (`:19090`; sandbox's `:8080`/`:18080` are pre-bound by a gateway):
- `GET /boq?projectId=demo-project` → **200** `application/json` with 3 lines (seeded revision 1)
- `GET /boq/history?projectId=demo-project` → **200** JSON array, newest-first
- `GET /boq?projectId=nope` → **404** `application/problem+json` `{"code":"BOQ-ERR-004",...}`
- `GET /boq` (no projectId) → **400** `BOQ-ERR-002`; `revision=abc` → **400** `BOQ-ERR-003`; `revision=99` → **404** `BOQ-ERR-004`
- `GET /unknown-path` → **200** `text/html` SPA shell — fallback only for non-API paths; no `/boq` path ever returns HTML.
- **Regression proof:** simulated the pre-fix state (no routes, no `SetBOQStore`, no seed) — `verifyBOQRoutes` fails all 6 subtests with `got SPA fallback ... — /boq route not wired`; restored, the suite is green again. `TestNilStoreIs503` documents the other pre-fix symptom (nil store → 503 `BOQ-ERR-001`).
- **Edge cases covered:** newest-first ordering, exact-revision lookup, `errors.Is(ErrBOQNotFound)` sentinel, defensive copy from `ListBOQs`, revision dedupe, idempotent `SeedDemo` (no cross-project leakage), concurrent saves under `-race`.{"model": "deepseek-v4-flash", "problem_class": "go-demo-route-wiring", "result": "passed", "tests": 10}