Files
Maven/cmd/mavend/ecosystem_harness_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

111 lines
3.6 KiB
Go

package main
import (
"context"
"strings"
"testing"
"time"
"github.com/kami/maven/internal/ipc"
"github.com/kami/maven/internal/router"
)
// These tests exercise the fake ecosystem harness (fakeecosystem_test.go)
// directly, covering paths the ad-hoc httptest servers in ecosystem_test.go
// don't: Praxis attention (happy + degraded) and fault injection against a
// reusable fake rather than a one-off inline handler.
func praxisActDec(fn string) router.Decision {
return router.Decision{Intent: router.IntentAct, Slots: router.Slots{Fn: fn, HasFn: true}}
}
func newPraxisTestHandler(t *testing.T, praxis *fakeServer) *reactiveHandler {
t.Helper()
st := newTestStore(t)
clock := newFakeClock(time.Now())
return &reactiveHandler{
api: ipc.NewStoreAPI(st),
dataStore: st,
now: clock.Now,
ecosystem: &ecosystemWiring{praxis: newPraxisClient(praxis.URL)},
}
}
func TestPraxisAttention_HappyPathSurfacesItems(t *testing.T) {
ctx := context.Background()
items := fixturePraxisAttentionItems(map[string]any{
"id": "item_1", "title": "disk almost full", "importance": 3.0, "rule": "low_disk",
})
praxis := newFakePraxis(t, items)
h := newPraxisTestHandler(t, praxis)
reply := h.handlePraxisAct(ctx, praxisActDec("list_attention"))
if !strings.Contains(reply, "disk almost full") {
t.Fatalf("expected attention digest to mention the item, got %q", reply)
}
var sawAttention, sawSurface bool
for _, r := range praxis.Requests() {
if r.Method == "GET" && strings.HasPrefix(r.Path, "/api/v1/tools/attention") {
sawAttention = true
}
if r.Method == "POST" && r.Path == "/api/v1/tools/surface" {
sawSurface = true
}
}
if !sawAttention {
t.Error("expected a GET to /api/v1/tools/attention")
}
if !sawSurface {
t.Error("expected surfaced item to POST /api/v1/tools/surface (surfaced != acknowledged)")
}
}
// TestPraxisAttention_DegradedFailsClosedNotEmpty covers the degraded-mode
// contract: when Praxis is down, Maven must say so rather than silently
// returning nothing or panicking.
func TestPraxisAttention_DegradedFailsClosedNotEmpty(t *testing.T) {
ctx := context.Background()
praxis := newFakePraxis(t, fixturePraxisAttentionItems())
praxis.SetFault(500)
h := newPraxisTestHandler(t, praxis)
reply := h.handlePraxisAct(ctx, praxisActDec("list_attention"))
if reply == "" {
t.Fatal("praxis outage must not produce an empty reply")
}
if strings.Contains(reply, "disk almost full") {
t.Fatal("degraded reply must not fabricate item content")
}
}
// TestFakeNexus_FaultInjectionThenRecovery demonstrates the shared harness's
// fault toggle affecting the same running server, matching the shape of a
// real dependency flapping and recovering mid-session.
func TestFakeNexus_FaultInjectionThenRecovery(t *testing.T) {
ctx := context.Background()
nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", "Muzick indexer", "service"))
caps := fixtureHexisCapabilities(map[string]any{"id": "cap_status", "name": "restart", "read_only": true})
hexis := newFakeHexis(t, caps, fixtureHexisExecuted("exec_1", "succeeded"))
st := newTestStore(t)
h := &reactiveHandler{
api: ipc.NewStoreAPI(st),
dataStore: st,
now: time.Now,
ecosystem: stubEcosystem(nexus.URL, hexis.URL),
}
nexus.SetFault(503)
reply := h.handleHexisAct(ctx, actDec("muzick indexer"))
if strings.Contains(reply, "выполнена") {
t.Fatalf("nexus outage must not report success, got %q", reply)
}
nexus.SetFault(0)
reply = h.handleHexisAct(ctx, actDec("muzick indexer"))
if !strings.Contains(reply, "выполнена") {
t.Fatalf("expected success once nexus recovers, got %q", reply)
}
}