Files
Maven/cmd/mavend/fakeecosystem_test.go
T
kami c932cd8677 Add reusable fake-ecosystem test harness with fault injection
Adds fakeecosystem_test.go: a shared fakeServer wrapping httptest.Server
with request capture, a runtime-toggleable fault (SetFault) that makes a
running fake Nexus/Praxis/Hexis fail closed like a real outage without
tearing the server down, protocol fixtures for each service's documented
response shapes, and a settable fakeClock for time-dependent assertions.

Uses it in ecosystem_harness_test.go to cover a gap the existing ad-hoc
per-test httptest servers didn't reach — handlePraxisAct had zero test
coverage — plus a fault-then-recovery test showing the same fake flapping
mid-session, the shape the earlier fail-closed fixes (#272/#273) need
regression coverage against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
2026-07-20 11:33:47 +04:00

207 lines
5.9 KiB
Go

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
// capturedRequest is a recorded inbound request against a fake ecosystem
// server, kept minimal (method/path/body) since that's what tests assert on.
type capturedRequest struct {
Method string
Path string
Body []byte
}
// fakeServer is the common shell behind fakeNexus/fakePraxis/fakeHexis: an
// httptest.Server whose responses are driven by a caller-supplied route
// table, with every inbound request captured for later assertions and an
// optional fault (status code) that overrides all routing while active —
// the degraded-mode lever for these tests.
type fakeServer struct {
*httptest.Server
mu sync.Mutex
requests []capturedRequest
fault int // non-zero: every request gets this HTTP status instead of routing
}
// newFakeServer starts a server dispatching to routes keyed by "METHOD
// PATH_PREFIX" (matched with HasPrefix on path), falling back to notFound
// then to a 404 if no route matches. Route handlers are plain
// http.HandlerFuncs so tests can write arbitrary fixture responses.
func newFakeServer(t *testing.T, routes map[string]http.HandlerFunc) *fakeServer {
t.Helper()
fs := &fakeServer{}
fs.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body := make([]byte, 0)
if r.Body != nil {
buf := make([]byte, r.ContentLength)
if r.ContentLength > 0 {
n, _ := r.Body.Read(buf)
body = buf[:n]
}
}
fs.mu.Lock()
fs.requests = append(fs.requests, capturedRequest{Method: r.Method, Path: r.URL.Path, Body: body})
fault := fs.fault
fs.mu.Unlock()
if fault != 0 {
http.Error(w, "injected fault", fault)
return
}
for key, handler := range routes {
method, prefix := splitRouteKey(key)
if r.Method == method && hasPrefix(r.URL.Path, prefix) {
handler(w, r)
return
}
}
http.NotFound(w, r)
}))
t.Cleanup(fs.Server.Close)
return fs
}
func splitRouteKey(key string) (method, prefix string) {
for i := 0; i < len(key); i++ {
if key[i] == ' ' {
return key[:i], key[i+1:]
}
}
return key, ""
}
func hasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
// SetFault makes every subsequent request fail closed with the given HTTP
// status, simulating the dependency being down. Pass 0 to clear it.
func (fs *fakeServer) SetFault(status int) {
fs.mu.Lock()
defer fs.mu.Unlock()
fs.fault = status
}
// Requests returns a snapshot of captured requests, in arrival order.
func (fs *fakeServer) Requests() []capturedRequest {
fs.mu.Lock()
defer fs.mu.Unlock()
out := make([]capturedRequest, len(fs.requests))
copy(out, fs.requests)
return out
}
func jsonHandler(status int, body string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write([]byte(body))
}
}
// --- Protocol fixtures ---
// Canned response bodies matching each service's documented contract shape,
// so individual tests don't hand-roll JSON literals for the common cases.
func fixtureNexusResolved(entityID, displayName, entityType string) string {
return mustJSON(map[string]any{
"status": "resolved",
"entity": map[string]any{"id": entityID, "display_name": displayName, "type": entityType},
})
}
func fixtureNexusNotFound() string {
return `{"status":"not_found"}`
}
func fixtureNexusAmbiguous(candidates ...map[string]string) string {
cs := make([]map[string]any, len(candidates))
for i, c := range candidates {
cs[i] = map[string]any{"entity_id": c["entity_id"], "display_name": c["display_name"]}
}
return mustJSON(map[string]any{"status": "ambiguous", "candidates": cs})
}
func fixtureHexisCapabilities(caps ...map[string]any) string {
return mustJSON(caps)
}
func fixtureHexisExecuted(id, status string) string {
return mustJSON(map[string]any{"id": id, "status": status})
}
func fixturePraxisAttentionItems(items ...map[string]any) string {
return mustJSON(items)
}
func mustJSON(v any) string {
b, err := json.Marshal(v)
if err != nil {
panic(err)
}
return string(b)
}
// --- Fake clock ---
// A settable time source satisfying the `func() time.Time` shape reactiveHandler
// takes as `now`, so tests can control "now" independently of wall-clock time
// (e.g. asserting age-based digest ordering without sleeping).
type fakeClock struct {
mu sync.Mutex
t time.Time
}
func newFakeClock(start time.Time) *fakeClock {
return &fakeClock{t: start}
}
func (c *fakeClock) Now() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.t
}
func (c *fakeClock) Advance(d time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.t = c.t.Add(d)
}
// --- Convenience constructors for the three ecosystem services ---
// newFakeNexus starts a fake Nexus exposing /api/v1/resolve. resolveBody is
// returned verbatim (200) for every resolve call unless a fault is injected.
func newFakeNexus(t *testing.T, resolveBody string) *fakeServer {
return newFakeServer(t, map[string]http.HandlerFunc{
"POST /api/v1/resolve": jsonHandler(http.StatusOK, resolveBody),
})
}
// newFakePraxis starts a fake Praxis exposing the /api/v1/tools/* surface
// Maven's praxisClient calls. Every route returns its fixed body until a
// fault is injected via SetFault.
func newFakePraxis(t *testing.T, attentionBody string) *fakeServer {
return newFakeServer(t, map[string]http.HandlerFunc{
"GET /api/v1/tools/attention": jsonHandler(http.StatusOK, attentionBody),
"POST /api/v1/tools/surface": jsonHandler(http.StatusOK, `{}`),
})
}
// newFakeHexis starts a fake Hexis exposing /api/v1/capabilities and
// /api/v1/execute.
func newFakeHexis(t *testing.T, capsBody, executeBody string) *fakeServer {
return newFakeServer(t, map[string]http.HandlerFunc{
"GET /api/v1/capabilities": jsonHandler(http.StatusOK, capsBody),
"POST /api/v1/execute": jsonHandler(http.StatusOK, executeBody),
})
}