Files
Maven/cmd/mavend/ecosystem_harness_test.go
T
claude 6a9d8a4dd5 mavend: name the service that is down, and never read an empty list (V-521)
Two caller-side halves of the same review.

«экосистема недоступна» named nothing. Nexus, Praxis and Hexis fail
independently, and every one of the six call sites already knew which one it was
talking to — it writes that name into the trace on the line above. So eco_down
and eco_denied now take {name}, and he hears which service refused him.

The list entries are single-variant and placeholder-only, so an empty list has
no shorter wording to fall back on: attention_list would render as its own label
and a colon. Both Praxis readers checked the response length and neither checked
what survived formatting, so an item with no title counted toward a list it
could not appear in. They skip the untitled item and fall to the _none entry
when nothing is left.

The ecosystem tests asserted the substring "выполнена", which was a literal out
of the act file that review has now reworded. Seventeen sites go through actRan,
which asks the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
2026-08-04 16:00:17 +04:00

118 lines
3.9 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}}
}
// praxisItemDec is praxisActDec for the lifecycle verbs, which need an item id
// in the value slot. Without one they answer "which item?" and never reach
// Praxis at all, which makes them useless for testing a Praxis outage.
func praxisItemDec(fn, itemID string) router.Decision {
return router.Decision{Intent: router.IntentAct, Slots: router.Slots{Fn: fn, HasFn: true, Value: itemID}}
}
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 actRan(reply) {
t.Fatalf("nexus outage must not report success, got %q", reply)
}
nexus.SetFault(0)
reply = h.handleHexisAct(ctx, actDec("muzick indexer"))
if !actRan(reply) {
t.Fatalf("expected success once nexus recovers, got %q", reply)
}
}