go-api-spec-gap-wiring
The gap: spec 01-core §6.2 promised GET /api/v1/pools/:appID/status served by sandbox.StatusHandler, but no handler and no route existed. The domain primitive already existed and is reused unchanged: SandboxManager.PoolStatus(appID) (PoolStatus, error) plus the PoolStatus JSON type (with ErrPoolNotFound sentinel). The fix is pure wiring, zero reimplementation.
1. New sandbox/status_handler.go — status mapping 200/404 POOL_NOT_FOUND/400/500, router-style {"error":{"code","message"}} envelope:
var appIDRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`)
// StatusHandler serves GET /api/v1/pools/{appID}/status (spec 01-core §6.2).
// 200 OK (PoolStatus JSON) | 400 BAD_REQUEST | 404 POOL_NOT_FOUND | 500 INTERNAL_ERROR
func StatusHandler(mgr SandboxManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
appID := r.PathValue("appID")
if appID == "" || !appIDRe.MatchString(appID) {
writeError(w, http.StatusBadRequest, "BAD_REQUEST",
"path parameter appID must be a non-empty alphanumeric identifier")
return
}
status, err := mgr.PoolStatus(appID) // existing primitive, untouched
if err != nil {
switch {
case errors.Is(err, ErrPoolNotFound):
writeError(w, http.StatusNotFound, "POOL_NOT_FOUND",
fmt.Sprintf("no pool exists for appID %q", appID))
default:
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR",
"failed to read pool status") // backend detail never leaked
}
return
}
writeJSON(w, http.StatusOK, status)
})
}
// Router-style error envelope shared with the rest of the API.
type errorEnvelope struct{ Error errorBody `json:"error"` }
type errorBody struct {
Code string `json:"code"`
Message string `json:"message"`
}
func writeError(w http.ResponseWriter, code int, errCode, message string) {
writeJSON(w, code, errorEnvelope{Error: errorBody{Code: errCode, Message: message}})
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
2. The one wiring line in framework.go (Go 1.22+ method-pattern ServeMux; {appID} populates r.PathValue):
mux := http.NewServeMux()
// U-GAP-005 fix: spec 01-core §6.2 promised this route; it was never wired.
mux.Handle("GET /api/v1/pools/{appID}/status", sandbox.StatusHandler(mgr))
// Existing route: /execute runs GetOrCreatePool before Acquire.
mux.Handle("POST /api/v1/execute", sandbox.ExecuteHandler(mgr))
The /execute handler already guarantees GetOrCreatePool runs before Acquire — which is exactly what makes the live E2E (below) work: the pool a request creates via /execute is immediately visible to the status route.
3. httptest fake-manager tests (sandbox/status_handler_test.go) — a fakeManager implements SandboxManager with injectable statusErr/createErr/acquireErr so the handler is tested against a controllable boundary, plus one live E2E test against the real in-memory manager through the exact framework.go wiring.
**Automated tests — 7/7 PASS** (`go test ./sandbox/ -count=1 -race -v`):
| # | Test | Verifies |
|---|------|----------|
| 1 | `TestStatusHandlerOK` | 200, `Content-Type: application/json`, body round-trips as `PoolStatus` with exact fields (Available=2, InUse=2, Queued=1) |
| 2 | `TestStatusHandlerPoolNotFound` | unknown pool → **404**, envelope code `POOL_NOT_FOUND` (not a silent zero-value 200) |
| 3 | `TestStatusHandlerBadRequestEmptyAppID` | empty path segment → **400** `BAD_REQUEST`, manager never called |
| 4 | `TestStatusHandlerBadRequestInvalidAppID` | appID `"a b"` through the real mux → **400** `BAD_REQUEST` |
| 5 | `TestStatusHandlerBackendError` | manager returns `"backend down"` → **500** `INTERNAL_ERROR`, detail not leaked to client |
| 6 | `TestStatusHandlerEnvelopeShape` | error body is exactly `{"error":{"code","message"}}` — one top-level key, router style |
| 7 | `TestExecuteCreatesPoolThenStatusE2E` | live E2E: `/execute` → 201 with sandbox id; status of `e2e-app` → 200 with `available:3, inUse:1` (proves GetOrCreatePool ran before Acquire); bad execute → 400 and never-created pool stays 404 |
`go vet ./...` clean; `go build ./...` OK.
**Live server smoke test** (real binary, curl; ran on :18081 because :8080 is occupied by environment infra):
```
GET /api/v1/pools/ghost/status → {"error":{"code":"POOL_NOT_FOUND","message":"no pool exists for appID \"ghost\""}} HTTP 404
POST /api/v1/execute {"appID":"live-app"} → {"id":"sbx-001","appID":"live-app"} HTTP 201
GET /api/v1/pools/live-app/status → {"appID":"live-app","poolSize":4,"available":3,"inUse":1,"queued":0,"capacity":4,...} HTTP 200
GET /api/v1/pools/a%20b/status → {"error":{"code":"BAD_REQUEST","message":"path parameter appID must be a non-empty alphanumeric identifier"}} HTTP 400
```
**Edge cases covered:** unknown pool vs. zero-value PoolStatus (404, never a false 200); empty/malformed appID (400 before reaching the manager); non-`ErrPoolNotFound` backend failure (500 with no detail leak); envelope shape on every error; content type on success and error; pool creation only happens for valid `/execute` requests; concurrency-safe in-memory manager (race detector clean).
---{"model": "deepseek-v4-flash", "problem_class": "go-api-spec-gap-wiring", "result": "passed", "tests": 7}