Files
ZacharyZhang-NY eb50367921
ci / backend (push) Failing after 3m25s
ci / mcp (push) Successful in 21s
ci / interface (push) Successful in 19s
ci / image (push) Failing after 1m5s
Move the canonical host to git.zacharyzhang.com and follow the module path
The GitHub account that hosted this repository is suspended, so the remote
is dead and the vanity import github.com/ZacharyZhang-NY/ely-eye/... no
longer resolves. The Go module path is that import, so it moves with the
host: git.zacharyzhang.com serves a go-import meta tag for the nested
ely-eye-mcp submodule, which go build and go get both need.

The installers read release assets from the host too, so their API and
download URLs move with it. The release workflow's -ldflags symbol path is
the module path, so it moves as well; a build stamped with the new path
prints the stamped version.
2026-10-10 11:26:19 -04:00

134 lines
5.1 KiB
Go

package mcpserver
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.zacharyzhang.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/apiclient"
)
func directoryServer(t *testing.T, body string, seen *[]string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
*seen = append(*seen, r.Method+" "+r.URL.Path+" "+string(payload))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(body))
}))
}
func TestWhoKnowsNamesTheHolderAndKeepsTheEvidence(t *testing.T) {
var seen []string
server := directoryServer(t, `{"query":"钥匙","as_holder":"husband","status":"found","hits":[{
"holder_id":"wife","name":"小美","confidence":0.91,"matched":2,"top_score":0.02,
"basis":["wife holds 2 matching memories the asker may not read"],"contact":{"channel":"signal"}}]}`, &seen)
defer server.Close()
_, out, err := (&Server{api: apiclient.New(server.URL)}).whoKnows(
context.Background(), nil, WhoKnowsInput{Query: "钥匙", AsHolder: "husband"})
if err != nil {
t.Fatalf("who_knows failed: %v", err)
}
if out.Report.Status != "found" || len(out.Report.Hits) != 1 {
t.Fatalf("report = %+v, want one found candidate", out.Report)
}
hit := out.Report.Hits[0]
if hit.HolderID != "wife" || hit.Confidence != 0.91 || hit.Contact["channel"] != "signal" {
t.Errorf("hit = %+v, want the holder, her confidence, and how to reach her", hit)
}
if len(seen) != 1 || !strings.Contains(seen[0], "POST /api/who_knows") || !strings.Contains(seen[0], `"query":"钥匙"`) {
t.Fatalf("requests = %v, want one POST carrying the query", seen)
}
}
func TestWhoKnowsRefusesAnEmptyQueryBeforeTheNetwork(t *testing.T) {
var seen []string
server := directoryServer(t, `{}`, &seen)
defer server.Close()
if _, _, err := (&Server{api: apiclient.New(server.URL)}).whoKnows(context.Background(), nil, WhoKnowsInput{}); err == nil {
t.Fatal("an empty query must not reach the directory")
}
if len(seen) != 0 {
t.Errorf("requests = %v, want none", seen)
}
}
func TestAskReturnsThePointerAndNotThePrivateText(t *testing.T) {
var seen []string
server := directoryServer(t, `{"question":"钥匙在哪里","as_holder":"husband","status":"referred",
"answered_by":"wife","asked":["husband","wife"],"hops":1,"atom_ids":[],"basis":["wife holds 1 matching memories the asker may not read"]}`, &seen)
defer server.Close()
_, out, err := (&Server{api: apiclient.New(server.URL)}).askMemory(
context.Background(), nil, AskInput{Question: "钥匙在哪里", AsHolder: "husband", MaxHops: 2})
if err != nil {
t.Fatalf("ask failed: %v", err)
}
if out.Answer.Status != "referred" || out.Answer.Answer != "" {
t.Errorf("answer = %+v, want a referral with no text", out.Answer)
}
if out.Answer.AnsweredBy != "wife" || out.Answer.Hops != 1 {
t.Errorf("answer = %+v, want wife at hop 1", out.Answer)
}
if !strings.Contains(seen[0], `"max_hops":2`) || !strings.Contains(seen[0], `"as_holder":"husband"`) {
t.Errorf("request = %s, want the hop budget and the asker", seen[0])
}
}
func TestAskWithoutAnAskerOmitsTheIdentity(t *testing.T) {
var seen []string
server := directoryServer(t, `{"question":"garage code","status":"no_holder_knows","asked":[],"hops":0}`, &seen)
defer server.Close()
if _, _, err := (&Server{api: apiclient.New(server.URL)}).askMemory(context.Background(), nil, AskInput{Question: "garage code"}); err != nil {
t.Fatalf("ask failed: %v", err)
}
var body map[string]any
parts := strings.SplitN(seen[0], " ", 3)
if err := json.Unmarshal([]byte(parts[2]), &body); err != nil {
t.Fatalf("request body is not JSON: %s", seen[0])
}
if _, present := body["as_holder"]; present {
t.Errorf("body = %v, want as_holder omitted so the server applies its own default", body)
}
if _, present := body["max_hops"]; present {
t.Errorf("body = %v, want max_hops omitted; a Go zero is not a hop budget", body)
}
}
func TestAskRefusesAnEmptyQuestion(t *testing.T) {
var seen []string
server := directoryServer(t, `{}`, &seen)
defer server.Close()
if _, _, err := (&Server{api: apiclient.New(server.URL)}).askMemory(context.Background(), nil, AskInput{}); err == nil {
t.Fatal("an empty question must not be asked")
}
if len(seen) != 0 {
t.Errorf("requests = %v, want none", seen)
}
}
func TestListHoldersReadsTheRegistry(t *testing.T) {
var seen []string
server := directoryServer(t, `[{"holder_id":"wife","name":"小美","kind":"person","knows_about":["家里的钥匙"],"contact":{"channel":"signal"},"created_at":"2026-05-01T00:00:00+00:00","updated_at":"2026-05-01T00:00:00+00:00"}]`, &seen)
defer server.Close()
_, out, err := (&Server{api: apiclient.New(server.URL)}).listHolders(context.Background(), nil, struct{}{})
if err != nil {
t.Fatalf("list_holders failed: %v", err)
}
if len(out.Holders) != 1 || out.Holders[0].KnowsAbout[0] != "家里的钥匙" {
t.Fatalf("holders = %+v, want the declared topics", out.Holders)
}
if len(seen) != 1 || !strings.HasPrefix(seen[0], "GET /api/holders") {
t.Errorf("requests = %v, want one GET", seen)
}
}