This commit is contained in:
2026-09-25 09:53:16 +05:30
parent 00317a00d8
commit db84a9a752
6 changed files with 317 additions and 1 deletions

View File

@@ -0,0 +1,103 @@
package controllers
import (
"net/http"
"runtime/debug"
"strings"
"nearle/services"
"github.com/gofiber/fiber/v2"
)
// What is running here, and is it wired up?
//
// ── Why this exists ─────────────────────────────────────────────────────────
//
// On 2026-09-24 the assistant sat switched off in production for most of a day,
// and neither of us could establish WHY from outside the container. Two
// questions had no answer:
//
// 1. which build is deployed? A redeploy can reuse a cached image, so
// "I pushed it" and "it is running" are different facts.
// 2. does the server have a model? `/assistant/status` knows, but it sits
// behind the session guard, and a 401 from `/v1/web` proves nothing —
// the middleware answers before routing, so a route that does not exist
// returns exactly the same 401 as one that does.
//
// Every diagnosis that day was guesswork for want of one request. Hours went
// into probing CORS headers and comparing nginx versions to infer a commit,
// which is what people do when a server will not simply say.
//
// ── What it deliberately does not say ───────────────────────────────────────
//
// Booleans, never values. "The assistant has a model" is operational; WHICH
// model, at which endpoint, under which key is not, and the reason string on
// `/assistant/status` names environment variables — that stays behind the
// guard. Nothing here distinguishes a tenant, so there is nothing to scope.
//
// Unauthenticated on purpose. A health check that needs a credential cannot be
// used by the person trying to work out why credentials are not working, and
// that is precisely when it is wanted.
type HealthController struct {
assistant services.AssistantService
// hasDatabase is a construction-time fact, not a live ping. A query per
// health check is a query per uptime probe, and "configured" is the thing
// that actually differs between a broken deployment and a working one.
hasDatabase bool
}
func NewHealthController(assistant services.AssistantService, hasDatabase bool) *HealthController {
return &HealthController{assistant: assistant, hasDatabase: hasDatabase}
}
// Version is stamped at build time:
//
// go build -ldflags "-X nearle/controllers.Version=$(git rev-parse --short HEAD)"
//
// Left as "unknown" when nothing stamps it, which is honest — and itself worth
// seeing, because it means the image was not built by the pipeline that does.
var Version = "unknown"
// buildVersion falls back to whatever the toolchain recorded.
//
// `debug.ReadBuildInfo` carries the VCS revision for a build made inside a git
// checkout, so even an image built by hand usually knows its own commit. The
// ldflag is preferred because a Docker build copies the tree without `.git`.
func buildVersion() string {
if Version != "unknown" && strings.TrimSpace(Version) != "" {
return Version
}
info, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
}
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" && setting.Value != "" {
if len(setting.Value) > 7 {
return setting.Value[:7]
}
return setting.Value
}
}
return "unknown"
}
func (ctl *HealthController) Health(c *fiber.Ctx) error {
assistant := false
if ctl.assistant != nil {
assistant = ctl.assistant.Available()
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK, "status": true, "message": "Success",
"details": fiber.Map{
"version": buildVersion(),
// True when a model is configured and the assistant can answer. False
// is the answer to "I set the key and redeployed, did it take?" —
// which took a day to establish without it.
"assistant": assistant,
"database": ctl.hasDatabase,
},
})
}

165
controllers/health_test.go Normal file
View File

