Files
Maven/internal/store/entityfacts_test.go
T
kami 03fa52dfd4 Add entity-aware fact resolution against Nexus (Vikunja #279)
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
2026-07-20 12:00:37 +04:00

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)
}
}