Sixteen mutating routes now refuse a caller who cannot present the token the server started with; queries stay open, because a question is not a write. The environment variable is ELY_EYE_AUTH_TOKEN, following the repo's env_prefix convention — the shorter name I reached for first returned 403 where the smoke expected 401, and the server printed the reason. Words from a stranger are quarantined on the way in: an HTTP upload is third_party by definition, and a sentence carrying an instruction-shaped phrase is quarantined whoever wrote it. A quarantined atom stays findable and stays out of the compiled context until a second, live, unquarantined source agrees with its rarest words. Corroboration is computed at read time because it is a property of the corpus now, not of the moment a memory was written; it is term-level, and the docstring says so. forget now erases the source row and the compiled contexts beside the atoms, so a forgotten memory leaves nothing that can still speak. The semantic cache key carries a store revision, which fixes a real staleness: a context compiled before new evidence arrived kept its old quarantine decision forever. bench.audit --product still pointed at evals.py, which the p1-6 split moved into backend/src/ely_eye/proofs/. It failed fast, but the anti-self-justification rule had quietly stopped auditing the product; it now walks the package, and a test pins that the package exists and holds no re-roll. Gates: pytest 306 passed; ruff clean; mypy cold cache 24 errors in 9 files, the recorded baseline, net zero; pyright 0 errors on the 23 backend files and the 2 bench files; go build/vet/test and gofmt clean; npm run build 221.39 kB with eslint exit 0; bench/filesize_check clean; bench.audit --product clean over 31 harness files; scripts/mcp_write_smoke.sh PASS with the gate live across two servers and the MCP child.
134 lines
4.2 KiB
Go
134 lines
4.2 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 TestTheWriteTokenRidesOnEveryRequest(t *testing.T) {
|
|
seen := make(chan string, 2)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seen <- r.Header.Get("Authorization")
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := New(server.URL)
|
|
client.Token = "sekret"
|
|
if err := client.Post(context.Background(), "/api/memory", map[string]any{}, &struct{}{}); err != nil {
|
|
t.Fatalf("Post failed: %v", err)
|
|
}
|
|
if err := client.Get(context.Background(), "/api/status", &struct{}{}); err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
for _, path := range []string{"POST", "GET"} {
|
|
if got := <-seen; got != "Bearer sekret" {
|
|
t.Errorf("%s Authorization = %q, want the bearer token the server demands", path, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoTokenMeansNoHeader(t *testing.T) {
|
|
seen := make(chan string, 1)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seen <- r.Header.Get("Authorization")
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := New(server.URL).Get(context.Background(), "/api/status", &struct{}{}); err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if got := <-seen; got != "" {
|
|
t.Errorf("Authorization = %q, want no header when no token is configured", got)
|
|
}
|
|
}
|
|
|
|
func TestTheTokenComesFromTheEnvironment(t *testing.T) {
|
|
t.Setenv("ELY_EYE_AUTH_TOKEN", " sekret ")
|
|
if got := New("http://127.0.0.1:8765").Token; got != "sekret" {
|
|
t.Errorf("Token = %q, want the environment's value, trimmed", got)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|