package main import ( "context" "database/sql" "testing" "time" "github.com/kami/maven/internal/store" ) func TestFactEnrichmentWorker_ResolvesPendingSubject(t *testing.T) { ctx := context.Background() nexus := newFakeNexus(t, fixtureNexusResolved("ent_espresso", "the espresso machine", "device")) st := newTestStore(t) id, err := st.WriteFactAboutSubject(ctx, time.Now(), store.KindEnv, "likes", "the espresso machine", `"true"`, "infer:pref", 0.8, sql.NullInt64{}) if err != nil { t.Fatalf("WriteFactAboutSubject: %v", err) } w := newFactEnrichmentWorker(st, stubEcosystem(nexus.URL, ""), time.Hour) w.tick(ctx) facts, err := st.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 resolved to ent_espresso, got %+v", id, facts) } pending, err := st.PendingFactResolutions(ctx, 10) if err != nil { t.Fatalf("PendingFactResolutions: %v", err) } if len(pending) != 0 { t.Fatalf("expected no facts left pending, got %+v", pending) } } // TestFactEnrichmentWorker_NexusDownLeavesFactPending covers the fail-closed // contract: a Nexus outage must not mark a fact resolved/not_found — it // should stay pending so a later successful tick can still resolve it. func TestFactEnrichmentWorker_NexusDownLeavesFactPending(t *testing.T) { ctx := context.Background() nexus := newFakeNexus(t, fixtureNexusResolved("ent_espresso", "the espresso machine", "device")) nexus.SetFault(503) st := newTestStore(t) id, err := st.WriteFactAboutSubject(ctx, time.Now(), store.KindEnv, "likes", "the espresso machine", `"true"`, "infer:pref", 0.8, sql.NullInt64{}) if err != nil { t.Fatalf("WriteFactAboutSubject: %v", err) } w := newFactEnrichmentWorker(st, stubEcosystem(nexus.URL, ""), time.Hour) w.tick(ctx) pending, err := st.PendingFactResolutions(ctx, 10) if err != nil { t.Fatalf("PendingFactResolutions: %v", err) } if len(pending) != 1 || pending[0].ID != id { t.Fatalf("expected fact %d to remain pending during nexus outage, got %+v", id, pending) } } func TestFactEnrichmentWorker_AmbiguousLeavesEntityIDUnset(t *testing.T) { ctx := context.Background() nexus := newFakeNexus(t, fixtureNexusAmbiguous( map[string]string{"entity_id": "ent_kate_1", "display_name": "Kate Smith"}, map[string]string{"entity_id": "ent_kate_2", "display_name": "Kate Jones"}, )) st := newTestStore(t) id, err := st.WriteFactAboutSubject(ctx, time.Now(), store.KindEnv, "likes", "kate", `"true"`, "infer:pref", 0.8, sql.NullInt64{}) if err != nil { t.Fatalf("WriteFactAboutSubject: %v", err) } w := newFactEnrichmentWorker(st, stubEcosystem(nexus.URL, ""), time.Hour) w.tick(ctx) pending, err := st.PendingFactResolutions(ctx, 10) if err != nil { t.Fatalf("PendingFactResolutions: %v", err) } if len(pending) != 0 { t.Fatalf("ambiguous resolution should leave the pending queue, got %+v", pending) } facts, err := st.FactsByEntity(ctx, "ent_kate_1", 10) if err != nil { t.Fatalf("FactsByEntity: %v", err) } if len(facts) != 0 { t.Fatalf("ambiguous resolution must not guess an entity_id, got %+v", facts) } _ = id }