remember / supersede / forget now reach the one Python writer from a terminal, from MCP, and from HTTP. The server, not the caller, decides who owns a memory. - cli: remember, supersede, forget. --holder falls back to settings.default_holder until the holder registry lands. Timestamps are parsed once with one clear message. - ingestion: import pymupdf as fitz. The deprecated fitz import printed a warning to stdout, so every CLI command's JSON answer was unparseable. - mcp: remember, supersede_memory and forget_memory forward to the Python writer over HTTP (ELY_EYE_API, default http://127.0.0.1:8765). SQLite stays read-only to the Go side, so the write rules live in one place. - mcp reads follow schema v2: atoms.time became ingested_at, and reads go through v_atoms_current, so a superseded memory stops being findable. holder, origin, scenario and event_time now show up in fetch_atom and search_atoms. - scripts/mcp_write_smoke.sh proves the loop end to end: remember, search, supersede, forget, then not found. Evidence: 92 pytest passed; go build, go vet and go test clean; mypy clean on the 10 touched source files with a fresh cache; ruff clean except a pre-existing B008 at cli.py:292, identical at HEAD.
88 lines
2.7 KiB
Go
88 lines
2.7 KiB
Go
package apiclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPostSendsJSONAndDecodesTheAnswer(t *testing.T) {
|
|
var gotPath string
|
|
var gotBody map[string]any
|
|
var gotType string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotType = r.Header.Get("Content-Type")
|
|
body, _ := io.ReadAll(r.Body)
|
|
_ = json.Unmarshal(body, &gotBody)
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = w.Write([]byte(`{"atom_id":"mem-1:text","holder_id":"wife"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
var out struct {
|
|
AtomID string `json:"atom_id"`
|
|
HolderID string `json:"holder_id"`
|
|
}
|
|
client := New(server.URL)
|
|
err := client.Post(context.Background(), "/api/memory", map[string]any{"text": "钥匙在玄关"}, &out)
|
|
if err != nil {
|
|
t.Fatalf("Post failed: %v", err)
|
|
}
|
|
if gotPath != "/api/memory" {
|
|
t.Errorf("path = %q, want /api/memory", gotPath)
|
|
}
|
|
if gotType != "application/json" {
|
|
t.Errorf("content type = %q, want application/json", gotType)
|
|
}
|
|
if gotBody["text"] != "钥匙在玄关" {
|
|
t.Errorf("body = %v, want the statement to survive the trip", gotBody)
|
|
}
|
|
if out.AtomID != "mem-1:text" || out.HolderID != "wife" {
|
|
t.Errorf("decoded = %+v, want the server's stamped atom", out)
|
|
}
|
|
}
|
|
|
|
func TestRejectedWriteCarriesTheServersReason(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
_, _ = w.Write([]byte(`{"detail":"remember needs a holder; a memory nobody owns cannot be referred to"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
err := New(server.URL).Post(context.Background(), "/api/memory", map[string]any{}, &struct{}{})
|
|
if err == nil {
|
|
t.Fatal("a rejected write must not look like success")
|
|
}
|
|
if !strings.Contains(err.Error(), "remember needs a holder") {
|
|
t.Errorf("error = %v, want the rule the caller broke", err)
|
|
}
|
|
}
|
|
|
|
func TestUnreachableAPIPointsAtTheServerThatShouldBeRunning(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
|
url := server.URL
|
|
server.Close()
|
|
|
|
err := New(url).Post(context.Background(), "/api/memory", map[string]any{}, &struct{}{})
|
|
if err == nil {
|
|
t.Fatal("an unreachable API must fail")
|
|
}
|
|
if !strings.Contains(err.Error(), "ely-eye serve") {
|
|
t.Errorf("error = %v, want the fix the operator can run", err)
|
|
}
|
|
}
|
|
|
|
func TestNewDefaultsToLocalServe(t *testing.T) {
|
|
if got := New("").BaseURL; got != "http://127.0.0.1:8765" {
|
|
t.Errorf("BaseURL = %q, want the default serve address", got)
|
|
}
|
|
if got := New("http://127.0.0.1:9000/").BaseURL; got != "http://127.0.0.1:9000" {
|
|
t.Errorf("BaseURL = %q, want the trailing slash trimmed", got)
|
|
}
|
|
}
|