@@ -0,0 +1,165 @@
package controllers
import (
"context"
"encoding/json"
"io"
"net/http/httptest"
"strings"
"testing"
"nearle/services"
"nearle/services/tools"
"github.com/gofiber/fiber/v2"
)
/*
A server that can say what it is.
This exists because of a day spent unable to answer two questions about a
running deployment: which build is it, and does the assistant have a model. Both
were knowable inside the container and neither was reachable from outside —
`/assistant/status` sits behind the session guard, and a 401 from `/v1/web`
proves nothing, because the middleware answers before routing and a route that
does not exist returns the same 401 as one that does.
So the tests that matter here are about what it answers WITHOUT a session, and
about what it refuses to include.
*/
func healthApp(t *testing.T, assistantReady bool, hasDatabase bool) *fiber.App {
t.Helper()
app := fiber.New()
controller := NewHealthController(stubAssistant{ready: assistantReady}, hasDatabase)
app.Get("/live/api/v1/health", controller.Health)
return app
}
func readHealth(t *testing.T, app *fiber.App) (int, map[string]any, string) {
t.Helper()
resp, err := app.Test(httptest.NewRequest("GET", "/live/api/v1/health", nil), -1)
if err != nil {
t.Fatalf("health: %v", err)
}
raw, _ := io.ReadAll(resp.Body)
var envelope struct {
Details map[string]any `json:"details"`
}
if err := json.Unmarshal(raw, &envelope); err != nil {
t.Fatalf("not the envelope the console unwraps: %s", raw)
}
return resp.StatusCode, envelope.Details, string(raw)
}
func TestHealthAnswersWithoutASession(t *testing.T) {
// The point. A health check that needs a credential cannot be used by the
// person working out why credentials are not working — which is exactly
// when somebody reaches for it.
status, details, body := readHealth(t, healthApp(t, true, true))
if status != fiber.StatusOK {
t.Fatalf("HTTP %d without a session: %s", status, body)
}
if details["version"] == nil {
t.Fatalf("no build id: %s", body)
}
}
func TestHealthSaysWhetherTheAssistantHasAModel(t *testing.T) {
// "I set the key and redeployed — did it take?" took a day to answer. This
// is that answer, in one unauthenticated request.
_, ready, _ := readHealth(t, healthApp(t, true, true))
if ready["assistant"] != true {
t.Fatalf("a configured assistant reported as %v", ready["assistant"])
}
_, off, body := readHealth(t, healthApp(t, false, true))
if off["assistant"] != false {
t.Fatalf("an unconfigured assistant reported as %v: %s", off["assistant"], body)
}
}
func TestHealthNeverLeaksTheConfiguration(t *testing.T) {
// Booleans, never values. WHICH model, at which endpoint, under which key is
// not operational information, and the `reason` string on /assistant/status
// names environment variables — that stays behind the guard.
_, _, body := readHealth(t, healthApp(t, false, true))
for _, secret := range []string{
"ASSISTANT_", "api.groq.com", "gsk_", "openai/gpt-oss", "POS_TOKEN", "password",
} {
if strings.Contains(strings.ToLower(body), strings.ToLower(secret)) {
t.Fatalf("%q is exposed on an unauthenticated endpoint: %s", secret, body)
}
}
}
func TestHealthSurvivesAServerWithNothingWiredUp(t *testing.T) {
// A deployment with no database and no model must still ANSWER. This is the
// state in which somebody is most likely to ask, and a 500 here would leave
// them exactly where they started.
app := fiber.New()
app.Get("/live/api/v1/health", NewHealthController(nil, false).Health)
resp, err := app.Test(httptest.NewRequest("GET", "/live/api/v1/health", nil), -1)
if err != nil {
t.Fatalf("health: %v", err)
}
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("a bare server could not report its own health: HTTP %d", resp.StatusCode)
}
raw, _ := io.ReadAll(resp.Body)
var envelope struct {
Details map[string]any `json:"details"`
}
_ = json.Unmarshal(raw, &envelope)
if envelope.Details["assistant"] != false || envelope.Details["database"] != false {
t.Fatalf("a bare server claimed to be wired up: %s", raw)
}
}
func TestAnUnstampedBuildSaysSoRatherThanGuessing(t *testing.T) {
// "unknown" is informative: it means nothing stamped the image, so the
// version cannot be trusted to date it. Inventing one would be worse than
// admitting it.
original := Version
Version = "unknown"
defer func() { Version = original }()
got := buildVersion()
// Either the toolchain recorded a revision, or it says unknown. What it must
// not do is return an empty string, which renders as a blank field and reads
// like the endpoint is broken.
if strings.TrimSpace(got) == "" {
t.Fatal("the build id is blank")
}
}
func TestAStampedBuildIsReported(t *testing.T) {
original := Version
Version = "abc1234"
defer func() { Version = original }()
_, details, body := readHealth(t, healthApp(t, true, true))
if details["version"] != "abc1234" {
t.Fatalf("the stamped build id was not reported: %s", body)
}
}
// stubAssistant is only ever asked one question.
type stubAssistant struct{ ready bool }
func (s stubAssistant) Available() bool { return s.ready }
func (s stubAssistant) Unavailable() string { return "" }
func (s stubAssistant) Ask(_ context.Context, _, _ string, _ tools.Caller) (services.AssistantAnswer, error) {
return services.AssistantAnswer{}, nil
}
func (s stubAssistant) Approve(_ context.Context, _, _ string, _ tools.Caller) (services.AssistantAnswer, error) {
return services.AssistantAnswer{}, nil
}