package main import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" hexisclient "github.com/kami/hexis/pkg/client" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/router" ) // stubEcosystem wires nexus+hexis clients at the given base URLs. func stubEcosystem(nexusURL, hexisURL string) *ecosystemWiring { return &ecosystemWiring{ nexus: newNexusClient(nexusURL), hexis: hexisclient.New(hexisURL), } } // newHexisTestHandler builds a reactiveHandler backed by fake nexus+hexis // servers. resolveBody is returned verbatim from nexus /resolve; caps is the // capability list; executed records whether Hexis /execute was called. func newHexisTestHandler(t *testing.T, resolveBody string, caps string) (*reactiveHandler, *bool) { t.Helper() executed := new(bool) nexus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(resolveBody)) })) t.Cleanup(nexus.Close) hexis := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case strings.HasPrefix(r.URL.Path, "/api/v1/capabilities"): w.Write([]byte(caps)) case r.URL.Path == "/api/v1/execute": *executed = true w.Write([]byte(`{"id":"exec_1","status":"succeeded"}`)) default: http.NotFound(w, r) } })) t.Cleanup(hexis.Close) st := newTestStore(t) now := time.Now() return &reactiveHandler{ api: ipc.NewStoreAPI(st), dataStore: st, now: func() time.Time { return now }, ecosystem: stubEcosystem(nexus.URL, hexis.URL), }, executed } func actDec(text string) router.Decision { return router.Decision{Intent: router.IntentAct, Slots: router.Slots{Text: text, Fn: "restart", HasFn: true}} } func TestHexisMutatingRequiresConfirm(t *testing.T) { ctx := context.Background() resolved := `{"status":"resolved","entity":{"id":"ent_muzick","display_name":"Muzick indexer","type":"service"}}` caps := `[{"id":"cap_restart","name":"restart","read_only":false,"risk":"high"}]` h, executed := newHexisTestHandler(t, resolved, caps) reply := h.handleHexisAct(ctx, actDec("muzick indexer")) if !strings.Contains(reply, "да") { t.Fatalf("mutating cap should ask to confirm, got %q", reply) } if *executed { t.Fatal("mutating cap must NOT execute before confirmation") } if h.pendingHexis == nil || h.pendingHexis.capabilityID != "cap_restart" { t.Fatalf("expected pending hexis bound to cap_restart, got %+v", h.pendingHexis) } // The follow-up "да" turn executes exactly the parked capability. confirmReply, handled := h.resolveConfirm(ctx, "да") if !handled || !strings.Contains(confirmReply, "выполнена") { t.Fatalf("confirm should execute, got handled=%v reply=%q", handled, confirmReply) } if !*executed { t.Fatal("confirmed mutating cap should have executed") } if h.pendingHexis != nil { t.Fatal("pending should be cleared after confirm") } } func TestHexisConfirmNoDoesNotExecute(t *testing.T) { ctx := context.Background() resolved := `{"status":"resolved","entity":{"id":"ent_muzick","display_name":"Muzick indexer","type":"service"}}` caps := `[{"id":"cap_restart","name":"restart","read_only":false}]` h, executed := newHexisTestHandler(t, resolved, caps) _ = h.handleHexisAct(ctx, actDec("muzick indexer")) reply, handled := h.resolveConfirm(ctx, "нет") if !handled || !strings.Contains(reply, "отменила") { t.Fatalf("no should cancel, got handled=%v reply=%q", handled, reply) } if *executed { t.Fatal("declined cap must not execute") } } func TestHexisReadOnlyExecutesImmediately(t *testing.T) { ctx := context.Background() resolved := `{"status":"resolved","entity":{"id":"ent_muzick","display_name":"Muzick indexer","type":"service"}}` caps := `[{"id":"cap_status","name":"restart","read_only":true}]` h, executed := newHexisTestHandler(t, resolved, caps) reply := h.handleHexisAct(ctx, actDec("muzick indexer")) if !*executed { t.Fatal("read-only cap should execute without confirmation") } if h.pendingHexis != nil { t.Fatal("read-only cap should not park a confirmation") } if !strings.Contains(reply, "выполнена") { t.Fatalf("unexpected reply %q", reply) } } func TestHexisAmbiguousAsksClarification(t *testing.T) { ctx := context.Background() ambiguous := `{"status":"ambiguous","candidates":[{"entity_id":"ent_muzick","display_name":"Muzick indexer"},{"entity_id":"ent_manga","display_name":"Manga indexer"}]}` h, executed := newHexisTestHandler(t, ambiguous, `[]`) reply := h.handleHexisAct(ctx, actDec("the indexer")) if !strings.Contains(reply, "Muzick indexer") || !strings.Contains(reply, "Manga indexer") { t.Fatalf("ambiguous should list candidates, got %q", reply) } if *executed { t.Fatal("ambiguous target must never execute") } }