//go:build integration // WS7 structured mission scoped retrieval, exercised against a REAL Postgres with pgvector, full text, and // RLS in force. It proves SearchCandidatesHybridMissionTagged gates retrieval on the structured // candidate_mission_tags predicate, not on candidate text: it seeds two org A candidates whose text would // BOTH satisfy the WS6 search_tsv mission filter, tags only ONE of them SIGINT in candidate_mission_tags // (written through the APP pool under org A's context, so the write is RLS scoped), embeds both, runs the // tagged query, and asserts ONLY the tagged candidate returns. A second org's tagged candidate must never // leak. This mirrors TestHybridSearchMissionScopeExecutes but for the WS7 structured path. Mission tags are // never a scoring input and never reach the eeo schema; this test only exercises retrieval scoping and the // tenancy guarantee. It tests the query directly via dbgen with a deterministic embedding, so it needs no // inference client. package integration import ( "context" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/pgvector/pgvector-go" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" "github.com/missionworkforce/missionmatch/internal/platform/embeddings" ) func TestSearchCandidatesHybridMissionTaggedStructuredScope(t *testing.T) { orgA, orgB := seedOrg(t), seedOrg(t) emb := embeddings.NewDeterministic(384) // seedTagged seeds a candidate via the migrator (so the headline is set) and returns its id. The headline // deliberately contains the SIGINT keyword for EVERY seeded candidate, so the WS6 text filter would match // all of them; only the structured tag, applied selectively below, should narrow the WS7 result. seed := func(org uuid.UUID, name string) uuid.UUID { vec, _ := emb.Embed(context.Background(), name+" intelligence analyst") pv := pgvector.NewVector(vec) hn := "SIGINT collection and analysis" c, err := dbgen.New(migrator).CreateCandidate(context.Background(), dbgen.CreateCandidateParams{ OrganizationID: org, FullName: name, Headline: &hn, ClearanceLevel: "top_secret", ClearancePoly: "none", ClearanceStatus: "active", Available: true, ResumeEmbedding: &pv, }) if err != nil { t.Fatalf("seed candidate %q: %v", name, err) } return c.ID } taggedA := seed(orgA, "Alice Tagged") // org A, will be tagged SIGINT untaggedA := seed(orgA, "Bob Untagged") // org A, NOT tagged, must be excluded by the structured gate foreignB := seed(orgB, "Eve Foreign") // org B, tagged SIGINT, must never leak into org A's scope // Tag only Alice (org A) and Eve (org B) through the APP pool under each org's own context, so each write // is RLS scoped to that org. tag := func(org, cand uuid.UUID) { if err := appDB.RunInTenantTx(ctxOrg(org), func(ctx context.Context, tx pgx.Tx) error { return dbgen.New(tx).UpsertCandidateMissionTag(ctx, dbgen.UpsertCandidateMissionTagParams{ OrganizationID: org, CandidateID: cand, MissionCode: "SIGINT", Mechanism: "keyword", Evidence: "signals intelligence", }) }); err != nil { t.Fatalf("tag candidate %s for org %s: %v", cand, org, err) } } tag(orgA, taggedA) tag(orgB, foreignB) qv := pgvector.NewVector(mustEmbed(emb, "intelligence analyst")) var rows []dbgen.SearchCandidatesHybridMissionTaggedRow err := appDB.RunInTenantTx(ctxOrg(orgA), func(ctx context.Context, tx pgx.Tx) error { if _, e := tx.Exec(ctx, "SET LOCAL hnsw.ef_search = 200"); e != nil { return e } var e error rows, e = dbgen.New(tx).SearchCandidatesHybridMissionTagged(ctx, dbgen.SearchCandidatesHybridMissionTaggedParams{ QueryVector: qv, RowLimit: 10, TextQuery: "intelligence analyst", MissionCode: "SIGINT", }) return e }) if err != nil { t.Fatalf("structured tagged hybrid search failed: %v", err) } // Exactly the one tagged org A candidate returns: the untagged org A candidate is excluded by the // structured gate even though its text matches, and org B's tagged candidate is hidden by RLS. if len(rows) != 1 { names := make([]string, len(rows)) for i, r := range rows { names[i] = r.FullName } t.Fatalf("structured tagged scope returned %d rows %v, want exactly the 1 tagged candidate", len(rows), names) } got := rows[0] if got.ID != taggedA { t.Fatalf("structured tagged scope returned %s (%s), want the tagged candidate %s", got.FullName, got.ID, taggedA) } if got.OrganizationID != orgA { t.Fatalf("structured tagged scope leaked a foreign tenant row: %s (%s)", got.FullName, got.OrganizationID) } if got.ID == untaggedA { t.Fatal("structured tagged scope returned the UNTAGGED org A candidate, the structured gate did not apply") } if got.ID == foreignB { t.Fatal("ISOLATION BREACH: structured tagged scope returned org B's tagged candidate") } // The probe the retrieval branch uses must see org A's tag (so the structured path is chosen) and zero // for an untagged third org (so an un backfilled tenant takes the WS6 fallback). var nA int64 if err := appDB.RunInTenantTx(ctxOrg(orgA), func(ctx context.Context, tx pgx.Tx) error { var e error nA, e = dbgen.New(tx).CountCandidateMissionTags(ctx) return e }); err != nil { t.Fatalf("org A count probe: %v", err) } if nA < 1 { t.Fatalf("CountCandidateMissionTags for org A = %d, want at least 1 so the structured path is chosen", nA) } orgC := seedOrg(t) var nC int64 if err := appDB.RunInTenantTx(ctxOrg(orgC), func(ctx context.Context, tx pgx.Tx) error { var e error nC, e = dbgen.New(tx).CountCandidateMissionTags(ctx) return e }); err != nil { t.Fatalf("org C count probe: %v", err) } if nC != 0 { t.Fatalf("CountCandidateMissionTags for an untagged org = %d, want 0 so it falls back to the WS6 text filter", nC) } }