//go:build integration package integration import ( "context" "os" "strings" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/missionworkforce/missionmatch/internal/modules/ai" "github.com/missionworkforce/missionmatch/internal/platform/audit" ) // TestPlannerComposesACompoundCrossPlaneQuestion is the live keystone proof of THE PLANNER: a COMPOUND // question that names BOTH the market plane and the bench plane and carries the bindable params (a quoted // agency, a bare naics, and the requirement uuid) routes to the planner, which SELECTS a set of closed // Registry intents, runs each through its existing guarded path, and composes ONE answer behind the // faithfulness gate. The model authors no SQL and supplies no param value; every value comes from a // deterministic extractor. The proof is STRUCTURAL, not on exact LLM text, because generation is live. // // It seeds a per run org with a requirement (exercising the bench gap), a bench that exercises both sides of // the gap, and a market segment (an agency and an award under it). It then poses a compound question that // does NOT match the single shot graph classifier (no capture, recompete, incumbent, or competitor keyword), // so it reaches the planner, and asserts: the answer composes a market line AND a suppressed bench line, the // bench counts come through RenderCount (no raw sub k number leaks), no score is computed, and the counts // only aedt.ai.plan_answered audit holds only names, kinds, counts, and verdicts with NO value and NO PII. func TestPlannerComposesACompoundCrossPlaneQuestion(t *testing.T) { if os.Getenv("MM_MEGAMIND_URL") == "" { t.Skip("MM_MEGAMIND_URL not set; the planner needs the live inference plane to propose a plan") } plane := testPlane(t) org := seedOrg(t) k := ai.ConfiguredSuppressionThreshold() run := uuid.NewString()[:8] agency := "DEPT OF THE AIR FORCE " + run // unique per run so the market segment is isolated naics := "541512" // A requirement linked to a tenant opportunity whose federal_ref points at a global opportunity_records row // carrying the naics, so the bench gap and the segment both bind. Reuses the G6 seed helper. req := seedRequirementForCapture(t, org, agency, "secret", naics, run) // A mandatory required skill so the structural gap's skill arm is meaningful. skill := seedSkillByName(t, "PlannerSkill-"+run) seedRequiredSkill(t, org, req, skill, true) // MEETS: k candidates hold the required clearance and the skill. DOES NOT MEET: k candidates hold none, so // both gap buckets are at or above k and the bench legs publish real counts. for i := 0; i < k; i++ { c := seedCandidate(t, org, "secret", "active", true) seedCandidateSkill(t, org, c.ID, skill) } for i := 0; i < k; i++ { seedCandidate(t, org, "none", "active", true) } // The market segment: awards under the requirement's agency and naics, so a market intent has structure to // report whichever one the planner selects. recent := time.Now().UTC().AddDate(0, -3, 0) insideWindow := time.Now().UTC().AddDate(0, 6, 0) seedMarketAward(t, "PL-INC-"+run, "PIID-PL-IN", agency, naics, "UEIINCUMBENTP", "Planner Incumbent LLC", "6000000.00", insideWindow, false) seedMarketAward(t, "PL-CF-"+run, "PIID-PL-CF", agency, naics, "UEICHALLENGP2", "Planner Challenger LLC", "2000000.00", recent, false) orch := ai.NewOrchestrator(appDB, plane, audit.New(appDB)) // A COMPOUND question that names the market plane and the bench plane and carries the bindable params, but // avoids every single shot graph classifier keyword (no capture, recompete, incumbent, competitor, vendor, // teaming, buying, or bench coverage phrase), so it reaches the planner rather than a single intent route. question := `Across the federal market and our bench, for the "` + agency + `" in naics ` + naics + `, give an integrated picture for requirement ` + req.String() + `.` resp, err := orch.Answer(ctxOrg(org), question, "", nil) if err != nil { t.Fatalf("planner answer: %v", err) } if resp.Refused { t.Fatalf("an authenticated compound question must not be refused, got %+v", resp) } if resp.Route != ai.RoutePlan { t.Fatalf("a compound cross plane question must dispatch to the plan route, got %q (answer %q)", resp.Route, resp.Answer) } lower := strings.ToLower(resp.Answer) // (b) The answer composes a market line AND a suppressed bench line. The deterministic union rendering keys // each line by its intent name, so a market intent name and a bench intent name both present prove the cross // plane composition. We accept any market intent and any bench intent the planner selected. marketIntents := []string{ ai.IntentSegmentCompetitorField, ai.IntentAgencyVendorConcentration, ai.IntentAgencySpendTrend, ai.IntentVendorCooccurrenceField, ai.IntentRecompeteClock, ai.IntentRecompetePipeline, ai.IntentRecompeteTeamingField, ai.IntentAgencyBuyingPattern, ai.IntentAgencyBuyingPatternDetail, ai.IntentIncumbentOfRecord, } benchIntentNames := []string{ ai.IntentBenchGapForRequirement, ai.IntentBenchReadinessDistribution, ai.IntentBenchSkillCoverage, ai.IntentBenchClearanceCoverage, ai.IntentCapturePictureForRequirement, "proposal_ready_for_requirement", } // Read the ran intents from the audit (the structural truth of what the plan executed), and assert the plan // spanned both planes. intents := readPlanIntents(t, org) if len(intents) == 0 { t.Fatalf("the planner must run at least one intent; the answer was %q", resp.Answer) } if !containsAnyOf(intents, marketIntents) { t.Errorf("the plan must run a market plane intent, ran %v", intents) } if !containsAnyOf(intents, benchIntentNames) { t.Errorf("the plan must run a bench plane intent, ran %v", intents) } // (c) No raw sub k number leaks: when a bench cell is suppressed it renders as the sentinel, never a count. // Both seeded buckets are at or above k, so the bench legs should publish real counts and not suppress; but // if any cell suppresses, it must be the sentinel and never a bare sub k integer. We assert the sentinel // discipline holds: a suppressed marker present means the withheld phrasing is present too. if strings.Contains(resp.Answer, ai.SuppressionSentinel) && !strings.Contains(lower, "below reporting threshold") { t.Errorf("a suppressed bench cell must render as the sentinel, got %q", resp.Answer) } // (d) No engine score leaks: a total_score or a band value must never appear (the planner computes no score). for _, scoreWord := range []string{"total_score", "band="} { if strings.Contains(lower, scoreWord) { t.Errorf("the plan answer must report no engine score, leaked %q in %q", scoreWord, resp.Answer) } } // (e) The counts only aedt.ai.plan_answered audit holds only names, kinds, counts, and verdicts, with NO // param value, NO candidate id, NO requirement id, and NO answer text. payloads := readAudits(t, org, "aedt.ai.plan_answered") if len(payloads) == 0 { t.Fatal("a planned answer must emit an aedt.ai.plan_answered audit event") } for _, p := range payloads { if !strings.Contains(p, "intents") || !strings.Contains(p, "available_kinds") || !strings.Contains(p, "self_consistency") { t.Errorf("the plan audit must record the ran intents, the available kinds, and the self consistency outcome, got %q", p) } // NO requirement id, NO agency value, NO naics value, NO candidate marker, NO answer text in the audit. if strings.Contains(p, req.String()) { t.Errorf("the plan audit must hold no requirement id, leaked it in %q", p) } if strings.Contains(p, agency) { t.Errorf("the plan audit must hold no agency value, leaked it in %q", p) } for _, leak := range []string{"candidate", "answer", "question", "541512"} { if strings.Contains(p, leak) { t.Errorf("the plan audit must hold no candidate, answer, question, or param value, leaked %q in %q", leak, p) } } } } // TestPlannerLeavesTheSingleShotRouteUnchanged is the regression guard: a non compound single intent question // still takes the unchanged single shot path (the structured TAG route here), never the planner. The planner // is the fallback for compound questions, not a replacement for the working single shot routes. func TestPlannerLeavesTheSingleShotRouteUnchanged(t *testing.T) { if os.Getenv("MM_MEGAMIND_URL") == "" { t.Skip("MM_MEGAMIND_URL not set; skipping the planner regression guard") } org := seedOrg(t) for i := 0; i < 3; i++ { seedCandidate(t, org, "secret", "active", true) } orch := ai.NewOrchestrator(appDB, testPlane(t), audit.New(appDB)) // A single plane structured intent question (a named TAG intent) takes the TAG route, byte for byte as // before, never the planner. resp, err := orch.Answer(ctxOrg(org), "", "count_candidates_by_clearance", []any{"secret"}) if err != nil { t.Fatalf("single intent answer: %v", err) } if resp.Route == ai.RoutePlan { t.Errorf("a single intent question must not route to the planner, got route %q", resp.Route) } if resp.Route != ai.RouteTAG { t.Errorf("a named TAG intent must take the unchanged TAG route, got %q", resp.Route) } } // TestPlannerRefusesAnEEOLacedCompoundQuestion proves the EEO guard runs FIRST on the plan path: a compound // question that names a protected attribute is refused (route plan, no answer), never planned and never run. func TestPlannerRefusesAnEEOLacedCompoundQuestion(t *testing.T) { if os.Getenv("MM_MEGAMIND_URL") == "" { t.Skip("MM_MEGAMIND_URL not set; skipping the planner EEO refusal proof") } org := seedOrg(t) orch := ai.NewOrchestrator(appDB, testPlane(t), audit.New(appDB)) // A compound question (market AND bench plane words) that also asks about a protected attribute. The EEO and // protected attribute guards run first, so it refuses before any plan is proposed or run. question := `Across the market and our bench, break the bench down by race and tell me the incumbent for the agency.` resp, err := orch.Answer(ctxOrg(org), question, "", nil) if err != nil { t.Fatalf("an eeo laced compound question must refuse cleanly, got error %v", err) } if !resp.Refused { t.Fatalf("an eeo laced compound question must be refused, got %+v", resp) } if resp.Answer != "" { t.Errorf("a refusal must ship no answer, got %q", resp.Answer) } } // readPlanIntents reads the ran intent names from the most recent aedt.ai.plan_answered audit for an org. It // is the structural truth of what the plan executed, independent of the live model's prose. func readPlanIntents(t *testing.T, org uuid.UUID) []string { t.Helper() var intents []string payloads := readAudits(t, org, "aedt.ai.plan_answered") allNames := registryIntentNames() for _, p := range payloads { for _, name := range allNames { if strings.Contains(p, `"`+name+`"`) { intents = append(intents, name) } } } return intents } // readAudits reads the new_values text of every audit row for an org and action. func readAudits(t *testing.T, org uuid.UUID, action string) []string { t.Helper() var payloads []string if err := appDB.RunInTenantTx(ctxOrg(org), func(ctx context.Context, tx pgx.Tx) error { rows, qerr := tx.Query(ctx, "SELECT new_values::text FROM audit_logs WHERE organization_id=$1 AND action=$2", org, action) if qerr != nil { return qerr } defer rows.Close() for rows.Next() { var p string if serr := rows.Scan(&p); serr != nil { return serr } payloads = append(payloads, p) } return rows.Err() }); err != nil { t.Fatalf("read %s audit: %v", action, err) } return payloads } // registryIntentNames is the closed set of Registry intent names, used to read the ran intents out of the // audit payload by exact name match. func registryIntentNames() []string { return []string{ "count_candidates_by_clearance", "proposal_ready_for_requirement", "open_requirements_count", "slate_decision_mix", ai.IntentSkillSubstitutionClosure, ai.IntentSkillGapBridge, ai.IntentSegmentCompetitorField, ai.IntentIncumbentOfRecord, ai.IntentRecompetePipeline, ai.IntentAgencyBuyingPattern, ai.IntentProcurementSuccession, ai.IntentRecompeteClock, ai.IntentForecastRunway, ai.IntentAgencyBuyingPatternDetail, ai.IntentAgencyVendorConcentration, ai.IntentAgencySpendTrend, ai.IntentVendorCooccurrenceField, ai.IntentPartnerOrCompetitorRatio, ai.IntentRecompeteTeamingField, ai.IntentClearanceReachability, ai.IntentCompartmentRequirement, ai.IntentBenchClearanceCoverage, ai.IntentBenchSkillCoverage, ai.IntentBenchReadinessDistribution, ai.IntentBenchGapForRequirement, ai.IntentCapturePictureForRequirement, ai.IntentBidWinRateBySegment, } } // containsAnyOf reports whether the haystack contains any of the needles. func containsAnyOf(haystack, needles []string) bool { set := map[string]bool{} for _, h := range haystack { set[h] = true } for _, n := range needles { if set[n] { return true } } return false }