//go:build integration package integration import ( "context" "os" "testing" "time" "github.com/missionworkforce/missionmatch/internal/modules/ai" "github.com/missionworkforce/missionmatch/internal/platform/inference" ) func aiClient(t *testing.T) *inference.Client { t.Helper() url := os.Getenv("MM_MEGAMIND_URL") if url == "" { t.Skip("MM_MEGAMIND_URL unset; skipping the AI layer integration test") } c, err := inference.New(inference.Config{BaseURL: url, EmbedModel: "all-minilm", Dimension: 384, Timeout: 60 * time.Second}) if err != nil { t.Fatalf("inference client: %v", err) } return c } // TestAIMemoryErasure proves per tenant memory and that the right to erasure wipes it: a semantic memory // about a candidate is stored, recalled, and then removed by ForgetCandidate. func TestAIMemoryErasure(t *testing.T) { client := aiClient(t) org := seedOrg(t) cand := seedCandidate(t, org, "secret", "active", true) ctx := ctxOrg(org) mem := ai.NewMemory(appDB, client) if err := mem.Store(ctx, &cand.ID, "semantic", "this candidate excelled at incident response under pressure"); err != nil { t.Fatalf("store memory: %v", err) } recalled, err := mem.Recall(ctx, "incident response", 5) if err != nil { t.Fatalf("recall: %v", err) } if len(recalled) == 0 { t.Fatal("a stored semantic memory should be recallable by similarity") } if err := mem.ForgetCandidate(ctx, cand.ID); err != nil { t.Fatalf("forget: %v", err) } // Count via the migrator (BYPASSRLS) to be certain the rows are physically gone, not just hidden. var remaining int if err := migrator.QueryRow(context.Background(), "SELECT count(*) FROM ai_memory WHERE candidate_id=$1", cand.ID).Scan(&remaining); err != nil { t.Fatalf("count: %v", err) } if remaining != 0 { t.Fatalf("erasure must wipe AI memory about the candidate, %d rows survived", remaining) } } // TestKAGSkillAdjacency proves the KAG skill graph is live over the populated embeddings. func TestKAGSkillAdjacency(t *testing.T) { if os.Getenv("MM_MEGAMIND_URL") == "" { t.Skip("requires the populated skills.embedding (run on the backfilled database)") } org := seedOrg(t) adj, err := ai.SkillAdjacency(ctxOrg(org), appDB, "AWS", 5) if err != nil { t.Fatalf("SkillAdjacency: %v", err) } if len(adj) == 0 { t.Skip("no skill embeddings in this database; run cmd/embed-backfill first") } for _, a := range adj { if a.Name == "AWS" { t.Fatal("a skill must not be its own neighbor") } } } // TestTAGIntentRunsReadOnly proves a curated TAG intent executes RLS scoped and returns the database's // own aggregate (no model authored SQL). func TestTAGIntentRunsReadOnly(t *testing.T) { org := seedOrg(t) seedCandidate(t, org, "secret", "active", true) seedCandidate(t, org, "top_secret", "active", true) intent, ok := ai.IntentByName("count_candidates_by_clearance") if !ok { t.Fatal("the count_candidates_by_clearance intent must be registered") } res, err := ai.ExecuteIntent(ctxOrg(org), appDB, intent, []any{""}) if err != nil { t.Fatalf("ExecuteIntent: %v", err) } if len(res.Columns) != 2 || res.Columns[0] != "clearance_level" { t.Fatalf("unexpected columns: %v", res.Columns) } if len(res.Rows) == 0 { t.Fatal("the org has candidates; the aggregate should return rows") } }