//go:build integration // Shadow mode + human promotion proof: a shadow run proposes a candidate profile WITHOUT flipping // the active scorer, the loader keeps returning the default until promotion, and Promote activates // the candidate. package integration import ( "context" "encoding/json" "testing" "time" "github.com/jackc/pgx/v5" "github.com/missionworkforce/missionmatch/internal/aioe" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" ) func TestAioeShadowModeThenPromote(t *testing.T) { pool := aioePool(t) // skips without MM_AIOE_DSN ctx := context.Background() org := seedOrg(t) // Record enough positive outcomes to move a weight. scores := map[string]string{ "clearance": "100", "skills": "95", "certifications": "50", "customer_experience": "50", "experience": "60", "location": "10", } meta, _ := json.Marshal(map[string]any{"dimension_scores": scores}) if err := appDB.RunInTenantTx(ctxOrg(org), func(ctx context.Context, tx pgx.Tx) error { for i := 0; i < 40; i++ { if _, err := tx.Exec(ctx, ` INSERT INTO outcome_events (organization_id, module, subject_id, event_type, signal_value, occurred_at, metadata) VALUES ($1, 'screen', gen_random_uuid(), 'screening_decision', 1, now() - interval '1 day', $2)`, org, meta); err != nil { return err } } return nil }); err != nil { t.Fatalf("seed outcomes: %v", err) } // SHADOW run: proposes a candidate, does NOT activate. shadow := aioe.NewUpdater(pool, aioe.DefaultParams(), nil).WithShadow(true) if _, err := shadow.RunAll(ctx, time.Now(), "admin"); err != nil { t.Fatalf("shadow run: %v", err) } // There is a candidate, and NO active profile: the scorer still uses the global default. if err := appDB.RunInTenantTx(ctxOrg(org), func(ctx context.Context, tx pgx.Tx) error { var candidates, actives int if err := tx.QueryRow(ctx, `SELECT count(*) FILTER (WHERE status='candidate'), count(*) FILTER (WHERE status='active') FROM aioe_weight_profiles WHERE organization_id = $1`, org).Scan(&candidates, &actives); err != nil { return err } if candidates != 1 { t.Fatalf("shadow run must leave exactly 1 candidate, got %d", candidates) } if actives != 0 { t.Fatalf("shadow run must NOT activate a profile, got %d active", actives) } _, label, lerr := aioe.LoadActiveWeightsOnTx(ctx, tx) if lerr != nil { return lerr } if label != engine.GlobalDefaultProfileVersion { t.Fatalf("scorer must still use the default before promotion, got %q", label) } return nil }); err != nil { t.Fatal(err) } // A second shadow run refreshes the candidate: still exactly one, still no active. if _, err := shadow.RunAll(ctx, time.Now(), "admin"); err != nil { t.Fatalf("shadow run 2: %v", err) } if err := appDB.RunInTenantTx(ctxOrg(org), func(ctx context.Context, tx pgx.Tx) error { var candidates int if err := tx.QueryRow(ctx, `SELECT count(*) FROM aioe_weight_profiles WHERE organization_id = $1 AND status='candidate'`, org).Scan(&candidates); err != nil { return err } if candidates != 1 { t.Fatalf("a second shadow run must keep exactly 1 pending candidate, got %d", candidates) } return nil }); err != nil { t.Fatal(err) } // PROMOTE: the human activates the candidate. rep, err := shadow.Promote(ctx, org) if err != nil { t.Fatalf("promote: %v", err) } if rep.PromotedVersion < 1 { t.Fatalf("promote should name the activated version, got %+v", rep) } // Now the scorer loads the promoted profile, and there is no lingering candidate. if err := appDB.RunInTenantTx(ctxOrg(org), func(ctx context.Context, tx pgx.Tx) error { var candidates, actives int if err := tx.QueryRow(ctx, `SELECT count(*) FILTER (WHERE status='candidate'), count(*) FILTER (WHERE status='active') FROM aioe_weight_profiles WHERE organization_id = $1`, org).Scan(&candidates, &actives); err != nil { return err } if actives != 1 || candidates != 0 { t.Fatalf("after promote expected 1 active 0 candidate, got active=%d candidate=%d", actives, candidates) } w, label, lerr := aioe.LoadActiveWeightsOnTx(ctx, tx) if lerr != nil { return lerr } if label == engine.GlobalDefaultProfileVersion { t.Fatal("scorer must load the promoted tenant profile, still got the default") } if w.Skills.LessThanOrEqual(aioe.DefaultWeightsMap()["skills"]) { t.Fatalf("promoted profile should carry the raised skills weight, got %s", w.Skills) } return nil }); err != nil { t.Fatal(err) } // Promote again with no pending candidate is a safe no op. rep2, err := shadow.Promote(ctx, org) if err != nil { t.Fatalf("promote no-op: %v", err) } if rep2.PromotedVersion != 0 { t.Fatalf("second promote with no candidate should be a no op, got %+v", rep2) } }