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) } } // TestPraxisEntityAttention_RemembersWhatItReadOut: the scoped digest is a list // she read out, so a positional follow-up must land on one of ITS items. It // surfaced them and remembered none, which left the previous digest live and // sent "отметь второй" at somebody else's item. func TestPraxisEntityAttention_RemembersWhatItReadOut(t *testing.T) { ctx := context.Background() nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", "Muzick indexer", "service")) scoped := fixturePraxisAttentionScoped("ent_muzick", map[string]any{"id": "item_scoped_1", "title": "indexer wedged"}) praxis := newFakePraxis(t, scoped) h := ecoHandler(t, nexus, praxis, nil) // A digest from an earlier turn, still the positional memory. h.rememberSurfaced([]string{"item_stale"}) reply := h.handlePraxisAct(ctx, router.Decision{ Intent: router.IntentAct, Slots: router.Slots{Fn: "entity_attention", HasFn: true, Value: "muzick indexer"}, }) if !strings.Contains(reply, "indexer wedged") { t.Fatalf("expected the scoped item to be read out, got %q", reply) } h.mu.Lock() surfaced := append([]string(nil), h.surfacedItems...) h.mu.Unlock() if len(surfaced) != 1 || surfaced[0] != "item_scoped_1" { t.Fatalf("scoped digest must replace the positional memory, got %v", surfaced) } // The follow-up resolves against what he just heard, not the stale list. if reply := h.handlePraxisAct(ctx, praxisItemDec("resolve_item", "last")); reply == "" { t.Fatal("positional follow-up should have been claimed by praxis") } var body string for _, r := range praxis.Requests() { if r.Method == "POST" && r.Path == "/api/v1/tools/resolve" { body = string(r.Body) } } if !strings.Contains(body, "item_scoped_1") { t.Fatalf("resolve must transition the item she read out, posted %q", body) } if strings.Contains(body, "item_stale") { t.Fatal("resolve transitioned an item from a previous digest") } } // TestHexisConfirm_KeepsOneCorrelationIDPerAction: the confirm arrives on a // later turn with a context of its own. The contract mints one id per action, // so the execution it authorises must still be joinable to the resolve and the // discovery that proposed it — it recorded a fresh id and no causation at all. func TestHexisConfirm_KeepsOneCorrelationIDPerAction(t *testing.T) { ctx := context.Background() nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", "Muzick indexer", "service")) caps := fixtureHexisCapabilities(map[string]any{"id": "cap_restart", "name": "restart", "read_only": false}) hexis := newFakeHexis(t, caps, fixtureHexisExecuted("exec_1", "succeeded")) h := ecoHandler(t, nexus, nil, hexis) if reply := h.handleHexisAct(ctx, actDec("restart")); !strings.Contains(reply, "да") { t.Fatalf("mutating capability must ask for confirmation, got %q", reply) } resolve := findTrace(t, h, "nexus", "resolve") if resolve == nil || resolve.CorrelationID == "" { t.Fatalf("expected a nexus resolve trace carrying a correlation id, got %+v", resolve) } if _, handled := h.resolveConfirm(ctx, "да"); !handled { t.Fatal("confirm should have been claimed") } exec := findTrace(t, h, "hexis", "execute") if exec == nil { t.Fatal("expected a hexis execute trace") } if exec.CausationID != resolve.CorrelationID { t.Fatalf("confirmed execution must cite the action that proposed it: causation %q, action %q", exec.CausationID, resolve.CorrelationID) } }