//go:build integration // Capture Readiness (Phase 1) integration. Proves at runtime against a REAL Postgres and the migrated 0041 // weights what the unit tests cannot: the AI brain composes the bid pursue decision for an opportunity from four // grounded opportunity level factors (staffing readiness of the requirements, the segment win likelihood and // engagement depth from the caller's own bid ledger, the agency incumbency position from the public award // reference), reads the tunable weights from the coefficient store, produces an exact decimal Capture Readiness // Score and a GO / REVIEW / NO_GO recommendation with a per factor trace and a disclosure, RLS scoped, and // computes NO per candidate score. // // Run: go test -tags=integration ./internal/integration/ -run CaptureReadiness -v // Env: MM_APP_DSN, MM_MIGRATOR_DSN. package integration import ( "context" "strings" "testing" "github.com/google/uuid" "github.com/missionworkforce/missionmatch/internal/modules/ai" "github.com/missionworkforce/missionmatch/internal/modules/bidoutcomes" "github.com/missionworkforce/missionmatch/internal/platform/audit" ) func TestCaptureReadinessComposesTheBidDecision(t *testing.T) { ctx := context.Background() org := seedOrg(t) run := uuid.NewString()[:8] agency := "CAPTURE AGENCY " + run // An open opportunity in the agency. oppID := uuid.New() if _, err := migrator.Exec(ctx, `INSERT INTO opportunities (id, organization_id, name, customer_agency, status) VALUES ($1,$2,$3,$4,'open')`, oppID, org, "Capture Test "+run, agency); err != nil { t.Fatalf("seed opportunity: %v", err) } // Two requirements under the opportunity; one has a proposal ready (green) candidate, so staffing readiness // is one of two, 0.5. req1, req2 := uuid.New(), uuid.New() for _, r := range []uuid.UUID{req1, req2} { if _, err := migrator.Exec(ctx, `INSERT INTO requirements (id, organization_id, opportunity_id, title, customer_agency, required_level, required_poly) VALUES ($1,$2,$3,$4,$5,'secret','none')`, r, org, oppID, "Req "+r.String()[:8], agency); err != nil { t.Fatalf("seed requirement: %v", err) } } cand := seedCandidate(t, org, "secret", "active", true) if _, err := migrator.Exec(ctx, `INSERT INTO candidate_evaluations (organization_id, candidate_id, requirement_id, total_score, band, clearance_score, skills_score, certs_score, customer_score, experience_score, location_score, explanation, model_version) VALUES ($1,$2,$3,95,'green',100,90,90,90,90,90,'seed','v1.4.0')`, org, cand.ID, req1); err != nil { t.Fatalf("seed green evaluation: %v", err) } // Bid history in the segment: two wins and one loss, so the win rate is two thirds and the engagement depth // is three decided bids. svc := bidoutcomes.NewService(appDB, nil) recordOutcome(t, svc, org, req1, bidoutcomes.ResultWin) recordOutcome(t, svc, org, req1, bidoutcomes.ResultWin) recordOutcome(t, svc, org, req2, bidoutcomes.ResultLoss) // Agency award structure: one dominant incumbent (80 percent share), so the incumbency position is 0.2. for _, a := range []struct { uei string amt int }{{"INCUMBENT-" + run, 800000}, {"OTHER-" + run, 200000}} { if _, err := migrator.Exec(ctx, `INSERT INTO usaspending_awards (generated_internal_id, recipient_uei, award_amount, awarding_agency, raw) VALUES ($1,$2,$3,$4,'{}')`, "gen-"+a.uei, a.uei, a.amt, agency); err != nil { t.Fatalf("seed award: %v", err) } } orch := ai.NewOrchestrator(appDB, nil, audit.New(appDB)) resp, err := orch.Answer(ctxOrg(org), "", ai.IntentOpportunityCaptureReadiness, []any{oppID.String()}) if err != nil { t.Fatalf("capture readiness answer: %v", err) } if resp.Refused { t.Fatalf("an authenticated capture readiness question must not be refused, got %+v", resp) } if resp.Route != ai.RouteGraph { t.Errorf("capture readiness must dispatch to the graph route, got %q", resp.Route) } // (a) The answer carries a Capture Readiness Score and a GO / REVIEW / NO_GO recommendation. if !strings.Contains(resp.Answer, "Capture Readiness Score") { t.Errorf("the answer must state the Capture Readiness Score, got %q", resp.Answer) } hasBand := strings.Contains(resp.Answer, "GO") || strings.Contains(resp.Answer, "REVIEW") || strings.Contains(resp.Answer, "NO_GO") if !hasBand { t.Errorf("the answer must state a GO / REVIEW / NO_GO recommendation, got %q", resp.Answer) } // (b) All four factors are named in the trace. for _, factor := range []string{"staffing_readiness", "market_win_likelihood", "incumbency_position", "past_performance"} { if !strings.Contains(resp.Answer, factor) { t.Errorf("the trace must name the %q factor, got %q", factor, resp.Answer) } } // (c) The disclosure is present and it names no per candidate score. if !strings.Contains(strings.ToLower(resp.Answer), "not a per candidate match score") { t.Errorf("the answer must disclose it is not a per candidate match score, got %q", resp.Answer) } for _, leak := range []string{"total_score", "band=green", "scorecandidate"} { if strings.Contains(strings.ToLower(resp.Answer), leak) { t.Errorf("capture readiness must leak no per candidate score, found %q in %q", leak, resp.Answer) } } // (d) With staffing 0.5, market 0.667, incumbency 0.2, past 0.3 and the seeded weights, the score is about // 0.35*0.5 + 0.30*0.667 + 0.20*0.2 + 0.15*0.3 = 0.46 => REVIEW. Assert the band is REVIEW. if !strings.Contains(resp.Answer, "Recommendation: REVIEW") { t.Errorf("the seeded scenario must land in REVIEW (score ~0.46), got %q", resp.Answer) } }