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