Wire hexis.resolve_target to real Nexus, fix changes cursor, pass full execute fields over MCP

resolve_target previously returned a hardcoded "requires_nexus_resolution"
placeholder; it now calls Nexus's /api/v1/resolve via a new minimal
internal/nexusclient, configurable with -nexus (default localhost:8987).

/api/v1/changes ignored the since query param and always returned from
sequence 0 (`since = 0` regardless of what was parsed) — fixed to actually
parse and use it, so change-cursor polling works.

The MCP hexis.execute tool only forwarded capability_id/target_entity_id/
arguments/idempotency_key, silently dropping entity_version, requested_by,
origin, correlation_id, causation_id, resolution_evidence, and
confirmation_id even though the native HTTP API and domain.ExecuteRequest
already supported all of them — MCP callers now get full parity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
This commit is contained in:
kami
2026-07-20 11:28:13 +04:00
parent ed593efb71
commit 74b19e091e
5 changed files with 262 additions and 13 deletions
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
@@ -334,7 +335,12 @@ func (h *Handler) handleChanges(w http.ResponseWriter, r *http.Request) {
seqStr := r.URL.Query().Get("since")
var since int64
if seqStr != "" {
since = 0
parsed, err := strconv.ParseInt(seqStr, 10, 64)
if err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse("since must be an integer sequence"))
return
}
since = parsed
}
events, err := h.store.EventsAfter(since, 100)
+80
View File
@@ -0,0 +1,80 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/kami/hexis/internal/domain"
"github.com/kami/hexis/internal/execution"
"github.com/kami/hexis/internal/provider"
"github.com/kami/hexis/internal/storage"
)
func newTestHandler(t *testing.T) (*Handler, *storage.Store) {
t.Helper()
path := filepath.Join(t.TempDir(), "hexis.db")
store, err := storage.Open(path)
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() { store.Close() })
engine := execution.New(store, provider.NewRegistry())
return NewHandler(store, engine), store
}
// TestHandleChanges_SinceFiltersEvents verifies the `since` query param
// actually filters to events after that sequence number, rather than being
// silently reset to 0 (the bug: `since = 0` unconditionally, ignoring the
// parsed value).
func TestHandleChanges_SinceFiltersEvents(t *testing.T) {
h, store := newTestHandler(t)
for i := 0; i < 3; i++ {
if err := store.AppendEvent(&domain.Event{
ID: domain.NewEventID(),
Type: domain.EventCapabilityRegistered,
Timestamp: time.Now().UTC(),
}); err != nil {
t.Fatalf("append event %d: %v", i, err)
}
}
all, err := store.EventsAfter(0, 100)
if err != nil {
t.Fatalf("events after 0: %v", err)
}
if len(all) != 3 {
t.Fatalf("expected 3 seed events, got %d", len(all))
}
cutoff := all[0].Sequence
req := httptest.NewRequest(http.MethodGet, "/api/v1/changes?since="+itoa(cutoff), nil)
w := httptest.NewRecorder()
h.handleChanges(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var events []*domain.Event
if err := json.Unmarshal(w.Body.Bytes(), &events); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(events) != 2 {
t.Fatalf("expected 2 events after sequence %d, got %d", cutoff, len(events))
}
for _, e := range events {
if e.Sequence <= cutoff {
t.Errorf("event with sequence %d should have been excluded by since=%d", e.Sequence, cutoff)
}
}
}
func itoa(n int64) string {
b, _ := json.Marshal(n)
return string(b)
}