03fa52dfd4
facts gain a Subject/EntityID/ResolutionState triple and an async enrichment worker that resolves free-text subjects to canonical Nexus entity_ids, mirroring Praxis's enrichment-worker pattern. Ambiguous or unreachable Nexus never guesses an entity_id — the fact stays pending or terminal-ambiguous instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestWriteFactAboutSubject_NoSubjectStaysNone(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
id, err := s.WriteFactAboutSubject(ctx, time.Now(), KindSelf, "mood", "", `"content"`, "tap:mood", 1.0, sql.NullInt64{})
|
|
if err != nil {
|
|
t.Fatalf("WriteFactAboutSubject: %v", err)
|
|
}
|
|
|
|
pending, err := s.PendingFactResolutions(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("PendingFactResolutions: %v", err)
|
|
}
|
|
for _, f := range pending {
|
|
if f.ID == id {
|
|
t.Fatalf("fact %d written with no subject should not be queued for resolution", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteFactAboutSubject_QueuesPendingResolution(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
id, err := s.WriteFactAboutSubject(ctx, time.Now(), KindEnv, "likes", "the espresso machine", `"true"`, "infer:pref", 0.8, sql.NullInt64{})
|
|
if err != nil {
|
|
t.Fatalf("WriteFactAboutSubject: %v", err)
|
|
}
|
|
|
|
pending, err := s.PendingFactResolutions(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("PendingFactResolutions: %v", err)
|
|
}
|
|
if len(pending) != 1 || pending[0].ID != id {
|
|
t.Fatalf("expected fact %d in pending queue, got %+v", id, pending)
|
|
}
|
|
if pending[0].Subject != "the espresso machine" {
|
|
t.Fatalf("expected subject preserved, got %q", pending[0].Subject)
|
|
}
|
|
if pending[0].ResolutionState != ResolutionPending {
|
|
t.Fatalf("expected ResolutionPending, got %q", pending[0].ResolutionState)
|
|
}
|
|
}
|
|
|
|
func TestResolveFactEntity_ResolvedMakesItFindableByEntity(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
id, err := s.WriteFactAboutSubject(ctx, time.Now(), KindEnv, "likes", "the espresso machine", `"true"`, "infer:pref", 0.8, sql.NullInt64{})
|
|
if err != nil {
|
|
t.Fatalf("WriteFactAboutSubject: %v", err)
|
|
}
|
|
|
|
if err := s.ResolveFactEntity(ctx, id, "ent_espresso", ResolutionResolved); err != nil {
|
|
t.Fatalf("ResolveFactEntity: %v", err)
|
|
}
|
|
|
|
pending, err := s.PendingFactResolutions(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("PendingFactResolutions: %v", err)
|
|
}
|
|
if len(pending) != 0 {
|
|
t.Fatalf("expected resolved fact to leave the pending queue, got %+v", pending)
|
|
}
|
|
|
|
facts, err := s.FactsByEntity(ctx, "ent_espresso", 10)
|
|
if err != nil {
|
|
t.Fatalf("FactsByEntity: %v", err)
|
|
}
|
|
if len(facts) != 1 || facts[0].ID != id {
|
|
t.Fatalf("expected fact %d under ent_espresso, got %+v", id, facts)
|
|
}
|
|
if !facts[0].EntityID.Valid || facts[0].EntityID.String != "ent_espresso" {
|
|
t.Fatalf("expected EntityID set, got %+v", facts[0].EntityID)
|
|
}
|
|
}
|
|
|
|
// TestResolveFactEntity_AmbiguousDoesNotStoreAnEntityID covers the
|
|
// ecosystem-wide invariant that ambiguity blocks mutation: an ambiguous
|
|
// resolve must never guess an entity_id, even transiently.
|
|
func TestResolveFactEntity_AmbiguousDoesNotStoreAnEntityID(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
id, err := s.WriteFactAboutSubject(ctx, time.Now(), KindEnv, "likes", "kate", `"true"`, "infer:pref", 0.8, sql.NullInt64{})
|
|
if err != nil {
|
|
t.Fatalf("WriteFactAboutSubject: %v", err)
|
|
}
|
|
|
|
// Simulate an ambiguous Nexus response: candidates found, but no single
|
|
// entity_id — the enrichment worker passes "" for entityID in this case.
|
|
if err := s.ResolveFactEntity(ctx, id, "", ResolutionAmbiguous); err != nil {
|
|
t.Fatalf("ResolveFactEntity: %v", err)
|
|
}
|
|
|
|
pending, err := s.PendingFactResolutions(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("PendingFactResolutions: %v", err)
|
|
}
|
|
if len(pending) != 0 {
|
|
t.Fatalf("ambiguous fact should leave the pending queue (it's terminal), got %+v", pending)
|
|
}
|
|
}
|